Skip to content

Commit 5e18ef6

Browse files
committed
add initial starter code
1 parent 17c3ef2 commit 5e18ef6

File tree

18 files changed

+501
-0
lines changed

18 files changed

+501
-0
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*.venv
2+
*__pycache__
3+
*.env
4+
*.env.sh
5+
*backend.tf
6+
*.terraform*
7+
*terraform.*

Makefile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
include .env
2+
3+
.EXPORT_ALL_VARIABLES:
4+
APP_NAME=my-app-name
5+
6+
TAG=latest
7+
TF_VAR_app_name=${APP_NAME}
8+
REGISTRY_NAME=${APP_NAME}
9+
TF_VAR_image=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REGISTRY_NAME}:${TAG}
10+
TF_VAR_region=${AWS_REGION}
11+
12+
13+
setup-ecr:
14+
cd infra/setup && terraform init && terraform apply -auto-approve
15+
16+
deploy-container:
17+
cd app && sh deploy.sh
18+
19+
deploy-service:
20+
cd infra/app && terraform init && terraform apply -auto-approve
21+
22+
destroy-service:
23+
cd infra/app && terraform init && terraform destroy -auto-approve

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# How to use this Repo
2+
## Warning
3+
- Always make sure to destroy your API Service. Forgetting to do so could incur a large AWS fee
4+
- Never commit your AWS Account ID to git. Save it in an `.env` file and ensure `.env` is added to your `.gitiginore`
5+
6+
## Setup, Deploy, and Destroy
7+
8+
### Setup Env Variables
9+
Add an `.env` file containing your AWS account ID and region. Example file:
10+
```
11+
AWS_ACCOUNT_ID=1234567890
12+
AWS_REGION=ap-southeast-1
13+
```
14+
15+
Create a `backend.tf` file and add it to both `/infra/setup/backend.tf` and `/infra/app/backend.tf`. Example files:
16+
```
17+
terraform {
18+
backend "s3" {
19+
region = "<AWS_REGION>"
20+
bucket = "<BUCKET_NAME>"
21+
key = "<APP_NAME>/terraform.tfstate"
22+
}
23+
}
24+
```
25+
```
26+
terraform {
27+
backend "s3" {
28+
region = "<AWS_REGION>"
29+
bucket = "<BUCKET_NAME>"
30+
key = "<APP_NAME>/terraform.tfstate"
31+
}
32+
}
33+
```
34+
Alternatively you can skip this step to store your Terraform state locally.
35+
36+
<br>
37+
38+
### Setup, Deploy, and Destroy Infrastructure/App
39+
All of the following commands are run via the Makefile.
40+
41+
1. Setup your ECR Repository (one time)
42+
```
43+
make setup-ecr
44+
```
45+
46+
<br>
47+
48+
2. Build and deploy your container
49+
```
50+
make deploy-container
51+
```
52+
53+
<br>
54+
55+
3. Deploy your API Service on ECS Fargate
56+
```
57+
make deploy-service
58+
```
59+
Note: The URL for your endpoint will be printed by Terraform once the above command is done executing. Example: `alb_dns_name = "<APP_NAME>-alb-123456789.<AWS_REGION>.elb.amazonaws.com"`. Navigate to that URL in your browser to ensure the API is working. You can also check out the API docs at the `<URL>/docs` endpoint.
60+
61+
<br>
62+
63+
4. Destroy your API Service on ECS Fargate
64+
```
65+
make destroy-service
66+
```
67+

app/Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM python:3.11-slim
2+
WORKDIR /app
3+
COPY . /app
4+
RUN pip install --no-cache-dir -r requirements.txt
5+
EXPOSE 80
6+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]

app/deploy.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
echo "Logging in to ECR"
3+
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
4+
5+
echo "Building image"
6+
docker build --no-cache --platform=linux/amd64 -t $REGISTRY_NAME .
7+
8+
echo "Tagging image"
9+
docker tag $REGISTRY_NAME:$TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$REGISTRY_NAME:$TAG
10+
11+
echo "Pushing image to ECR"
12+
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$REGISTRY_NAME:$TAG

app/main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from fastapi import FastAPI
2+
3+
app = FastAPI()
4+
5+
@app.get("/")
6+
def root():
7+
return {"message": "Welcome to the API"}

app/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fastapi==0.109.2
2+
uvicorn==0.27.1

infra/app/ecs/main.tf

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# ALB
2+
resource "aws_security_group" "alb" {
3+
name = "${var.app_name}-alb-sg"
4+
vpc_id = var.vpc_id
5+
egress {
6+
from_port = 0
7+
to_port = 0
8+
protocol = "-1"
9+
cidr_blocks = ["0.0.0.0/0"]
10+
}
11+
ingress {
12+
from_port = 80
13+
to_port = 80
14+
protocol = "tcp"
15+
cidr_blocks = ["0.0.0.0/0"]
16+
}
17+
ingress {
18+
from_port = 443
19+
to_port = 443
20+
protocol = "tcp"
21+
cidr_blocks = ["0.0.0.0/0"]
22+
}
23+
}
24+
resource "aws_lb" "this" {
25+
name = "${var.app_name}-alb"
26+
load_balancer_type = "application"
27+
security_groups = [aws_security_group.alb.id]
28+
subnets = var.public_subnet_ids
29+
}
30+
resource "aws_lb_target_group" "this" {
31+
name = "${var.app_name}-lb-tg"
32+
vpc_id = var.vpc_id
33+
port = 80
34+
protocol = "HTTP"
35+
target_type = "ip"
36+
health_check {
37+
port = 80
38+
path = "/docs"
39+
interval = 30
40+
protocol = "HTTP"
41+
timeout = 5
42+
unhealthy_threshold = 2
43+
matcher = 200
44+
}
45+
}
46+
resource "aws_lb_listener" "http" {
47+
port = "80"
48+
protocol = "HTTP"
49+
load_balancer_arn = aws_lb.this.arn
50+
default_action {
51+
target_group_arn = aws_lb_target_group.this.arn
52+
type = "forward"
53+
}
54+
depends_on = [aws_lb_target_group.this]
55+
}
56+
resource "aws_lb_listener_rule" "this" {
57+
listener_arn = aws_lb_listener.http.arn
58+
action {
59+
type = "forward"
60+
target_group_arn = aws_lb_target_group.this.arn
61+
}
62+
condition {
63+
path_pattern {
64+
values = ["*"]
65+
}
66+
}
67+
}
68+
69+
# IAM
70+
data "aws_iam_policy_document" "ecs_assume_policy" {
71+
statement {
72+
actions = ["sts:AssumeRole"]
73+
principals {
74+
type = "Service"
75+
identifiers = ["ecs-tasks.amazonaws.com"]
76+
}
77+
}
78+
}
79+
resource "aws_iam_role" "ecs_execution_role" {
80+
name = "${var.app_name}-execution-role"
81+
assume_role_policy = data.aws_iam_policy_document.ecs_assume_policy.json
82+
}
83+
resource "aws_iam_policy" "ecs_execution_policy" {
84+
name = "${var.app_name}-ecs-execution-role-policy"
85+
policy = jsonencode({
86+
Version = "2012-10-17"
87+
Statement = [
88+
{
89+
Effect : "Allow",
90+
Action : [
91+
"ecr:*",
92+
"ecs:*",
93+
"elasticloadbalancing:*",
94+
"cloudwatch:*",
95+
"logs:*"
96+
],
97+
Resource : "*"
98+
}
99+
]
100+
})
101+
}
102+
resource "aws_iam_role_policy_attachment" "ecs_execution_role_policy_attach" {
103+
role = aws_iam_role.ecs_execution_role.name
104+
policy_arn = aws_iam_policy.ecs_execution_policy.arn
105+
}
106+
107+
# ECS
108+
resource "aws_cloudwatch_log_group" "ecs" {
109+
name = "/aws/ecs/${var.app_name}/cluster"
110+
}
111+
resource "aws_ecs_task_definition" "api" {
112+
family = "${var.app_name}-api-task"
113+
requires_compatibilities = ["FARGATE"]
114+
network_mode = "awsvpc"
115+
execution_role_arn = aws_iam_role.ecs_execution_role.arn
116+
task_role_arn = aws_iam_role.ecs_execution_role.arn
117+
cpu = 256
118+
memory = 512
119+
container_definitions = jsonencode([
120+
{
121+
name = "${var.app_name}-api-container"
122+
image = "${var.image}"
123+
command = ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
124+
portMappings = [
125+
{
126+
hostPort = 80
127+
containerPort = 80
128+
protocol = "tcp"
129+
}
130+
],
131+
logConfiguration = {
132+
logDriver = "awslogs"
133+
options = {
134+
awslogs-group = aws_cloudwatch_log_group.ecs.name
135+
awslogs-stream-prefix = "ecs"
136+
awslogs-region = var.region
137+
}
138+
}
139+
}
140+
])
141+
}
142+
143+
# Cluster
144+
resource "aws_ecs_cluster" "this" {
145+
name = "${var.app_name}-cluster"
146+
setting {
147+
name = "containerInsights"
148+
value = "enabled"
149+
}
150+
}
151+
152+
# Security Group and Service
153+
resource "aws_security_group" "ecs" {
154+
name = "${var.app_name}-ecs-sg"
155+
vpc_id = var.vpc_id
156+
egress {
157+
from_port = 0
158+
to_port = 0
159+
protocol = "-1"
160+
cidr_blocks = ["0.0.0.0/0"]
161+
}
162+
ingress {
163+
from_port = 80
164+
to_port = 80
165+
protocol = "tcp"
166+
security_groups = [aws_security_group.alb.id]
167+
}
168+
}
169+
resource "aws_ecs_service" "api" {
170+
name = "${var.app_name}-ecs-service"
171+
cluster = aws_ecs_cluster.this.name
172+
launch_type = "FARGATE"
173+
desired_count = length(var.private_subnet_ids)
174+
task_definition = aws_ecs_task_definition.api.arn
175+
network_configuration {
176+
subnets = var.private_subnet_ids
177+
security_groups = [aws_security_group.ecs.id]
178+
}
179+
load_balancer {
180+
target_group_arn = aws_lb_target_group.this.arn
181+
container_name = "${var.app_name}-api-container"
182+
container_port = "80"
183+
}
184+
lifecycle {
185+
ignore_changes = [
186+
desired_count,
187+
]
188+
}
189+
depends_on = [aws_lb_listener_rule.this]
190+
}
191+

infra/app/ecs/output.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
output "alb_dns_name" {
2+
value = aws_lb.this.dns_name
3+
}

infra/app/ecs/variable.tf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
variable "app_name" {
2+
description = "Name of the app."
3+
type = string
4+
}
5+
variable "region" {
6+
description = "AWS region to deploy the network to."
7+
type = string
8+
}
9+
variable "image" {
10+
description = "Image used to start the container. Should be in repository-url/image:tag format."
11+
type = string
12+
}
13+
variable "vpc_id" {
14+
description = "ID of the VPC where the ECS will be hosted."
15+
type = string
16+
}
17+
variable "public_subnet_ids" {
18+
description = "IDs of public subnets where the ALB will be attached to."
19+
type = list(string)
20+
}
21+
variable "private_subnet_ids" {
22+
description = "IDs of private subnets where the ECS service will be deployed to."
23+
type = list(string)
24+
}

0 commit comments

Comments
 (0)