Workshops ... Part 12: Deploy to AWS with infrastructure as code

Part 12: Deploy to AWS with infrastructure as code

Locally we run the app and Postgres together with Docker Compose. Now we put the same app on a real server.

I'll use AWS, but the setup is the same on any cloud: a managed Postgres and a box to run the container. You can deploy to Google Cloud, Azure, Hetzner, or anywhere else that gives you those two things.

A quick managed-host detour

During the workshop we first tried the fastest possible path: a managed host. The assistant suggested render, railway, and fly.io. We went with render, which deploys straight from the Dockerfile. render's free tier no longer includes a managed Postgres. So we put the database on Aiven, which has a free-tier Postgres, and pointed the app's SNAKE_ROYALE_DATABASE_URL at it.

It came up, but the free-tier container kept restarting. So we tore it back down and moved to AWS for a setup we control end to end. The detour still teaches something. For a small proof of concept, a managed host plus a free hosted Postgres like Aiven is the quickest way to get a public URL.

The AWS setup

We have two components to host:

  • the Postgres database, which we run on AWS RDS
  • the app, which runs as a single container on an EC2 instance (t3.medium)

AWS has other ways to run a container, such as Fargate, ECS, or EKS.

They're worth it once you run many services. For one container, a single EC2 instance is the simplest thing that works, and the cheapest to leave running.

We also want the infrastructure written down rather than clicked together by manually, so we describe it as code with CloudFormation.

Terraform, the AWS CDK, and Pulumi all work the same way, so use whichever you know.

Give the assistant AWS access

The deploy runs from your environment, so the AWS CLI there needs credentials. Those come from an IAM user with an access key.

Create the user in the AWS console, under IAM:

  • Go to Users and add a user. It doesn't need console access, since we only reach it from the CLI.
  • Open its Security credentials and create an access key for command-line use.

Put the key and a region in a top-level .env:

AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=eu-west-1

dirdotenv will automatically load these credentials.

Permissions for the IAM user

CloudFormation does the provisioning, but it acts as this user, so the user has to be allowed to create everything in the template.

For our case we need:

  • CloudFormation, to create and delete the stack.
  • EC2, for the instance, its security group, and networking.
  • RDS, for the Aurora database.
  • Secrets Manager, for the database credentials.
  • IAM, because the template creates a role for the instance. That's what the CAPABILITY_NAMED_IAM flag in the deploy command acknowledges.

You can attach the AWS managed policy for each of those services.

Working out the exact least-privilege set is fiddly, so for a workshop it's simpler to attach AdministratorAccess and move on.

Be careful with that, though, because an admin key can do anything in your account. Keep it out of git and use it only from the throwaway Codespace. Once the pipeline in Part 13: CI/CD with GitHub Actions is running, the deploy authenticates with short-lived OIDC credentials and nothing needs this key. Remove the AdministratorAccess policy, or delete the user outright, at that point.

The resources we provision

Ask the assistant to write the template:

Create a CloudFormation template that deploys this app to AWS, with:

- an Aurora PostgreSQL (Serverless v2) database
- the database user and password stored in Secrets Manager
- a single EC2 instance that reads those secrets at boot, builds the image, and
  runs the container with docker
- a public URL for the app
- an idempotent deploy: re-running it must not rotate the secrets

Running costs

Here's roughly what each piece costs to leave running (approximate, us-east-1, mid-2026):

  • EC2 t3.medium: about $0.04 an hour, so roughly $30 a month if you leave it on.
  • Aurora Serverless v2 (PostgreSQL): about $0.06 an hour at its 0.5-ACU floor, so roughly $40 a month, plus about $0.10 per GB-month of storage.
  • Secrets Manager: about $0.40 per secret a month, so under a dollar for the two.
  • Data transfer and the other small charges add a few dollars.

A stack left running costs on the order of a couple of dollars a day, so delete it when you're not using it. We tear everything down in Part 14: Delete the AWS resources.

Deploy it

The assistant wraps the deploy in a helper script, scripts/aws-deploy.sh, so you don't retype the long command.

Run it from the repo root:

./scripts/aws-deploy.sh

It calls aws cloudformation deploy against infra/cloudformation.yaml, passing the repo URL the instance builds from and the Aurora engine version. The first deploy takes a few minutes, mostly waiting on Aurora. When it finishes, the script prints the app's public URL - the AppUrl stack output.

You can read that URL again any time:

aws cloudformation describe-stacks --stack-name snake-royale \
  --query "Stacks[0].Outputs[?OutputKey=='AppUrl'].OutputValue" --output text

Open it and the app is live on AWS, backed by Aurora. AppUrl is an HTTPS URL served through CloudFront. The template also creates a GitHub Actions OIDC deploy role, exposed as the GitHubDeployRoleArn output. The pipeline in Part 13: CI/CD with GitHub Actions uses it, so no long-lived key is needed for deploys.

Commit the deploy template:

git add .
git commit -m "Add CloudFormation template for AWS deploy"

New versions don't need another stack deploy. The pipeline in Part 13: CI/CD with GitHub Actions runs the tests on every push. When they pass, it rebuilds the container on the instance, leaving Aurora and the rest of the stack untouched.

Questions & Answers

Sign up to ask questions, track your progress, and get access to other workshops · Already have an account? Sign in