Newer post
2022: My Year in Review
A personal recap of the milestones, memories, and goals that shaped 2022—from paying off loans and co-organizing NormConf to hiking Sequoia and building a PC—with a look ahead to ambitions for 2023.
AWS
A straight-to-the-point guide for deploying a Dockerized FastAPI app on AWS using ECS, ECR, Route 53, and an Application Load Balancer—ideal for developers looking to get an HTTPS API live without overspending or overengineering.
If you're reading this post then you probably want to learn how to deploy a docker container to AWS cheaply, quickly, and without much ado. Perhaps you've seen my NormConf Talk "Building an HTTPS API for Cheap: AWS, Docker, and the NormConf API".
In the example below, I will push a FastAPI app that has been containerized by Docker, pushed to AWS ECR, and hosted using Farage in ECS. I assume you seek to have external users connect to your API, so I demosntrate how to use a Route 53 A record and an Application Load Balancer to connect to the ECR image.
This is a lot, so I've written instructions as succinctly and directly as I could. If I've missed anything, please feel free to reach out!
This post assumes the following of you:
Text1- You have the proper IAM permissions to deploy in an AWS environment. 2- You've installed the AWS CLI in your terminal. 3- You have a docker image built and are ready to deploy it to the cloud. 4- Have access to a domain or will buy one in Route53
Alright, let's get to it.
As I've said, ensure you have the IAM permissions for all of the below AWS services.
Text1 - S3 2 - ECS 3 - ECR 4 - VPC 5 - Load Balancing 6 - Route53 7 - Certificate Manager 8 - KMS 9 - SSM
First you're going to need a repo into which your images will be stored:
aws ecr create-repository --repository-name <REPOSITORY NAME>Now that you have a repository, you'll want to load an image to ECR:
aws ecr get-login-password --region <REGION> | docker login --username AWS --password-stdin <ACCOUNT>.dkr.ecr.<REGION>.amazonaws.comdocker tag goodies:latest <ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/<REPOSITORY NAME>docker push <ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/<IMAGE NAME>Create a VPC. You'll need to determine what CIDR block is best for your use-case.
aws ec2 create-vpc --cidr-block 10.0.0.0/16
Record the ID of the VPC
aws ec2 describe-vpcs -> e.g. vpc-123456789
Create Subnets using the ID of the VPC, do this twice, one per availability zone. Be sure to check the documentation to determine which availability zone to use, for example us-west-2a
aws ec2 create-subnet --vpc-id <VPC ID> --cidr-block 10.0.0.0/24 --availability-zone <REGION>a
aws ec2 create-subnet --vpc-id <VPC ID> --cidr-block 10.0.1.0/24 --availability-zone <REGION>d
aws ec2 create-internet-gateway --query InternetGateway.InternetGatewayId --output text -> e.g. igw-123456789aws ec2 attach-internet-gateway --vpc-id <VPC ID> --internet-gateway-id <INTERNET GATEWAY ID>aws ec2 create-route-table --vpc-id <VPC ID> --query RouteTable.RouteTableId --output text -> e.g. rtb-123456789aws ec2 create-route --route-table-id <ROUTE TABLE ID> --destination-cidr-block 0.0.0.0/0 --gateway-id <INTERNET GATEWAY ID>aws ec2 create-route --route-table-id <ROUTE TABLE ID> --destination-cidr-block 10.0.0.0/16aws ec2 describe-route-tables --route-table-id <ROUTE TABLE ID>aws ec2 describe-subnets --filters "Name=vpc-id,Values=<VPC ID>" --query "Subnets[*].{ID:SubnetId,CIDR:CidrBlock}" -> e.g. subnet-123456789 & subnet-987654321aws ec2 associate-route-table --subnet-id <SUBNET ID A> --route-table-id <ROUTE TABLE ID>
aws ec2 associate-route-table --subnet-id<SUBNET ID A> --route-table-id <ROUTE TABLE ID>You'll need to create two security groups here.
Create a security group for your networks and record the id. Group name should be made up here. Ensure you record the ID for the newly created security group. -> e.g. sg-123456789
aws ec2 create-security-group --group-name <GROUP 1 NAME> --description "SG used for Fargate VPC" --vpc-id <VPC ID>
Add port rules for security group 1. These will allow or connectivity to your app, and to securely forward to HTTPS on port 443.
aws ec2 authorize-security-group-ingress --group-id <SECURITY GROUP 1 ID> --protocol tcp --port 8000 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id <SECURITY GROUP 1 ID> --protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id <SECURITY GROUP 1 ID> --protocol tcp --port 443 --cidr 0.0.0.0/0
sg-987654321aws ec2 create-security-group --group-name <GROUP NAME 2> --description "SG used for api" --vpc-id <VPC ID>aws ec2 authorize-security-group-ingress --group-id <SECURITY GROUP 2 ID> --protocol tcp --port 8000 --source-group <SECURITY GROUP 1 ID>aws ec2 authorize-security-group-ingress --group-id <SECURITY GROUP 2 ID> --protocol tcp --port 80 --source-group <SECURITY GROUP 1 ID>aws ec2 authorize-security-group-ingress --group-id <SECURITY GROUP 2 ID> --protocol tcp --port 443 --source-group <SECURITY GROUP 1 ID>If your domain is hosted through a different vendor, you'll need to import the SSL certificate. This is what websites use to "prove" they're not sketchy.
Go to AWS Certificate Manager, enter the subdomain + domain, email verification, press send, and have the site owner approve the reques through their email. Otherwise, you'll need to get the SSL certs from your site yourself and import them into AWS. You will need your SSL established before moving forward.
Before creating your load balancer and its listeners, you'll need to create a Target Group which routes requests. Here's the high level overview you'll need for your target group:
Text1 aws elbv2 create-target-group 2 --name <TARGET GROUP NAME> 3 --protocol HTTP --port 8000 4 --target-type ip 5 --protocol-version HTTP1 6 --vpc-id <VPC ID>
Next you'll need to create an application load balancer. Here's the high level overview you'll need for your application load balancer:
Text1 aws elbv2 create-load-balancer 2 --name <LOAD BALANCER NAME> 3 --type application 4 --ip-address-type ipv4 5 --subnets "<SUBNET ID A>" "<SUBNET ID B>" 6 --security-group <SECURITY GROUP 1 ID>
Here you'll need to create two listeners to allows your load balancer to check for connection requests.
You'll need your loadbalancer arn and your target group arn. Here are some helpful CLIs for these
Load-balancer ARN:
aws elbv2 describe-load-balancers --names <LOAD BALANCER NAME> --query "LoadBalancers[0].LoadBalancerArn" --output text
Target Group ARN:
aws elbv2 describe-target-groups --names <TARGET GROUP NAME> --query 'TargetGroups[0].TargetGroupArn' --output text
Details of the first listener:
<TARGET GROUP NAME>Create first listener via CLI:\
Text1 aws elbv2 create-listener 2 --load-balancer-arn <LOAD BALANCER ARN> 3 --protocol HTTP --port 80 4 --default-actions Type=forward,TargetGroupArn=arn:<TARGET GROUP ARN>
Details of the second listener: This one is a bit more complicated. We need to reference both our security policy and our SSL certificate we established above - Listener Protocol: https: 443 - Forward to target group above: normconf-target
To get your ssl certificate arn:
aws acm list-certificates --query "CertificateSummaryList[?DomainName=='<YOUR DOMAIN>'].CertificateArn" --output text
To create a second Listener via cli:
Bash1 aws elbv2 create-listener 2 --load-balancer-arn <LOAD BALANCER ARN> 3 --protocol HTTPS 4 --port 443 5 --certificates CertificateArn=<CERTIFICATE ARN> 6 --ssl-policy ELBSecurityPolicy-2016-08 7 --default-actions Type=forward,TargetGroupArn=<TARGET GROUP ARN>
Now that you've created your load balancer, you'll need to create a public facing hosted zone. The name should be a subdomain/domain that you own or have access to (i.e. api.normconf.com). The caller-reference should be random. Try using the current date.
aws route53 create-hosted-zone --name <DOMAIN YOU OWN> --caller-reference 2022-12-13 --hosted-zone-config Comment="cli version"
You'll need your hosted zones id for the next step. If you've misplace your hosted zone id, here's a command to retrieve it:
aws route53 list-hosted-zones | jq -r '.HostedZones[] | select(.Name=="<YOUR DOMAIN>.") | .Id'
(requires you install jq, I suggest brew install jq)
Next, we're going to connect the application load balancer to a new "record A" Alias recordset. I prefer to use a json for this command, but you could enter it in your CLI. The example below uses a JSON file. You'll need the following information:
Details of Record A
CREATE<YOUR DOMAIN> -> e.g. api.normconf.com<LOAD BALANACER ADDRESS> -> e.g. dualstack.myapp-loadbalancer-123456789.<REGION>.elb.amazonaws.comExample Record A JSON:
JSON1{ 2"Comment": "Creating Alias resource record sets in Route 53", 3"Changes": [ 4 { 5 "Action": "CREATE", 6 "ResourceRecordSet": { 7 "Name": "<YOUR DOMAIN>", 8 "Type": "A", 9 "AliasTarget": { 10 "HostedZoneId": "<HOSTED ZONE ID>", 11 "DNSName": "dualstack.myapp-loadbalancer-123456789.<REGION>.elb.amazonaws.com", 12 "EvaluateTargetHealth": false 13 } 14 } 15 } 16] 17}
aws route53 change-resource-record-sets --hosted-zone-id <HOSTED ZONE ID> --change-batch file://record_A.json
Skip this section if you host your domain in AWS
If you don't host your domain on AWS, you'll need to ensure that your DNS host has access to your NS records information. For example, for namecheap.com you would need to do the following:
domain listmanageadvanced dns tab look for host record, there should be a button that says add new record<NUMBER>.awsdns-<NUMBER>.org<NUMBER>.awsdns-<NUMBER>.co.uk<NUMBER>.awsdns-<NUMBER>.com<NUMBER>.awsdns-<NUMBER>.netecsTaskExecutionRole. We'll be using a JSON again to pass in the information.Example IAM policy JSON:
JSON1 { 2 "Version": "2008-10-17", 3 "Statement": [ 4 { 5 "Sid": "", 6 "Effect": "Allow", 7 "Principal": {"Service": "ecs-tasks.amazonaws.com"}, 8 "Action": "sts:AssumeRole" 9 } 10 ] 11}
aws iam create-policy --policy-name <POLICY NAME> --policy-document file://ecs_policy.json
A note: filling out service and task definition from scratchthe first time is an exercise in madness as they are highly-configurable objects. I suggest doing this part in the UI the first time just to learn what you want. Below, I write out the CLI instructions.
aws ecs create-cluster --cluster-name <CLUSTER NAME>service to run within your cluster (it manages your task definitions). I prefer to add this as a json rather than using all the cli. You can find a service template here.
aws ecs create-service --cli-input-json file://service.jsonBe sure to include the following details in your task definition.
- The ARN from the policy we created above
- The ARN of the ECR image we pushed earlier
-
I prefer to add this as a json rather than typing the instructions in the the cli. Here's an example template you can edit.
aws ecs register-task-definition --cli-input-json file:/task-definition.json
And that's it! Wasn't that easy?
Oh, it was actually highly confusing and convoluted? Well, welcome to the cloud.
At this point your task definition should be deployed in your cluster and your image should be hosted in ECS from ECR. In a future post, I'll demonstrate how to automate this deployment process using GitHub Actions.
Related reading
Newer post
A personal recap of the milestones, memories, and goals that shaped 2022—from paying off loans and co-organizing NormConf to hiking Sequoia and building a PC—with a look ahead to ambitions for 2023.
Older post
A walkthrough for managing multiple GitHub accounts with 1Password's SSH key integration, covering common pitfalls and offering a cleaner setup than the official docs.
Stay in the loop
No spam — just updates when something new ships or the book hits a milestone.