Deployment
ECS Fargate + ALB + ElastiCache
Serverless containers behind a managed load balancer. Stateless API scales horizontally. All secrets injected from SSM at container start.
health checks
1–N replicas
CPU: 1024 | Mem: 2048
token cache
What & Why
ECS Fargate removes all server management — no EC2 instances to patch, no cluster nodes to scale. The API is fully stateless: every replica shares the same Redis ElastiCache instance for token caching and queue state. This means horizontal scaling is trivial — increase app_desired_count and Fargate spins up new tasks instantly.
Rolling deployments through the ALB ensure zero downtime: new tasks must pass health checks (GET /health) before the ALB routes traffic to them. Old tasks are drained gracefully.
All secrets (JWT keys, OAuth client credentials) are stored in SSM Parameter Store and injected as environment variables when the container starts. Terraform manages the parameter paths — you never commit secrets to the repo.
Step-by-Step Deployment Guide
Prerequisites
- AWS CLI configured with credentials that have ECS, ECR, SSM, and IAM permissions
- Terraform ≥ 1.5 installed
- Docker installed and running
- ECR repository
octopusalready created in your AWS account
1. Configure AWS credentials
aws configure
# AWS Access Key ID: <your-key>
# AWS Secret Access Key: <your-secret>
# Default region name: eu-west-1
# Default output format: json
Verify access:
aws sts get-caller-identity
2. Initialize Terraform
cd infra/production
terraform init
This downloads the AWS provider and initializes the S3 backend. Run once per new workspace or after upgrading providers.
3. Plan changes
terraform plan -var-file=terraform.tfvars
Review the plan output. Expect changes only when infrastructure config or variable values have changed. A clean deployment with no infra changes will show No changes.
For the first deploy, or when rotating secrets, pass secret values inline:
terraform plan \
-var-file=terraform.tfvars \
-var='ssm_secret_values={"JWT_SECRET_KEY":"...","INTERNAL_API_KEY":"..."}'
4. Apply infrastructure
terraform apply -var-file=terraform.tfvars
Confirm with yes. Terraform creates or updates VPC, subnets, ALB, ECS cluster, ECS service, ElastiCache Redis, SSM parameters, IAM roles, and CloudWatch log groups.
Key outputs after apply:
alb_dns_name = "octopus-prod-alb-xxxxxxxxx.eu-west-1.elb.amazonaws.com"
ecs_cluster_name = "octopus-production"
ecs_service_name = "octopus-production"
redis_host = "redis.octopus-production.local"
5. Build the Docker image
# From the repo root
docker build -t octopus .
The multi-stage build installs dependencies in a deps stage and copies only what is needed into the final stage. First build takes 3–5 minutes (PyTorch CPU download). Subsequent builds use layer cache and complete in under 30 seconds when only app code changed.
6. Push to ECR
# Set your ECR URL
ECR_URL="<aws-account-id>.dkr.ecr.eu-west-1.amazonaws.com"
# Authenticate Docker to ECR
aws ecr get-login-password --region eu-west-1 \
| docker login --username AWS --password-stdin "${ECR_URL}"
# Tag the image
docker tag octopus:latest "${ECR_URL}/octopus:production-latest"
# Push
docker push "${ECR_URL}/octopus:production-latest"
7. Deploy to ECS
Force a new deployment so ECS pulls the freshly pushed image:
aws ecs update-service \
--cluster octopus-production \
--service octopus-production \
--force-new-deployment \
--region eu-west-1
ECS starts new tasks, waits for ALB health checks to pass on GET /health, then drains old tasks. Rolling update takes 1–3 minutes depending on health check intervals.
8. Monitor the deployment
Watch the deployment reach a steady state:
aws ecs describe-services \
--cluster octopus-production \
--services octopus-production \
--region eu-west-1 \
--query 'services[0].{status:status,running:runningCount,desired:desiredCount,deployments:deployments[*].{id:id,status:status,running:runningCount,desired:desiredCount}}' \
--output table
Stream live logs:
aws logs tail /ecs/octopus-production --follow --region eu-west-1
Deployment is complete when runningCount == desiredCount and the PRIMARY deployment shows COMPLETED.
Multi-Stage Build — Layer Cache Optimization
Dependencies install once and are cached. Only app code changes on most rebuilds.
uv pip install torch (CPU)
uv pip install -r requirements.txt
<5s cached
EXPOSE 9000
CMD uvicorn app.main:app
are uploaded
(Docker layer diffing)
requirements.txt is copied before app code. When only Python source files change, the deps stage is fully cached — Docker skips the 3–5 minute dependency install and jumps straight to copying app code.
Reference
Terraform Variables
| Variable | Type | Default | Description |
|---|---|---|---|
aws_region | string | — | AWS region (e.g. eu-west-1) |
acm_certificate_arn | string | "" | ACM certificate ARN for HTTPS. Leave empty for HTTP-only. |
image_tag | string | production-latest | Docker image tag to deploy from ECR |
app_cpu | number | 1024 | CPU units for the app task (1024 = 1 vCPU) |
app_memory | number | 2048 | Memory in MiB for the app task |
app_desired_count | number | 1 | Number of ECS task replicas |
app_workers | number | 2 | Uvicorn worker processes per task |
redis_cpu | number | 512 | CPU units for Redis task |
redis_memory | number | 1024 | Memory in MiB for Redis task |
redis_image_tag | string | latest | Redis Stack image tag |
redis_args | string | --appendonly yes … | Redis server startup arguments |
log_retention_days | number | 30 | CloudWatch log retention (days) |
ssm_prefix | string | — | SSM parameter path prefix (e.g. octopus) |
ssm_secret_values | map(string) | {} | SecureString parameter initial values (never overwritten after creation) |
ssm_plain_values | map(string) | {} | String parameter values (synced on every apply) |
alb_access_logs_bucket | string | "" | S3 bucket for ALB access logs. Leave empty to disable. |
AWS Resource Summary
| Resource | Name Pattern | Purpose |
|---|---|---|
| ECS Cluster | octopus-production | Fargate cluster hosting app + Redis tasks |
| ECS Service (app) | octopus-production | Manages desired count, rolling updates |
| ECS Service (Redis) | octopus-production-redis | Sidecar Redis via service discovery |
| ALB | octopus-prod-alb | Public HTTPS entry point, health checks |
| ECR Repository | octopus | Container image storage |
| SSM Parameters | /octopus/<KEY> | Secrets (SecureString) and config (String) |
| CloudWatch Log Group | /ecs/octopus-production | Application logs |
| VPC | octopus-production | Isolated network with public + private subnets |
Key Environment Variables (Production)
| Variable | Source | Description |
|---|---|---|
PORT | Terraform static | Container port (default 9000) |
WORKERS | Terraform static | Uvicorn worker count (default 2) |
REDIS_HOST | Terraform static | Redis hostname via service discovery |
REDIS_PORT | Terraform static | Redis port (6379) |
SAGITTARIUS_REDIS_URL | Terraform static | Full Redis URL for Sagittarius queue |
SAGITTARIUS_ENABLED | Terraform static | Enable background intelligence pipeline |
JWT_SECRET_KEY | SSM SecureString | JWT signing secret |
JWT_PUBLIC_KEY | SSM SecureString | JWT verification public key |
INTERNAL_API_KEY | SSM SecureString | Service-to-service auth key |
GOOGLE_CLIENT_SECRET | SSM SecureString | Google OAuth client secret |
SLACK_CLIENT_SECRET | SSM SecureString | Slack OAuth client secret |
ENVIRONMENT | SSM String | production |
CORS_ORIGINS | SSM String | Allowed CORS origins |
DATABASE_URL | SSM String | PostgreSQL connection string |
PUBLIC_API_URL | SSM String | Public-facing API base URL |
Health Check Endpoint
GET /health
The ALB uses this endpoint to determine whether a task is ready to receive traffic. A task failing health checks is replaced automatically.
Response (200 OK):
{
"status": "ok"
}
ECS task definition health check configuration:
- Command:
curl -f http://localhost:9000/health - Interval: 30 seconds
- Timeout: 10 seconds
- Retries: 3
- Start period: 60 seconds (allows time for model loading on cold start)