The app module that you are going to implement has the following folder structure:
app/
│
├── alb/
│
├── asg/
│
├── fargate-app/
│
├── allow-deploy-app-policy.tpl
│
├── dependencies.tf
│
├── locals.tf
│
├── main.tf
│
├── outputs.tf
│
└── variables.tf
You then start building the submodules and configuration files required for the app module.
1. The app/alb submodule is defined by the following structure:
app/
│
├── alb/
│ │
│ ├── main.tf
│ │
│ ├── outputs.tf
│ │
│ └── variables.tf
└── ...
Fill the following lines of code to app/alb/variables.tf:
variable "name" {
description = "The name of the alb"
type = string
}
variable "domain_name" {
description = "The domain name of the alb"
type = string
}
variable "hosted_zone_id" {
description = "The id of the Route53 hosted zone"
type = string
}
variable "certificate_arn" {
description = "The arn of the certificate"
type = string
}
variable "vpc_id" {
description = "The id of the vpc"
type = string
}
variable "subnet_ids" {
description = "The subnet ids of the alb"
type = list(string)
}
Fill the following lines of code to app/alb/main.tf:
resource "aws_lb" "main" {
name = var.name
load_balancer_type = "application"
subnets = var.subnet_ids
security_groups = [aws_security_group.alb.id]
}
resource "aws_lb_listener" "main" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
certificate_arn = var.certificate_arn
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "404: page not found"
status_code = 404
}
}
}
resource "aws_lb_listener_rule" "main" {
listener_arn = aws_lb_listener.main.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.main.arn
}
condition {
path_pattern {
values = ["/", "/api/anime"]
}
}
}
resource "aws_route53_record" "main" {
zone_id = var.hosted_zone_id
name = var.domain_name
type = "A"
alias {
name = aws_lb.main.dns_name
zone_id = aws_lb.main.zone_id
evaluate_target_health = false
}
}
resource "aws_lb_target_group" "main" {
name = var.name
vpc_id = var.vpc_id
target_type = "ip"
port = 80
protocol = "HTTP"
protocol_version = "HTTP1"
health_check {
path = "/health"
protocol = "HTTP"
matcher = "200"
interval = 15
timeout = 3
healthy_threshold = 2
unhealthy_threshold = 2
}
}
resource "aws_security_group" "alb" {
name = "${var.name}-sg-alb"
vpc_id = var.vpc_id
}
Fill the following lines of code to app/alb/outputs.tf:
output "target_group_arn" {
description = "The arn of the alb targetgroup"
value = aws_lb_target_group.main.arn
}
output "security_group_id" {
description = "The security group id of the alb"
value = aws_security_group.alb.id
}
2. The app/asg submodule is defined by the following structure:
app/
│
├── asg/
│ │
│ ├── main.tf
│ │
│ └── variables.tf
└── ...
Fill the following lines of code to app/asg/variables.tf:
variable "ecs_cluster_name" {
description = "The name of the ecs cluster"
type = string
}
variable "ecs_service_name" {
description = "The name of the ecs service"
type = string
}
variable "max_capacity" {
description = "The maximum number of tasks allowed in service"
type = number
default = 4
}
variable "min_capacity" {
description = "The minimum number of tasks allowed in service"
type = number
default = 1
}
Fill the following lines of code to app/asg/main.tf:
resource "aws_appautoscaling_target" "main" {
max_capacity = var.max_capacity
min_capacity = var.min_capacity
resource_id = "service/${var.ecs_cluster_name}/${var.ecs_service_name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "main" {
name = "app"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.main.resource_id
scalable_dimension = aws_appautoscaling_target.main.scalable_dimension
service_namespace = aws_appautoscaling_target.main.service_namespace
target_tracking_scaling_policy_configuration {
target_value = 70
scale_in_cooldown = 300
scale_out_cooldown = 300
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
}
}
3. The app/fargate-app submodule is defined by the following structure:
app/
│
├── fargate-app/
│ │
│ ├── allow-to-communicate-with-dynamodb-policy.tpl
│ │
│ ├── dependencies.tf
│ │
│ ├── main.tf
│ │
│ ├── outputs.tf
│ │
│ └── variables.tf
└── ...
Fill the following lines of code to app/fargate-app/allow-to-communicate-with-dynamodb-policy.tpl:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query",
"dynamodb:Scan"
],
"Resource": "${dynamodb_table_arn}"
}
]
}
Fill the following lines of code to app/fargate-app/dependencies.tf:
data "aws_iam_role" "ecs_task_execution" {
name = "ecsTaskExecutionRole"
}
data "aws_ecs_task_definition" "main" {
task_definition = aws_ecs_task_definition.main.family
depends_on = [aws_ecs_task_definition.main]
}
Fill the following lines of code to app/fargate-app/variables.tf:
variable "vpc_id" {
description = "The id of the vpc"
type = string
}
variable "ecs_cluster_name" {
description = "The name of the ecs cluster"
type = string
}
variable "ecs_service_name" {
description = "The name of the ecs service"
type = string
}
variable "cpu" {
description = "Number of cpu units used by the task"
type = string
}
variable "memory" {
description = "Amount (in MiB) of memory used by the task"
type = string
}
variable "desired_count" {
description = "Number of instances of the task definition"
type = number
}
variable "container_definitions" {
description = "The container definitions for the task"
type = string
}
variable "target_group_arn" {
description = "The arn of the target group"
type = string
}
variable "subnet_ids" {
description = "The subnet ids of the app"
type = list(string)
}
variable "dynamodb_table_arn" {
description = "The arn of the dynamodb table"
type = string
}
Fill the following lines of code to app/fargate-app/main.tf:
resource "aws_ecs_cluster" "main" {
name = var.ecs_cluster_name
}
resource "aws_ecs_cluster_capacity_providers" "main" {
cluster_name = aws_ecs_cluster.main.name
capacity_providers = ["FARGATE"]
}
resource "aws_ecs_service" "main" {
cluster = aws_ecs_cluster.main.id
name = var.ecs_service_name
task_definition = "${aws_ecs_task_definition.main.family}:${max(aws_ecs_task_definition.main.revision, data.aws_ecs_task_definition.main.revision)}"
desired_count = var.desired_count
scheduling_strategy = "REPLICA"
deployment_minimum_healthy_percent = 50
capacity_provider_strategy {
base = 0
weight = 1
capacity_provider = "FARGATE"
}
deployment_circuit_breaker {
enable = true
rollback = true
}
network_configuration {
subnets = var.subnet_ids
security_groups = [aws_security_group.app.id]
# Turn on for testing with public subnets
# assign_public_ip = true
}
load_balancer {
target_group_arn = var.target_group_arn
container_name = "container-1"
container_port = 8080
}
lifecycle {
ignore_changes = [desired_count]
}
depends_on = [data.aws_ecs_task_definition.main]
}
resource "aws_ecs_task_definition" "main" {
family = var.ecs_service_name
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.cpu
memory = var.memory
task_role_arn = aws_iam_role.allow_to_communicate_with_dynamodb.arn
execution_role_arn = data.aws_iam_role.ecs_task_execution.arn
runtime_platform {
operating_system_family = "LINUX"
cpu_architecture = "X86_64"
}
container_definitions = var.container_definitions
}
resource "aws_iam_role" "allow_to_communicate_with_dynamodb" {
name = "allow-to-communicate-with-dynamodb"
assume_role_policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Principal" : {
"Service" : "ecs-tasks.amazonaws.com"
},
"Action" : "sts:AssumeRole"
}
]
})
managed_policy_arns = [aws_iam_policy.allow_to_communicate_with_dynamodb.arn]
}
resource "aws_iam_policy" "allow_to_communicate_with_dynamodb" {
name = "AllowToCommunicateWithDynamodb"
description = "IAM policy for ECS to communicate with DynamoDB"
policy = templatefile("${path.module}/allow-to-communicate-with-dynamodb-policy.tpl", {
dynamodb_table_arn = var.dynamodb_table_arn,
})
}
resource "aws_security_group" "app" {
name = "${var.ecs_cluster_name}-sg-app"
vpc_id = var.vpc_id
}
Fill the following lines of code to app/fargate-app/outputs.tf:
output "security_group_id" {
description = "The security group id of the app"
value = aws_security_group.app.id
}
output "ecs_service_arn" {
description = "The arn of the ecs service"
value = aws_ecs_service.main.id
}
output "ecs_cluster_name" {
description = "The name of the ecs cluster"
value = aws_ecs_cluster.main.name
}
output "ecs_service_name" {
description = "The name of the ecs service"
value = aws_ecs_service.main.name
}
output"allow_to_communicate_with_dynamodb_role_arn" {
description = "The role arn for allowing to communicate with dynamodb"
value = aws_iam_role.allow_to_communicate_with_dynamodb.arn
}
4. You now implement the Terraform configuration files for app module.
Fill the following lines of code to app/allow-deploy-app-policy.tpl:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RegisterAndDescribeTaskDefinition",
"Effect": "Allow",
"Action": [
"ecs:RegisterTaskDefinition",
"ecs:DescribeTaskDefinition"
],
"Resource": "*"
},
{
"Sid": "PassRolesInTaskDefinition",
"Effect": "Allow",
"Action": [
"iam:PassRole"
],
"Resource": [
"${ecs_execution_role_arn}",
"${ecs_task_role_arn}"
]
},
{
"Sid": "DeployService",
"Effect": "Allow",
"Action": [
"ecs:UpdateService",
"ecs:DescribeServices"
],
"Resource": "${ecs_service_arn}"
},
{
"Sid": "GetAuthorizationToken",
"Effect": "Allow",
"Action": "ecr:GetAuthorizationToken",
"Resource": "*"
},
{
"Sid": "AllowPush",
"Effect": "Allow",
"Action": [
"ecr:CompleteLayerUpload",
"ecr:UploadLayerPart",
"ecr:InitiateLayerUpload",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage"
],
"Resource": "${ecr_repository_arn}"
}
]
}
Fill the following lines of code to app/dependencies.tf:
data "aws_region" "current" {}
data "aws_iam_role" "ecs_task_execution" {
name = "ecsTaskExecutionRole"
}
data "aws_route_table" "app" {
subnet_id = var.app_subnet_ids[0]
}
Fill the following lines of code to app/variables.tf:
variable "environment" {
description = "The environment to which the app delploys"
type = string
}
variable "project_name" {
description = "The name of the project"
type = string
}
variable "hosted_zone_id" {
description = "The id of the Route53 hosted zone"
type = string
}
variable "vpc_id" {
description = "The id of the vpc"
type = string
}
variable "gh_oidc_provider_arn" {
description = "The arn of GitHub openid connect provider"
type = string
}
variable "ecr_repository_arn" {
description = "The arn of the ecr repository"
type = string
}
variable "dynamodb_table_arn" {
description = "The arn of the dynamodb table"
type = string
}
variable "domain_name" {
description = "The domain name of the alb"
type = string
}
variable "app_name" {
description = "The name of the application"
type = string
}
variable "max_capacity" {
description = "The maximum number of tasks allowed in service"
type = number
default = 4
}
variable "min_capacity" {
description = "The minimum number of tasks allowed in service"
type = number
default = 1
}
variable "cpu" {
description = "Number of cpu units used by the task"
type = string
}
variable "memory" {
description = "Amount (in MiB) of memory used by the task"
type = string
}
variable "desired_count" {
description = "Number of instances of the task definition"
type = number
}
variable "github_org" {
description = "The GitHub organization that the Github Actions role trusts"
type = string
}
variable "github_repo" {
description = "The GitHub repository that the Github Actions role trusts"
type = string
}
variable "container_definitions" {
description = "The container definitions for the task"
type = string
}
variable "alb_subnet_ids" {
description = "The subnet ids of the alb"
type = list(string)
}
variable "app_subnet_ids" {
description = "The subnet ids of the app"
type = list(string)
}
variable "app_port" {
description = "The port of the app"
type = number
}
Fill the following lines of code to app/locals.tf:
locals {
tcp_protocol = "tcp"
interface_endpoint_port = 443
all_ips = "0.0.0.0/0"
any_protocol = "-1"
resource_name = "${var.environment}-${var.project_name}"
}
Fill the following lines of code to app/main.tf:
#----------------------------------------------------------------------
# TSL Certificate
#----------------------------------------------------------------------
module "certificate" {
source = "../security/certificate"
domain_name = var.domain_name
hosted_zone_id = var.hosted_zone_id
}
#----------------------------------------------------------------------
# Application Load Balancer
#----------------------------------------------------------------------
module "alb" {
source = "./alb"
name = local.resource_name
domain_name = var.domain_name
vpc_id = var.vpc_id
subnet_ids = var.alb_subnet_ids
hosted_zone_id = var.hosted_zone_id
certificate_arn = module.certificate.arn
}
resource "aws_vpc_security_group_ingress_rule" "allow_all_to_alb" {
security_group_id = module.alb.security_group_id
cidr_ipv4 = local.all_ips
ip_protocol = local.any_protocol
}
resource "aws_vpc_security_group_egress_rule" "allow_all_from_alb" {
security_group_id = module.alb.security_group_id
cidr_ipv4 = local.all_ips
ip_protocol = local.any_protocol
}
#----------------------------------------------------------------------
# Auto Scaling Group
#----------------------------------------------------------------------
module "asg" {
source = "./asg"
ecs_cluster_name = module.fargate_app.ecs_cluster_name
ecs_service_name = module.fargate_app.ecs_service_name
max_capacity = var.max_capacity
min_capacity = var.min_capacity
}
#----------------------------------------------------------------------
# Fargate App
#----------------------------------------------------------------------
module "fargate_app" {
source = "./fargate-app"
ecs_cluster_name = local.resource_name
ecs_service_name = var.app_name
vpc_id = var.vpc_id
subnet_ids = var.app_subnet_ids
target_group_arn = module.alb.target_group_arn
dynamodb_table_arn = var.dynamodb_table_arn
cpu = var.cpu
memory = var.memory
desired_count = var.desired_count
container_definitions = var.container_definitions
depends_on = [module.alb]
}
resource "aws_vpc_security_group_ingress_rule" "allow_alb_to_app" {
security_group_id = module.fargate_app.security_group_id
referenced_security_group_id = module.alb.security_group_id
ip_protocol = local.tcp_protocol
from_port = var.app_port
to_port = var.app_port
}
resource "aws_vpc_security_group_egress_rule" "allow_all_from_app" {
security_group_id = module.fargate_app.security_group_id
cidr_ipv4 = local.all_ips
ip_protocol = local.any_protocol
}
#----------------------------------------------------------------------
# GitHub Actions role for deploying app
#----------------------------------------------------------------------
module "gha_role" {
source = "../security/gha-role"
gh_oidc_provider_arn = var.gh_oidc_provider_arn
role_name = "gha-role-for-deploying-app"
github_repo = var.github_repo
github_org = var.github_org
policy_arn = aws_iam_policy.allow_deploy_app.arn
}
resource "aws_iam_policy" "allow_deploy_app" {
name = "AllowDeployApp"
description = "IAM policy for deploying app to fargate"
policy = templatefile("${path.module}/allow-deploy-app-policy.tpl", {
ecs_execution_role_arn = data.aws_iam_role.ecs_task_execution.arn,
ecs_task_role_arn = module.fargate_app.allow_to_communicate_with_dynamodb_role_arn,
ecs_service_arn = module.fargate_app.ecs_service_arn,
ecr_repository_arn = var.ecr_repository_arn
})
}
# #----------------------------------------------------------------------
# # s3 Gateway Endpoint
# #----------------------------------------------------------------------
resource "aws_vpc_endpoint" "s3_gateway_endpoint" {
vpc_id = var.vpc_id
vpc_endpoint_type = "Gateway"
service_name = "com.amazonaws.${data.aws_region.current.name}.s3"
route_table_ids = [data.aws_route_table.app.route_table_id]
tags = {
Name = "s3-gateway-endpoint"
Environment = var.environment
}
}
# #----------------------------------------------------------------------
# # DynamoDB Gateway Endpoint
# #----------------------------------------------------------------------
resource "aws_vpc_endpoint" "dynamodb_gateway_endpoint" {
vpc_id = var.vpc_id
vpc_endpoint_type = "Gateway"
service_name = "com.amazonaws.${data.aws_region.current.name}.dynamodb"
route_table_ids = [data.aws_route_table.app.route_table_id]
tags = {
Name = "dynamodb-gateway-endpoint"
Environment = var.environment
}
}
# #----------------------------------------------------------------------
# # Security Group for Interface Endpoints allowing traffic from App
# #----------------------------------------------------------------------
resource "aws_security_group" "interface_endpoint" {
vpc_id = var.vpc_id
}
resource "aws_vpc_security_group_ingress_rule" "allow_app" {
security_group_id = aws_security_group.interface_endpoint.id
referenced_security_group_id = module.fargate_app.security_group_id
ip_protocol = local.tcp_protocol
from_port = local.interface_endpoint_port
to_port = local.interface_endpoint_port
}
# #----------------------------------------------------------------------
# # ecr api Interface Endpoint
# #----------------------------------------------------------------------
resource "aws_vpc_endpoint" "ecr_api_interface_endpoint" {
vpc_id = var.vpc_id
vpc_endpoint_type = "Interface"
private_dns_enabled = true
service_name = "com.amazonaws.${data.aws_region.current.name}.ecr.api"
subnet_ids = var.app_subnet_ids
security_group_ids = [aws_security_group.interface_endpoint.id]
tags = {
Name = "ecr-api-interface-endpoint"
Environment = var.environment
}
}
# #----------------------------------------------------------------------
# # ecr dkr Interface Endpoint
# #----------------------------------------------------------------------
resource "aws_vpc_endpoint" "ecr_dkr_interface_endpoint" {
vpc_id = var.vpc_id
vpc_endpoint_type = "Interface"
private_dns_enabled = true
service_name = "com.amazonaws.${data.aws_region.current.name}.ecr.dkr"
subnet_ids = var.app_subnet_ids
security_group_ids = [aws_security_group.interface_endpoint.id]
tags = {
Name = "ecr-dkr-interface-endpoint"
Environment = var.environment
}
}
Fill the following lines of code to app/outputs.tf:
output "gha_role_deploy_app_arn" {
description = "The role arn for GitHub Actions that deploys app"
value = module.gha_role.arn
}
5. Commit and push the module to the GitHub repository.
git add . && \
git commit -m "add app module" && \
git push