Skip to main content

Deployment

AWS Infrastructure

ECS Fargate + ALB + ElastiCache

Serverless containers behind a managed load balancer. Stateless API scales horizontally. All secrets injected from SSM at container start.

Client
Internet
HTTPS :443
DNS
Route 53
DNS
CNAME → ALB
public subnet
Load Balancer
ALB
TLS termination
health checks
private subnet
ECS Fargate
octopus tasks
uvicorn :9000
1–N replicas
CPU: 1024 | Mem: 2048
service discovery
ElastiCache
Redis 7
shared state
token cache
ECR
Container Registry
octopus:latest
ECS pull
SSM Parameter Store
Secrets + Config
/octopus/JWT_SECRET…
task env
CloudWatch
Logs + Metrics
/ecs/octopus-production

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 octopus already 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.


Dockerfile Build Flow

Multi-Stage Build — Layer Cache Optimization

Dependencies install once and are cached. Only app code changes on most rebuilds.

Stage 1 — deps
FROM python:3.11-slim AS deps
Layer 1 — changes rarely
COPY requirements.txt .
Install uv
uv pip install torch (CPU)
uv pip install -r requirements.txt
~3–5 min first run
<5s cached
copy .venv
Stage 2 — final
FROM deps AS final
Layer 2 — changes on code push
COPY apps/constellation/app/ ./app/
apt install curl (healthcheck)
EXPOSE 9000
CMD uvicorn app.main:app
<10s on code-only changes
push
ECR
octopus:production-latest
Only changed layers
are uploaded
(Docker layer diffing)
ECS pulls on deploy
Layer Cache Rule

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

VariableTypeDefaultDescription
aws_regionstringAWS region (e.g. eu-west-1)
acm_certificate_arnstring""ACM certificate ARN for HTTPS. Leave empty for HTTP-only.
image_tagstringproduction-latestDocker image tag to deploy from ECR
app_cpunumber1024CPU units for the app task (1024 = 1 vCPU)
app_memorynumber2048Memory in MiB for the app task
app_desired_countnumber1Number of ECS task replicas
app_workersnumber2Uvicorn worker processes per task
redis_cpunumber512CPU units for Redis task
redis_memorynumber1024Memory in MiB for Redis task
redis_image_tagstringlatestRedis Stack image tag
redis_argsstring--appendonly yes …Redis server startup arguments
log_retention_daysnumber30CloudWatch log retention (days)
ssm_prefixstringSSM parameter path prefix (e.g. octopus)
ssm_secret_valuesmap(string){}SecureString parameter initial values (never overwritten after creation)
ssm_plain_valuesmap(string){}String parameter values (synced on every apply)
alb_access_logs_bucketstring""S3 bucket for ALB access logs. Leave empty to disable.

AWS Resource Summary

ResourceName PatternPurpose
ECS Clusteroctopus-productionFargate cluster hosting app + Redis tasks
ECS Service (app)octopus-productionManages desired count, rolling updates
ECS Service (Redis)octopus-production-redisSidecar Redis via service discovery
ALBoctopus-prod-albPublic HTTPS entry point, health checks
ECR RepositoryoctopusContainer image storage
SSM Parameters/octopus/<KEY>Secrets (SecureString) and config (String)
CloudWatch Log Group/ecs/octopus-productionApplication logs
VPCoctopus-productionIsolated network with public + private subnets

Key Environment Variables (Production)

VariableSourceDescription
PORTTerraform staticContainer port (default 9000)
WORKERSTerraform staticUvicorn worker count (default 2)
REDIS_HOSTTerraform staticRedis hostname via service discovery
REDIS_PORTTerraform staticRedis port (6379)
SAGITTARIUS_REDIS_URLTerraform staticFull Redis URL for Sagittarius queue
SAGITTARIUS_ENABLEDTerraform staticEnable background intelligence pipeline
JWT_SECRET_KEYSSM SecureStringJWT signing secret
JWT_PUBLIC_KEYSSM SecureStringJWT verification public key
INTERNAL_API_KEYSSM SecureStringService-to-service auth key
GOOGLE_CLIENT_SECRETSSM SecureStringGoogle OAuth client secret
SLACK_CLIENT_SECRETSSM SecureStringSlack OAuth client secret
ENVIRONMENTSSM Stringproduction
CORS_ORIGINSSSM StringAllowed CORS origins
DATABASE_URLSSM StringPostgreSQL connection string
PUBLIC_API_URLSSM StringPublic-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)