Pulse

Pulse AWS

Deploy Pulse apps to AWS using ECS Fargate with automated blue-green deployments, sticky sessions, and zero-downtime updates.

Installation

uv add pulse-aws

Prerequisites

  • AWS CLI configured with appropriate credentials
  • Docker installed and running
  • An AWS account with permissions to create ECS, ECR, ALB, VPC, and related resources
  • A domain name you control (for SSL certificates)

Quick Start

1. Configure Your App

Create a deploy.py script:

import asyncio
from pathlib import Path
from pulse_aws import deploy, DockerBuild, TaskConfig, HealthCheckConfig


async def main():
    result = await deploy(
        domain="app.example.com",
        deployment_name="prod",
        docker=DockerBuild(
            dockerfile_path=Path("Dockerfile"),
            context_path=Path("."),
        ),
    )
    print(f"Deployed: {result['deployment_id']}")
    print(f"URL: https://app.example.com")


if __name__ == "__main__":
    asyncio.run(main())

2. Create a Dockerfile

FROM python:3.12-slim

WORKDIR /app

# Install dependencies
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen

# Copy application
COPY . .

# Build frontend
RUN cd web && bun install && bun run build

# Run the app
EXPOSE 8000
CMD ["uv", "run", "pulse", "run", "app.py", "--host", "0.0.0.0"]

3. Deploy

AWS_PROFILE=your-profile uv run deploy.py

The first deployment creates all infrastructure automatically (VPC, ECS cluster, ALB, etc.).

How It Works

The deployment workflow:

  1. Certificate: Creates/validates an ACM SSL certificate for your domain
  2. Infrastructure: Provisions baseline AWS resources via CloudFormation
  3. Build: Builds and pushes your Docker image to ECR
  4. Deploy: Creates an ECS service with ALB target group
  5. Health Check: Waits for containers to become healthy
  6. Traffic Switch: Routes traffic to the new deployment
  7. Drain: Marks old deployments for cleanup

Blue-Green Deployments

Each deployment gets a unique ID (e.g., prod-20251027-183000Z). Traffic switches instantly once health checks pass. Old deployments drain gracefully - existing WebSocket connections stay active until the browser tab closes.

Sticky Sessions

Pulse uses header-based routing (X-Pulse-Render-Affinity) to ensure each browser tab stays connected to the same deployment. This prevents state inconsistencies during rolling updates.

Configuration

DockerBuild

Docker build settings:

from pulse_aws import DockerBuild

docker = DockerBuild(
    dockerfile_path=Path("Dockerfile"),
    context_path=Path("."),
    build_args={
        "PULSE_SERVER_ADDRESS": "https://app.example.com",
    },
)
ParameterTypeDescription
dockerfile_pathPathPath to your Dockerfile
context_pathPathDocker build context directory
build_argsdict[str, str]Additional build arguments

TaskConfig

ECS task configuration:

from pulse_aws import TaskConfig

task = TaskConfig(
    cpu="512",
    memory="1024",
    desired_count=2,
    env_vars={"LOG_LEVEL": "info"},
    drain_poll_seconds=5,
    drain_grace_seconds=20,
)
ParameterTypeDefaultDescription
cpustr"256"CPU units (256, 512, 1024, 2048, 4096)
memorystr"512"Memory in MB (512, 1024, 2048, etc.)
desired_countint2Number of tasks to run
env_varsdict[str, str]{}Environment variables
drain_poll_secondsint5Seconds between drain state checks
drain_grace_secondsint20Grace period before marking as draining

HealthCheckConfig

ALB health check settings:

from pulse_aws import HealthCheckConfig

health = HealthCheckConfig(
    path="/_pulse/health",
    interval_seconds=30,
    timeout_seconds=5,
    healthy_threshold=2,
    unhealthy_threshold=3,
    wait_for_health=True,
    min_healthy_targets=2,
)
ParameterTypeDefaultDescription
pathstr"/_health"Health check endpoint path
interval_secondsint30Time between health checks
timeout_secondsint5Health check timeout
healthy_thresholdint2Successes to mark healthy
unhealthy_thresholdint3Failures to mark unhealthy
wait_for_healthboolTrueWait for healthy before switching
min_healthy_targetsint2Minimum healthy targets required

ReaperConfig

Automated cleanup settings:

from pulse_aws import ReaperConfig

reaper = ReaperConfig(
    schedule_minutes=5,
    max_age_hours=24.0,
    deployment_timeout=1.0,
)
ParameterTypeDefaultDescription
schedule_minutesint1How often the reaper Lambda runs
max_age_hoursfloat1.0Max age before forced cleanup
deployment_timeoutfloat1.0Max deployment time before cleanup

Full Deployment Example

import asyncio
from pathlib import Path
from pulse_aws import (
    deploy,
    DockerBuild,
    TaskConfig,
    HealthCheckConfig,
    ReaperConfig,
)


async def main():
    result = await deploy(
        domain="app.example.com",
        deployment_name="prod",
        docker=DockerBuild(
            dockerfile_path=Path("Dockerfile"),
            context_path=Path("."),
            build_args={
                "PULSE_SERVER_ADDRESS": "https://app.example.com",
            },
        ),
        task=TaskConfig(
            cpu="512",
            memory="1024",
            desired_count=2,
            env_vars={
                "PULSE_SERVER_ADDRESS": "https://app.example.com",
            },
        ),
        health_check=HealthCheckConfig(
            path="/_pulse/health",
        ),
        reaper=ReaperConfig(
            schedule_minutes=5,
            max_age_hours=24.0,
        ),
    )

    print("Deployment complete!")
    print(f"  ID: {result['deployment_id']}")
    print(f"  Service: {result['service_arn']}")
    print(f"  Image: {result['image_uri']}")
    print(f"  URL: https://app.example.com")


if __name__ == "__main__":
    asyncio.run(main())

DNS Setup

After the first deployment, point your domain to the ALB:

  1. The deploy script outputs the ALB DNS name
  2. Create a CNAME record pointing your domain to the ALB DNS
  3. For apex domains, use an ALIAS record (Route 53) or Cloudflare proxy

Example DNS record:

app.example.com  CNAME  prod-baseline-alb-123456789.us-east-1.elb.amazonaws.com

Using the AWSECSPlugin

For apps that need to be aware of their deployment environment, use the plugin:

import pulse as ps
from pulse_aws import AWSECSPlugin

app = ps.App(
    routes=[...],
    plugins=[AWSECSPlugin()],
)

The plugin automatically:

  • Reads deployment configuration from environment variables
  • Handles graceful shutdown when marked for draining
  • Reports health status to CloudWatch

Teardown

To remove all AWS resources for a deployment:

from pulse_aws.teardown import teardown

await teardown(deployment_name="prod")

Or use the included script:

AWS_PROFILE=your-profile uv run packages/pulse-aws/scripts/teardown.py

Cost Considerations

Estimated monthly costs for a minimal deployment (2 tasks, t3.micro equivalent):

ResourceApproximate Cost
ECS Fargate (2 x 0.25 vCPU, 0.5 GB)~$15
Application Load Balancer~$20
NAT Gateway~$35
CloudWatch Logs~$5
Total~$75/month

For development/testing, consider:

  • Using a single task (desired_count=1)
  • Smaller CPU/memory allocations
  • Tearing down when not in use

On this page