33## Prerequisites
44
551 . AWS account with appropriate permissions
6- 2 . Terraform installed locally
6+ 2 . AWS CLI installed and configured
773 . Supabase project set up
884 . Redis instance (recommend Upstash for serverless Redis)
9- 5 . GitHub repository with the main branch
9+ 5 . GitHub repository (PolicyEngine/policyengine-api-v2-alpha)
1010
11- ## Step 1: Set up AWS credentials for Terraform
11+ ## Step 1: Create Terraform state bucket
1212
13- Install and configure AWS CLI with SSO (no long-term keys) :
13+ The S3 bucket stores Terraform state and enables automated deployments :
1414
1515``` bash
16- aws configure sso
16+ make create-state-bucket
1717```
1818
19- Follow the prompts to authenticate via your browser .
19+ This creates ` policyengine-api-v2-terraform-state ` with versioning enabled .
2020
21- ## Step 2: Deploy infrastructure with Terraform
21+ ** Note ** : Only needs to be done once.
2222
23- 1 . Create ` terraform/terraform.tfvars ` :
23+ ## Step 2: Set up GitHub OIDC
2424
25- ``` hcl
26- aws_region = "us-east-1"
27- project_name = "policyengine-api-v2-alpha"
28- supabase_url = "https://your-project.supabase.co"
29- supabase_key = "your-anon-key"
30- supabase_db_url = "postgresql://postgres:[password]@db.[project].supabase.co:5432/postgres"
31- redis_url = "redis://default:[password]@your-redis.upstash.io:6379"
32- logfire_token = "pylf_v1_us_..."
33- storage_bucket = "datasets"
34- api_cpu = "512"
35- api_memory = "1024"
36- api_desired_count = 1
37- worker_cpu = "1024"
38- worker_memory = "2048"
39- worker_desired_count = 1
40- ```
41-
42- 2 . Deploy:
25+ 1 . Get your AWS account ID:
4326
4427``` bash
45- cd terraform
46- terraform init
47- terraform plan
48- terraform apply
28+ aws sts get-caller-identity --query Account --output text
4929```
5030
51- 3 . Save the outputs :
31+ 2 . Create OIDC provider (if not already done) :
5232
5333``` bash
54- terraform output
34+ aws iam create-open-id-connect-provider \
35+ --url https://token.actions.githubusercontent.com \
36+ --client-id-list sts.amazonaws.com
5537```
5638
57- You'll get:
58- - ` ecr_repository_url ` - where to push Docker images
59- - ` load_balancer_url ` - your API endpoint
60- - ` ecs_cluster_name ` - cluster name for GitHub Actions
61- - ` api_service_name ` - API service name
62- - ` worker_service_name ` - worker service name
63-
64- ## Step 3: Set up GitHub OIDC (no access keys needed)
65-
66- 1 . Get your AWS account ID:
67-
68- ``` bash
69- aws sts get-caller-identity --query Account --output text
70- ```
71-
72- 2 . In AWS Console, go to IAM → Identity providers → Add provider:
73- - Provider type: OpenID Connect
74- - Provider URL: ` https://token.actions.githubusercontent.com `
75- - Audience: ` sts.amazonaws.com `
76- - Click "Add provider"
77-
78393 . Create IAM role for GitHub Actions:
79- - IAM → Roles → Create role
40+ - AWS Console → IAM → Roles → Create role
8041 - Trusted entity type: Web identity
8142 - Identity provider: ` token.actions.githubusercontent.com `
8243 - Audience: ` sts.amazonaws.com `
83- - GitHub organization: your-username-or-org
44+ - GitHub organization: ` PolicyEngine `
8445 - GitHub repository: ` policyengine-api-v2-alpha `
8546 - GitHub branch: ` main `
8647 - Click Next
8748
88494 . Attach these policies:
8950 - ` AmazonECS_FullAccess `
9051 - ` AmazonEC2ContainerRegistryPowerUser `
52+ - ` IAMFullAccess `
53+ - ` AmazonVPCFullAccess `
54+ - ` CloudWatchLogsFullAccess `
55+ - ` TerraformStateAccess ` (custom policy created earlier)
9156
92575 . Name the role: ` GitHubActionsDeployRole `
9358
94- 6 . After creation, copy the role ARN (looks like ` arn:aws:iam::123456789012 :role/GitHubActionsDeployRole ` )
59+ 6 . Copy the role ARN: ` arn:aws:iam::YOUR_ACCOUNT_ID :role/GitHubActionsDeployRole `
9560
96- ## Step 4 : Configure GitHub secrets and variables
61+ ## Step 3 : Configure GitHub secrets and variables
9762
98- Go to your GitHub repository → Settings → Secrets and variables → Actions
63+ Go to repo Settings → Secrets and variables → Actions
9964
100- ** Add this secret ** (Secrets tab):
65+ ** Add these secrets ** (Secrets tab):
10166
10267```
10368AWS_ROLE_ARN=arn:aws:iam::YOUR_ACCOUNT_ID:role/GitHubActionsDeployRole
69+ SUPABASE_URL=https://your-project.supabase.co
70+ SUPABASE_KEY=your-anon-key
71+ SUPABASE_DB_URL=postgresql://postgres:[password]@db.[project].supabase.co:5432/postgres
72+ REDIS_URL=redis://default:[password]@your-redis.upstash.io:6379
73+ LOGFIRE_TOKEN=pylf_v1_us_...
10474```
10575
10676** Add these variables** (Variables tab):
10777
10878```
109- AWS_REGION=us-east -1
79+ AWS_REGION=eu-north -1
11080ECR_REPOSITORY_NAME=policyengine-api-v2-alpha
11181ECS_CLUSTER_NAME=policyengine-api-v2-cluster
11282ECS_API_SERVICE_NAME=policyengine-api-v2-api
11383ECS_WORKER_SERVICE_NAME=policyengine-api-v2-worker
11484```
11585
116- ## Step 5 : Deploy from GitHub
86+ ## Step 4 : Deploy
11787
11888Push to the main branch:
11989
@@ -123,39 +93,81 @@ git push origin main
12393```
12494
12595GitHub Actions will automatically:
126- 1 . Build the Docker image
127- 2 . Push to ECR
128- 3 . Update ECS services
129- 4 . Wait for deployment to stabilise
96+ 1 . Set up Terraform
97+ 2 . Run ` terraform init `
98+ 3 . Run ` terraform plan `
99+ 4 . Run ` terraform apply ` (creates all infrastructure)
100+ 5 . Build Docker image
101+ 6 . Push to ECR
102+ 7 . Update ECS services
103+ 8 . Wait for deployment to stabilise
104+
105+ ** First deployment** : Takes ~ 10 minutes as it creates VPC, load balancer, ECS cluster, etc.
106+
107+ ** Subsequent deployments** : ~ 3-5 minutes (only updates Docker images and ECS tasks)
108+
109+ ## Step 5: Verify deployment
110+
111+ After deployment completes:
112+
113+ 1 . ** Get API endpoint** :
114+ ``` bash
115+ cd terraform
116+ terraform output load_balancer_url
117+ ```
118+
119+ 2 . ** Check health** :
120+ ``` bash
121+ curl http://YOUR-ALB-URL/health
122+ ```
123+
124+ 3 . ** View logs** :
125+ - AWS Console → CloudWatch → Log groups → ` /ecs/policyengine-api-v2 `
130126
131- ** Note** : After running Terraform, the ECS services will initially fail to start (no Docker image exists yet). Once you push to ` main ` and GitHub Actions completes, the services will automatically recover and start successfully.
127+ 4 . ** Monitor services** :
128+ - AWS Console → ECS → Clusters → policyengine-api-v2-cluster
129+
130+ ## Local deployment (optional)
131+
132+ Deploy infrastructure manually from your machine:
133+
134+ ``` bash
135+ make deploy-local
136+ ```
137+
138+ This runs Terraform with variables from your ` .env ` file and prompts for confirmation before applying.
132139
133140## Monitoring
134141
135- - View logs: AWS Console → CloudWatch → Log groups → ` /ecs/policyengine-api-v2 `
136- - View services: AWS Console → ECS → Clusters → policyengine-api-v2-cluster
137- - API endpoint: Check Terraform output for ` load_balancer_url `
142+ - ** Logs** : CloudWatch → ` /ecs/policyengine-api-v2 `
143+ - ** Services** : ECS → policyengine-api-v2-cluster
144+ - ** Metrics** : CloudWatch → ECS metrics
145+ - ** Logfire** : https://logfire-us.pydantic.dev/nikhilwoodruff/api-v2
138146
139147## Updating the deployment
140148
141- Any push to the ` main ` branch will trigger a new deployment automatically.
149+ Any push to ` main ` automatically:
150+ 1 . Updates infrastructure if Terraform files changed
151+ 2 . Builds new Docker image
152+ 3 . Deploys to ECS
142153
143- ## Cost estimates (us-east -1)
154+ ## Cost estimates (eu-north -1)
144155
145- - VPC/networking: Free (within free tier)
146- - Application Load Balancer: ~ $16/month
147- - ECS Fargate API (0.5 vCPU, 1GB): ~ $15/month
148- - ECS Fargate Worker (1 vCPU, 2GB): ~ $30/month
149- - CloudWatch Logs: ~ $1/month
156+ - VPC/networking: Free
157+ - Application Load Balancer: ~ €14/month
158+ - ECS Fargate API (0.5 vCPU, 1GB): ~ €13/month
159+ - ECS Fargate Worker (1 vCPU, 2GB): ~ €26/month
160+ - CloudWatch Logs: ~ €1/month
161+ - S3 (Terraform state): ~ €0.10/month
150162- Data transfer: Variable
151163
152- ** Total: ~ $62 /month** (plus data transfer and Redis costs)
164+ ** Total: ~ €54 /month** (plus data transfer and Redis costs)
153165
154166## Troubleshooting
155167
156168### GitHub Actions can't assume role
157169
158- Check the trust policy on your IAM role includes :
170+ Trust policy must match your repo exactly :
159171``` json
160172{
161173 "Version" : " 2012-10-17" ,
@@ -168,28 +180,53 @@ Check the trust policy on your IAM role includes:
168180 "Action" : " sts:AssumeRoleWithWebIdentity" ,
169181 "Condition" : {
170182 "StringEquals" : {
171- "token.actions.githubusercontent.com:aud" : " sts.amazonaws.com" ,
172- "token.actions.githubusercontent.com:sub" : " repo:YOUR_ORG/policyengine-api-v2-alpha:ref:refs/heads/main"
183+ "token.actions.githubusercontent.com:aud" : " sts.amazonaws.com"
184+ },
185+ "StringLike" : {
186+ "token.actions.githubusercontent.com:sub" : " repo:PolicyEngine/policyengine-api-v2-alpha:*"
173187 }
174188 }
175189 }
176190 ]
177191}
178192```
179193
194+ ** Note** : ` PolicyEngine ` is case-sensitive (capital P and E)
195+
196+ ### Terraform state locking errors
197+
198+ If deployment fails midway, you might see "state is locked". Wait 2 minutes for the lock to expire, or manually unlock:
199+
200+ ``` bash
201+ # DON'T run this unless you're sure no other process is running Terraform
202+ aws s3 rm s3://policyengine-api-v2-terraform-state/.terraform.tflock
203+ ```
204+
180205### ECS tasks not starting
181206
182207Check CloudWatch logs for errors. Common issues:
183- - Environment variables not set correctly
208+ - Environment variables not set in GitHub secrets
184209- Supabase/Redis connection issues
185210- Image build failures
186211
187- ### Deployment timeout
212+ ### High costs
213+
214+ Reduce task resources in ` terraform/variables.tf ` :
215+ ``` hcl
216+ api_cpu = "256" # Lower from 512
217+ api_memory = "512" # Lower from 1024
218+ worker_desired_count = 0 # Disable worker when not needed
219+ ```
188220
189- Increase health check grace period in task definition if app takes long to start .
221+ Then push to trigger redeployment .
190222
191- ### High costs
223+ ## Destroying infrastructure
224+
225+ To tear everything down:
226+
227+ ``` bash
228+ cd terraform
229+ ./deploy.sh destroy
230+ ```
192231
193- - Reduce task CPU/memory in ` terraform.tfvars `
194- - Set ` api_desired_count = 0 ` and ` worker_desired_count = 0 ` when not in use
195- - Use AWS Cost Explorer to identify expensive resources
232+ ** Warning** : This deletes all resources and data. The Terraform state bucket is preserved for recovery.
0 commit comments