Skip to content

Commit

Permalink
Added AWS Fargate Service module.
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesiarmes committed May 17, 2024
1 parent e92d618 commit 560bd83
Show file tree
Hide file tree
Showing 19 changed files with 566 additions and 7 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ terraform.

## Modules

| Provider | Module | Description |
|----------|--------------------------------------|--------------------------------------------------------------------------|
| AWS | [backend][aws-backend] | S3 storage backend for tfstate. |
| AWS | [cloudfront_waf][aws-cloudfront-waf] | CloudFront distribution that passes traffic through WAF without caching. |
| AWS | [logging][aws-logging] | CloudFront distribution that passes traffic through WAF without caching. |
| AWS | [vpc][aws-vpc] | CloudFront distribution that passes traffic through WAF without caching. |
| Provider | Module | Description |
|----------|----------------------------------------|--------------------------------------------------------------------------|
| AWS | [backend][aws-backend] | S3 storage backend for tfstate. |
| AWS | [cloudfront_waf][aws-cloudfront-waf] | CloudFront distribution that passes traffic through WAF without caching. |
| AWS | [fargate_service][aws-fargate_service] | CloudFront distribution that passes traffic through WAF without caching. |
| AWS | [logging][aws-logging] | CloudFront distribution that passes traffic through WAF without caching. |
| AWS | [vpc][aws-vpc] | CloudFront distribution that passes traffic through WAF without caching. |

[aws-backend]: ./aws/backend/README.md
[aws-cloudfront-waf]: ./aws/cloudfront_waf/README.md
[aws-fargate_service]: ./aws/fargate_service/README.md
[aws-logging]: ./aws/logging/README.md
[aws-vpc]: ./aws/vpc/README.md
[opentofu]: https://opentofu.org/
Expand Down
61 changes: 61 additions & 0 deletions aws/fargate_service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# AWS Fargate Service Module

This module launches a service on AWS Fargate. It creates a cluster, task
definition, service, and container repository. In addition, it creates the load
balancer, ACM certificate, Route53 records, and security groups needed to expose
the service.

## Usage

Add this module to your `main.tf` (or appropriate) file and configure the inputs
to match your desired configuration. For example:

```hcl
module "cloudfront_waf" {
source = "github.com/codeforamerica/tofu-modules/aws/fargate_service"
project = "my-project"
environment = "dev"
}
```

Make sure you re-run `tofu init` after adding the module to your configuration.

```bash
tofu init
tofu plan
```

To update the source for this module, pass `-upgrade` to `tofu init`:

```bash
tofu init -upgrade
```

## Inputs

| Name | Description | Type | Default | Required |
|--------------------------|------------------------------------------------------------------------------------|----------|------------|----------|
| domain | Domain name for service. Example: `"staging.service.org"` | `string` | n/a | yes |
| logging_key_id | KMS key to use for log encryption. | `string` | n/a | yes |
| private_subnets | List of private subnet CIDR blocks. | `list` | n/a | yes |
| project | Name of the project. | `string` | n/a | yes |
| project_short | Short name for the project. Used in resource names with character limits. | `string` | n/a | yes |
| public_subnets | List of public subnet CIDR blocks. | `list` | n/a | yes |
| service | Service that these resources are supporting. Example: `"api"`, `"web"`, `"worker"` | `string` | n/a | yes |
| service_short | Short name for the service. Used in resource names with character limits. | `string` | n/a | yes |
| vpc_id | Id of the VPC to deploy into. | `string` | n/a | yes |
| container_port | Port the container listens on. | `number` | `80` | no |
| environment | Environment for the project. | `string` | `"dev"` | no |
| image_tag | Tag of the container image to be deployed. | `string` | `"latest"` | no |
| internal | Creates an internal ALB instead of a public one. | `bool` | `false` | no |
| key_recovery_period | Number of days to recover the service KMS key after deletion. | `number` | `30` | no |
| log_retention_period | Retention period for flow logs, in days. | `number` | `30` | no |
| untagged_image_retention | Retention period (after push) for untagged images, in days. | `number` | `14` | no |

## Outputs

| Name | Description | Type |
|--------------|--------------------------------------------------------------|----------|
| cluster_name | Name of the ECS Fargate cluster. | `string` |
| docker_push | Commands to push a Docker image to the container repository. | `string` |
100 changes: 100 additions & 0 deletions aws/fargate_service/alb.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
module "alb" {
source = "terraform-aws-modules/alb/aws"
version = "~> 9.9"
enable_deletion_protection = false

name = local.prefix_short
load_balancer_type = "application"
security_groups = [module.endpoint_security_group.security_group_id]
subnets = var.internal ? var.private_subnets : var.public_subnets
vpc_id = var.vpc_id

# TODO: Support IPv6 and/or dualstack.
ip_address_type = "ipv4"

listeners = {
http = {
port = 80
protocol = "HTTP"
redirect = {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}

https = {
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = aws_acm_certificate.endpoint.arn
forward = {
target_group_key = "endpoint"
}
}
}

target_groups = {
endpoint = {
name = "${local.prefix_short}-app"
protocol = "HTTP"
target_type = "ip"
port = var.container_port

# Theres nothing to attach here in this definition. Instead, ECS will
# attach the IPs of the tasks to this target group.
create_attachment = false

health_check = {
path = "/health"
healthy_threshold = 5
unhealthy_threshold = 2
}
}
}
}

resource "aws_acm_certificate" "endpoint" {
domain_name = var.domain
validation_method = "DNS"

lifecycle {
create_before_destroy = true
}
}

resource "aws_route53_record" "endpoint" {
name = var.domain
type = "A"
zone_id = data.aws_route53_zone.domain.zone_id

alias {
name = module.alb.dns_name
zone_id = module.alb.zone_id
evaluate_target_health = true
}
}

resource "aws_route53_record" "endpoint_validation" {
for_each = {
for dvo in aws_acm_certificate.endpoint.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}

allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = data.aws_route53_zone.domain.zone_id
}

resource "aws_acm_certificate_validation" "endpoint" {
certificate_arn = aws_acm_certificate.endpoint.arn
validation_record_fqdns = [
for record in aws_route53_record.endpoint_validation : record.fqdn
]
}
9 changes: 9 additions & 0 deletions aws/fargate_service/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
data "aws_caller_identity" "identity" {}

data "aws_partition" "current" {}

data "aws_region" "current" {}

data "aws_route53_zone" "domain" {
name = var.domain
}
56 changes: 56 additions & 0 deletions aws/fargate_service/iam.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
resource "aws_iam_policy" "execution" {
name = "${local.prefix}-execution"
description = "${var.service} task execution policy for ${var.project} ${var.environment}."

policy = jsonencode(yamldecode(templatefile("${path.module}/templates/execution-policy.yaml.tftpl", {
project = var.project
environment = var.environment
ecr_arn = module.ecr.repository_arn
})))
}

resource "aws_iam_role" "execution" {
name = "${local.prefix}-execution"
description = "${var.service} task execution role for ${var.project} ${var.environment}."

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})

managed_policy_arns = [
# aws_iam_policy.execution.arn
"arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
]
}

resource "aws_iam_role" "task" {
name = "${local.prefix}-task"
description = "${var.service} task role for ${var.project} ${var.environment}."

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})

managed_policy_arns = [
"arn:${data.aws_partition.current.partition}:iam::aws:policy/CloudWatchFullAccess",
"arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonSSMFullAccess"
]
}
16 changes: 16 additions & 0 deletions aws/fargate_service/kms.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
resource "aws_kms_key" "fargate" {
description = "${var.service} hosting encryption key for ${var.project} ${var.environment}"
deletion_window_in_days = var.key_recovery_period
enable_key_rotation = true
policy = jsonencode(yamldecode(templatefile("${path.module}/templates/key-policy.yaml.tftpl", {
account_id : data.aws_caller_identity.identity.account_id,
partition : data.aws_partition.current.partition,
region : data.aws_region.current.name,
repository_name : local.prefix,
})))
}

resource "aws_kms_alias" "fargate" {
name = "alias/${var.project}/${var.environment}/${var.service}"
target_key_id = aws_kms_key.fargate.id
}
4 changes: 4 additions & 0 deletions aws/fargate_service/local.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
locals {
prefix = "${var.project}-${var.environment}-${var.service}"
prefix_short = "${var.project_short}-${var.environment}-${var.service_short}"
}
4 changes: 4 additions & 0 deletions aws/fargate_service/logs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "aws_cloudwatch_log_group" "service" {
name = "/aws/ecs/${var.project}/${var.environment}/${var.service}"
retention_in_days = 30
}

Check notice on line 4 in aws/fargate_service/logs.tf

View workflow job for this annotation

GitHub Actions / trivy

AVD-AWS-0017

Artifact: aws/fargate_service/logs.tf
95 changes: 95 additions & 0 deletions aws/fargate_service/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
module "ecr" {
source = "terraform-aws-modules/ecr/aws"
version = "~> 2.2"

repository_name = local.prefix
repository_image_scan_on_push = true
repository_encryption_type = "KMS"
repository_kms_key = aws_kms_key.fargate.arn
repository_lifecycle_policy = jsonencode(yamldecode(templatefile(
"${path.module}/templates/repository-lifecycle.yaml.tftpl", {
untagged_image_retention : var.untagged_image_retention
}
)))
}

# TODO: Configure internal CIDR
module "endpoint_security_group" {
source = "terraform-aws-modules/security-group/aws"
version = "~> 5.1"

name = "${local.prefix}-endpoint"
vpc_id = var.vpc_id

# Ingress for HTTP
ingress_cidr_blocks = [var.internal ? "10.0.0.0/16" : "0.0.0.0/0"]
ingress_rules = ["http-80-tcp", "https-443-tcp"]

# Allow all egress
egress_cidr_blocks = ["0.0.0.0/0"]
egress_ipv6_cidr_blocks = ["::/0"]
egress_rules = ["all-all"]
}

# TODO: Configure internal CIDR
module "task_security_group" {
source = "terraform-aws-modules/security-group/aws"
version = "~> 5.1"

name = "${local.prefix}-endpoint"
vpc_id = var.vpc_id

ingress_with_source_security_group_id = [{
from_port = var.container_port
to_port = var.container_port
protocol = "tcp"
description = "${var.service} access from the load balancer."
source_security_group_id = module.endpoint_security_group.security_group_id
}]

# Allow all egress
# TODO: Can we restrict this?
egress_cidr_blocks = ["0.0.0.0/0"]
egress_ipv6_cidr_blocks = ["::/0"]
egress_rules = ["all-all"]
}

module "ecs" {
source = "HENNGE/ecs/aws"
version = "~> 4.2"

name = local.prefix
capacity_providers = ["FARGATE"]
enable_container_insights = true
}

module "ecs_service" {
source = "HENNGE/ecs/aws//modules/simple/fargate"
version = "~> 4.2"
depends_on = [module.alb, module.ecs]

name = local.prefix
cluster = module.ecs.arn
container_port = var.container_port
container_name = local.prefix
cpu = 256
memory = 512
desired_count = 1
vpc_subnets = var.private_subnets
target_group_arn = module.alb.target_groups["endpoint"].arn
security_groups = [module.task_security_group.security_group_id]
iam_daemon_role = aws_iam_role.execution.arn
iam_task_role = aws_iam_role.task.arn

container_definitions = jsonencode(yamldecode(templatefile(
"${path.module}/templates/container_definitions.yaml.tftpl", {
name = local.prefix
cpu = 256
memory = 512
image = "${module.ecr.repository_url}:${var.image_tag}"
container_port = "${var.container_port}"

Check warning on line 90 in aws/fargate_service/main.tf

View workflow job for this annotation

GitHub Actions / lint

Interpolation-only expressions are deprecated in Terraform v0.12.14
log_group = aws_cloudwatch_log_group.service.name
region = data.aws_region.current.name
}
)))
}
12 changes: 12 additions & 0 deletions aws/fargate_service/output.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
output "cluster_name" {
value = module.ecs.name
}

output "docker_push" {
value = <<EOT
aws ecr get-login-password --region ${data.aws_region.current.name} | docker login --username AWS --password-stdin ${module.ecr.repository_url}
docker build -t ${module.ecr.repository_name} .
docker tag ${module.ecr.repository_name}:${var.image_tag} ${module.ecr.repository_url}:latest
docker push ${module.ecr.repository_url}:latest
EOT
}
7 changes: 7 additions & 0 deletions aws/fargate_service/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
provider "aws" {
default_tags {
tags = {
service = var.service
}
}
}
Loading

0 comments on commit 560bd83

Please sign in to comment.