Skip to content

Commit ea366c6

Browse files
committed
add terraform configuration
1 parent afff500 commit ea366c6

File tree

20 files changed

+998
-3
lines changed

20 files changed

+998
-3
lines changed

Makefile

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# for local development
2+
13
dcud:
24
docker compose -f docker-compose.dev.yml up
35

@@ -8,4 +10,21 @@ dcug:
810
docker compose -f docker-compose.gunicorn.yml up
911

1012
dcdg:
11-
docker compose -f docker-compose.gunicorn.yml down
13+
docker compose -f docker-compose.gunicorn.yml down
14+
15+
# for terraform
16+
17+
tf-init:
18+
terraform -chdir=terraform init
19+
20+
tf-plan:
21+
terraform -chdir=terraform plan
22+
23+
tf-apply:
24+
terraform -chdir=terraform apply
25+
26+
tf-fmt:
27+
terraform fmt -recursive
28+
29+
tf-destroy:
30+
terraform -chdir=terraform destroy

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,18 @@ This repo is for testing ECS networking configurations using:
1717
- [x] add a simple docker-compose file with flask app and redis (prod mode with gunicorn)
1818
- [x] add a redis connection to flask app
1919
- [x] add basic view to test redis connection
20-
- [ ] add a terraform config that can be used locally
20+
- [x] add a terraform config that can be used locally
2121

22+
## AWS Setup
23+
24+
- [x] Setup ECR repository for flask backend
25+
- [x] Build and push flask backend to ECR
26+
- [x] ACM certificate ARN stored in terraform.tfvars file locally
27+
- [x] Hosted Zone and Domain name purchased through AWS Route 53
28+
29+
## Terraform Configuration Details
30+
31+
- [x] VPC with only public subnets and no NAT gateway
32+
- [x] Launch Configuration with 1 EC2 instance
33+
- [x] ASG for ECS
34+
- [ ] Service Discovery Resources

app/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.9-slim-buster
1+
FROM --platform=linux/amd64 python:3.9-slim-buster
22

33
ENV PYTHONUNBUFFERED 1
44
ENV PYTHONDONTWRITEBYTECODE 1

app/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@ def set_foo(path):
2828
def unset_foo():
2929
r.delete('foo')
3030
return "<b>foo</b> has been deleted"
31+
32+
@app.route("/api/status/")
33+
def status():
34+
return "OK"

scripts/build_and_push_to_ecr.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
3+
ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
4+
IMAGE_TAG=${IMAGE_TAG:-latest}
5+
echo "IMAGE_TAG is ${IMAGE_TAG}"
6+
7+
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com
8+
9+
docker build -t flask:$IMAGE_TAG ./app
10+
11+
docker tag flask:$IMAGE_TAG $ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/flask:$IMAGE_TAG
12+
13+
docker push $ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/flask:$IMAGE_TAG
14+

terraform/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.terraform
2+
.terraform.lock.hcl
3+
terraform.tfvars
4+
terraform.tfstate.backup
5+
terraform.tfstate

terraform/main.tf

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
module "vpc" {
2+
source = "terraform-aws-modules/vpc/aws"
3+
4+
name = "${terraform.workspace}-vpc"
5+
cidr = var.cidr
6+
7+
azs = var.azs
8+
public_subnets = var.public_subnets
9+
10+
enable_nat_gateway = false
11+
}
12+
13+
module "lb" {
14+
source = "./modules/lb"
15+
vpc_id = module.vpc.vpc_id
16+
acm_certificate_arn = var.acm_certificate_arn
17+
health_check_path = "/"
18+
public_subnets = module.vpc.public_subnets
19+
}
20+
21+
module "ecs" {
22+
source = "./modules/ecs"
23+
vpc_id = module.vpc.vpc_id
24+
public_subnets = module.vpc.public_subnets
25+
instance_type = var.instance_type
26+
alb_sg_id = module.lb.alb_sg_id
27+
region = var.region
28+
}
29+
30+
module "route53" {
31+
source = "./modules/route53"
32+
zone_name = var.zone_name
33+
record_name = var.record_name
34+
alb_dns_name = module.lb.dns_name
35+
}
36+
37+
module "redis" {
38+
source = "./modules/redis"
39+
name = "redis"
40+
vpc_id = module.vpc.vpc_id
41+
ecs_cluster_id = module.ecs.cluster_id
42+
public_subnets = module.vpc.public_subnets
43+
ecs_sg_id = module.ecs.ecs_sg_id
44+
image = "redis:latest"
45+
region = var.region
46+
service_discovery_arn = module.ecs.registry_arn
47+
ecs_service_iam_role_arn = module.ecs.service_iam_role_arn
48+
}
49+
50+
# locals {
51+
# be_image = "${var.ecr_app_repo}:${var.app_image_tag}"
52+
# env_vars = [
53+
# {
54+
# name = "REDIS_HOST"
55+
# value = "redis"
56+
# }
57+
# ]
58+
# }
59+
60+
# module "app" {
61+
# source = "./modules/app"
62+
# name = "gunicorn"
63+
# ecs_cluster_id = module.ecs.cluster_id
64+
# task_role_arn = module.ecs.task_role_arn
65+
# ecs_service_iam_role_arn = module.ecs.service_iam_role_arn
66+
# command = ["gunicorn", "--bind", "0.0.0.0:5000", "wsgi:app"]
67+
# env_vars = local.env_vars
68+
# image = local.be_image
69+
# alb_default_tg_arn = module.lb.alb_default_tg_arn
70+
# log_group_name = "/ecs/${terraform.workspace}/app"
71+
# log_stream_prefix = "app"
72+
# region = var.region
73+
# cpu = var.app_cpu
74+
# memory = var.app_memory
75+
# port = 8000
76+
# path_patterns = ["/*"]
77+
# health_check_path = "/api/status/"
78+
# listener_arn = module.lb.listener_arn
79+
# vpc_id = module.vpc.vpc_id
80+
# priority = 1
81+
# }

terraform/modules/app/main.tf

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
resource "aws_cloudwatch_log_group" "this" {
2+
name = var.log_group_name
3+
retention_in_days = 1
4+
}
5+
6+
resource "aws_cloudwatch_log_stream" "this" {
7+
name = var.log_stream_prefix
8+
log_group_name = aws_cloudwatch_log_group.this.name
9+
}
10+
11+
resource "aws_ecs_task_definition" "this" {
12+
family = "${terraform.workspace}-${var.name}"
13+
container_definitions = jsonencode([
14+
{
15+
name = var.name
16+
image = var.image
17+
cpu = var.cpu
18+
memory = var.memory
19+
essential = true
20+
links = []
21+
environment = var.env_vars
22+
command = var.command
23+
logConfiguration = {
24+
logDriver = "awslogs"
25+
options = {
26+
"awslogs-group" = var.log_group_name
27+
"awslogs-region" = var.region
28+
"awslogs-stream-prefix" = var.log_stream_prefix
29+
}
30+
}
31+
portMappings = [
32+
{
33+
containerPort = var.port
34+
hostPort = 0
35+
protocol = "tcp"
36+
}
37+
]
38+
}
39+
])
40+
task_role_arn = var.task_role_arn
41+
}
42+
43+
resource "aws_ecs_service" "this" {
44+
name = "${terraform.workspace}-${var.name}"
45+
cluster = var.ecs_cluster_id
46+
task_definition = aws_ecs_task_definition.this.arn
47+
iam_role = var.ecs_service_iam_role_arn
48+
desired_count = var.app_count
49+
50+
load_balancer {
51+
target_group_arn = aws_lb_target_group.this.arn
52+
container_name = var.name
53+
container_port = var.port
54+
}
55+
}
56+
57+
resource "aws_lb_target_group" "this" {
58+
port = var.port
59+
protocol = "HTTP"
60+
vpc_id = var.vpc_id
61+
deregistration_delay = 5
62+
63+
health_check {
64+
healthy_threshold = var.health_check_healthy_threshold
65+
unhealthy_threshold = 3
66+
interval = var.health_check_interval
67+
matcher = "200-399"
68+
path = var.health_check_path
69+
port = "traffic-port"
70+
protocol = "HTTP"
71+
timeout = "5"
72+
}
73+
tags = {
74+
Name = "${terraform.workspace}-${var.name}-tg" #* https://github.com/hashicorp/terraform-provider-aws/issues/636#issuecomment-397459646
75+
}
76+
77+
lifecycle {
78+
create_before_destroy = true
79+
}
80+
81+
}
82+
83+
resource "aws_lb_listener_rule" "this" {
84+
listener_arn = var.listener_arn
85+
priority = var.priority
86+
87+
condition {
88+
path_pattern {
89+
values = var.path_patterns
90+
}
91+
}
92+
action {
93+
type = "forward"
94+
target_group_arn = aws_lb_target_group.this.arn
95+
}
96+
}

terraform/modules/app/variables.tf

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
variable "alb_default_tg_arn" {
2+
description = "The ARN of the default target group"
3+
type = string
4+
}
5+
6+
variable "app_count" {
7+
description = "Number of Docker containers to run"
8+
default = 1
9+
}
10+
11+
variable "ecs_cluster_id" {
12+
description = "ECS Cluster ID"
13+
type = string
14+
}
15+
16+
variable "ecs_service_iam_role_arn" {
17+
description = "ECS Service IAM Role ARN"
18+
type = string
19+
}
20+
21+
variable "task_role_arn" {
22+
description = "Task Role ARN"
23+
type = string
24+
}
25+
26+
variable "command" {
27+
default = null
28+
type = list(string)
29+
description = "Command to run in Docker container"
30+
}
31+
32+
variable "env_vars" {
33+
type = list(object({ name = string, value = string }))
34+
description = "Environment variables to set in Docker container"
35+
}
36+
37+
variable "image" {
38+
type = string
39+
description = "Container image from ECS to run"
40+
}
41+
42+
variable "log_group_name" {
43+
type = string
44+
description = "Name of the CloudWatch Logs group"
45+
}
46+
47+
variable "log_stream_prefix" {
48+
type = string
49+
description = "Name of the CloudWatch Logs stream"
50+
}
51+
52+
variable "log_retention_in_days" {
53+
default = 1
54+
type = number
55+
}
56+
57+
variable "region" {
58+
default = "us-east-1"
59+
description = "AWS region"
60+
type = string
61+
}
62+
63+
variable "cpu" {
64+
default = null
65+
description = "CPU to allocate to container"
66+
type = number
67+
}
68+
69+
variable "memory" {
70+
default = null
71+
description = "Amount (in MiB) of memory used by the task"
72+
type = number
73+
}
74+
75+
variable "name" {
76+
description = "Name to use for the service and task"
77+
type = string
78+
}
79+
80+
variable "port" {
81+
default = 80
82+
description = "Port to expose on the container"
83+
type = number
84+
}
85+
86+
variable "priority" {
87+
description = "Priority for the listener rule"
88+
type = number
89+
}
90+
91+
92+
variable "path_patterns" {
93+
description = "Path patterns to match"
94+
type = list(string)
95+
}
96+
97+
variable "listener_arn" {
98+
description = "Listener ARN"
99+
type = string
100+
}
101+
102+
variable "vpc_id" {
103+
description = "VPC ID"
104+
type = string
105+
}
106+
107+
variable "health_check_path" {
108+
description = "Path to check for health"
109+
default = "/"
110+
type = string
111+
}
112+
113+
variable "health_check_healthy_threshold" {
114+
description = "Number of consecutive health checks successes required"
115+
default = 2
116+
type = number
117+
}
118+
119+
120+
variable "health_check_interval" {
121+
description = "Time between health checks"
122+
default = 7
123+
type = number
124+
}

0 commit comments

Comments
 (0)