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-awsPrerequisites
- 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.pyThe first deployment creates all infrastructure automatically (VPC, ECS cluster, ALB, etc.).
How It Works
The deployment workflow:
- Certificate: Creates/validates an ACM SSL certificate for your domain
- Infrastructure: Provisions baseline AWS resources via CloudFormation
- Build: Builds and pushes your Docker image to ECR
- Deploy: Creates an ECS service with ALB target group
- Health Check: Waits for containers to become healthy
- Traffic Switch: Routes traffic to the new deployment
- 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",
},
)| Parameter | Type | Description |
|---|---|---|
dockerfile_path | Path | Path to your Dockerfile |
context_path | Path | Docker build context directory |
build_args | dict[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,
)| Parameter | Type | Default | Description |
|---|---|---|---|
cpu | str | "256" | CPU units (256, 512, 1024, 2048, 4096) |
memory | str | "512" | Memory in MB (512, 1024, 2048, etc.) |
desired_count | int | 2 | Number of tasks to run |
env_vars | dict[str, str] | {} | Environment variables |
drain_poll_seconds | int | 5 | Seconds between drain state checks |
drain_grace_seconds | int | 20 | Grace 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,
)| Parameter | Type | Default | Description |
|---|---|---|---|
path | str | "/_health" | Health check endpoint path |
interval_seconds | int | 30 | Time between health checks |
timeout_seconds | int | 5 | Health check timeout |
healthy_threshold | int | 2 | Successes to mark healthy |
unhealthy_threshold | int | 3 | Failures to mark unhealthy |
wait_for_health | bool | True | Wait for healthy before switching |
min_healthy_targets | int | 2 | Minimum 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,
)| Parameter | Type | Default | Description |
|---|---|---|---|
schedule_minutes | int | 1 | How often the reaper Lambda runs |
max_age_hours | float | 1.0 | Max age before forced cleanup |
deployment_timeout | float | 1.0 | Max 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:
- The deploy script outputs the ALB DNS name
- Create a CNAME record pointing your domain to the ALB DNS
- 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.comUsing 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.pyCost Considerations
Estimated monthly costs for a minimal deployment (2 tasks, t3.micro equivalent):
| Resource | Approximate 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