Terraform scaffolding for the R|P infrastructure stack.
This scaffold is set up for:
- one EC2 instance per API
- a
t2.smallinstance for Hermes - a
t2.microinstance for the Terraform-managed RP API host - default VPC usage to keep costs and complexity down
- host-level Nginx terminating TLS and proxying to the app container
- CodeDeploy-managed app deployments onto EC2
- stable Elastic IPs so Cloudflare DNS can point directly at each API host
The production entrypoint now wires up:
- Hermes on its current host (retired, see below)
- the
rp-apihost servingapi.reflectionsprojections.org
The previous hand-provisioned api.reflectionsprojections.org box is out of rotation since the cutover and can be decommissioned once nothing on it is still needed.
environments/prod: production entrypointmodules/ec2_api_service: reusable EC2 + Nginx + CodeDeploy app module.github/workflows: R|P-owned deployment automation that builds public app repos and deploys them into the AWS environment
This scaffold intentionally does not write application secrets into Terraform state.
- Hermes keeps a manually managed host-side
.envfile at/home/ubuntu/hermes/.env. - rp-api reads its secrets from AWS Secrets Manager at deploy time. Terraform creates three empty secret shells (
rp-api/prod/env,rp-api/prod/firebase-admin-cert, andrp-api/prod/supabase-backup-env) and grants the instance rolesecretsmanager:GetSecretValueon them. Load the values manually in the AWS console as plaintext: the full.envfile content in the first, the Firebase service account JSON in the second, and the backup script's Postgres connection lines in the third. A CodeDeploy hook (services/api/scripts/load_env.shin rp-monorepo) fetches the first two on every deployment and writes/home/ubuntu/rp-api/.envand/home/ubuntu/rp-api/firebase-admin-cert.json; the backup script fetches the third at runtime and writes nothing to disk.
Terraform never reads the secret values, so they stay out of the state file. No secret file on any rp-api host is created by hand.
- Copy
environments/prod/terraform.tfvars.exampleto a localterraform.tfvars - Fill in the domain names and, if needed, an EC2 key pair name
- From
environments/prod, run:
terraform init
terraform plan
terraform apply- Cloudflare DNS is not yet managed in this scaffold. Terraform outputs the Elastic IPs you can point A records at.
- Nginx is configured to use stable per-service cert paths. The bootstrap script creates a temporary self-signed cert so Nginx can start cleanly before Certbot issues a real certificate.
- The RP API host gets postgresql-client for the Supabase backup script, which ships in the CodeDeploy bundle; scheduling remains entirely manual on the instance.
- Deployment automation lives here, not in the app repos themselves. This repo can safely hold AWS-specific GitHub Actions secrets/vars because it is the infrastructure owner.
- Hermes is retired: its deploy workflow has been removed, though its instance is still Terraform-managed. See the "Hermes (retired)" section below.
The backup script itself lives in rp-monorepo (services/api/scripts/supabase_backups.sh) and arrives on the host with every CodeDeploy bundle, so it never goes stale. It exports every public table to CSV and uploads to the rp-api-supabase-backups bucket. Terraform's part is:
install_supabase_backup_tools = trueon the service module installspostgresql-clientat first bootsupabase_backup_bucketgrants the instance roles3:PutObjecton the backup bucket- the
rp-api/prod/supabase-backup-envsecret shell holds the Postgres connection values the script fetches at runtime (nothing credential-bearing is created on the host)
Load these plaintext lines into the secret (values from the Supabase dashboard's session pooler; the DB password is the database password, not the service key):
DB_HOST=...
DB_PORT=...
DB_USER=...
DB_NAME=postgres
DB_PASSWORD=...Optional lines: S3_BUCKET (default rp-api-supabase-backups) and S3_PREFIX (default supabase).
Test it on the host with:
sudo /home/ubuntu/rp-api/scripts/supabase_backups.shNothing schedules it by default. For recurring backups, add a cron entry on the box (note the PATH line — cron does not include /usr/local/bin, where the AWS CLI lives):
PATH=/usr/local/bin:/usr/bin:/bin
0 9 * * * root /home/ubuntu/rp-api/scripts/supabase_backups.sh >> /var/log/supabase-backup.log 2>&1
Hermes now uses host-managed Let's Encrypt certificates rather than long-lived self-signed certs or a cert copied in from Terraform.
- Set
hermes_letsencrypt_emailinterraform.tfvars - Keep the DNS record for
hermes_domain_namepointed at the instance - The instance bootstrap will:
- start Nginx with a temporary self-signed certificate
- install Certbot
- attempt
certbot certonly --nginx - copy the issued certificate into the stable Nginx paths Terraform manages
- install a Certbot renewal hook so renewals keep those paths fresh
If first-boot issuance happens before DNS is ready, you can rerun it on the box with:
sudo /usr/local/bin/hermes-api-issue-letsencrypt-cert.shAfter the origin has a real certificate, Cloudflare should be set to Proxied with SSL/TLS mode Full (strict).
Hermes is no longer deployed. The deploy-hermes.yml workflow has been removed so nothing can trigger a Hermes deploy. The Terraform module for the Hermes instance is still in environments/prod and the instance is still running; remove the hermes_api module (and the Hermes statements in the deployer IAM policy) and apply when you are ready to decommission it.
The intended RP API deploy flow is:
- Terraform provisions the EC2 instance, Nginx, PM2, and the CodeDeploy resources
- A GitHub Actions workflow in
rp-infrachecks out therp-monoreporepository - The workflow packages the contents of
services/apias the CodeDeploy bundle root - The workflow uploads the zip to the RP API artifact bucket
- The workflow starts a CodeDeploy deployment against the RP API application and deployment group
The workflow authenticates to AWS with GitHub's OIDC provider: it assumes the
rp-github-deployer IAM role (defined in environments/prod), so there are no
long-lived AWS keys in repository secrets. The role's trust policy only accepts
workflow runs from the main branch of this repository.
The workflow needs the following repository configuration in rp-infra:
- Secrets:
RP_API_REPOSITORY_TOKEN
- Variables:
AWS_REGIONAWS_DEPLOY_ROLE_ARNRP_API_CODEDEPLOY_BUCKETRP_API_CODEDEPLOY_APP_NAMERP_API_CODEDEPLOY_DEPLOYMENT_GROUPRP_API_REPOSITORYRP_API_REF
Suggested values:
AWS_REGION=us-east-2AWS_DEPLOY_ROLE_ARN— thegithub_deployer_role_arnTerraform outputRP_API_CODEDEPLOY_BUCKET=rp-api-codedeploy-artifactsRP_API_CODEDEPLOY_APP_NAME=rp-api-codedeploy-appRP_API_CODEDEPLOY_DEPLOYMENT_GROUP=rp-api-deployment-groupRP_API_REPOSITORY=ReflectionsProjections/rp-monorepoRP_API_REF=main
If rp-monorepo is private, RP_API_REPOSITORY_TOKEN should be a token that can read that repository.