From 5a2bbef0550999a79590f12a7e19aac719015dbd Mon Sep 17 00:00:00 2001 From: Moritz Zimmer Date: Thu, 19 Sep 2024 11:21:51 +0200 Subject: [PATCH] fix(alb): complete example can be applied without errors (#150) * fix(example): complete example can be applied without errors * fix initial apply w/o target * Fix alb lookup * fixed ecr login in example * updated docs * fixed race conditions and trivy errors --------- Co-authored-by: Saef Taher --- README.md | 3 +- examples/complete/README.md | 18 ++- examples/complete/data.tf | 7 +- examples/complete/main.tf | 155 +++++++++++------------- examples/complete/outputs.tf | 4 +- examples/complete/versions.tf | 4 +- examples/fixtures/context/Dockerfile | 12 +- main.tf | 11 +- modules/deployment/iam_code_pipeline.tf | 3 +- outputs.tf | 7 +- 10 files changed, 111 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 485e114..9f33f03 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,8 @@ for example. | [cloudwatch\_log\_group](#output\_cloudwatch\_log\_group) | Name of the CloudWatch log group for container logs. | | [container\_definitions](#output\_container\_definitions) | Container definitions used by this service including all sidecars. | | [ecr\_repository\_arn](#output\_ecr\_repository\_arn) | Full ARN of the ECR repository. | -| [ecr\_repository\_url](#output\_ecr\_repository\_url) | URL of the ECR repository. | +| [ecr\_repository\_id](#output\_ecr\_repository\_id) | The registry ID where the repository was created. | +| [ecr\_repository\_url](#output\_ecr\_repository\_url) | The URL of the repository (in the form `aws_account_id.dkr.ecr.region.amazonaws.com/repositoryName`) | | [task\_execution\_role\_arn](#output\_task\_execution\_role\_arn) | ARN of the task execution role that the Amazon ECS container agent and the Docker daemon can assume. | | [task\_execution\_role\_name](#output\_task\_execution\_role\_name) | Friendly name of the task execution role that the Amazon ECS container agent and the Docker daemon can assume. | | [task\_execution\_role\_unique\_id](#output\_task\_execution\_role\_unique\_id) | Stable and unique string identifying the IAM role that the Amazon ECS container agent and the Docker daemon can assume. | diff --git a/examples/complete/README.md b/examples/complete/README.md index 787b5f0..e3fd2e9 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -17,8 +17,8 @@ Note that this example may create resources which cost money. Run `terraform des | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 4.9 | +| [terraform](#requirement\_terraform) | >= 1.3 | +| [aws](#requirement\_aws) | >= 5.32 | | [null](#requirement\_null) | >= 3.2 | | [random](#requirement\_random) | >= 3.4 | @@ -26,7 +26,7 @@ Note that this example may create resources which cost money. Run `terraform des | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.9 | +| [aws](#provider\_aws) | >= 5.32 | | [null](#provider\_null) | >= 3.2 | | [random](#provider\_random) | >= 3.4 | @@ -34,22 +34,20 @@ Note that this example may create resources which cost money. Run `terraform des | Name | Source | Version | |------|--------|---------| -| [alb\_security\_group\_public](#module\_alb\_security\_group\_public) | registry.terraform.io/terraform-aws-modules/security-group/aws | >= 4.17 | +| [alb](#module\_alb) | terraform-aws-modules/alb/aws | ~> 9.0 | | [service](#module\_service) | ../../ | n/a | -| [vpc](#module\_vpc) | registry.terraform.io/terraform-aws-modules/vpc/aws | >= 4.0 | -| [vpc\_endpoints](#module\_vpc\_endpoints) | registry.terraform.io/terraform-aws-modules/vpc/aws//modules/vpc-endpoints | >= 4.0 | +| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 5.0 | ## Resources | Name | Type | |------|------| | [aws_ecs_cluster.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster) | resource | -| [aws_lb.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb) | resource | -| [aws_lb_listener.http](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener) | resource | +| [aws_security_group.egress_all](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [null_resource.initial_image](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | | [random_pet.this](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource | | [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | -| [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/security_group) | data source | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | ## Inputs @@ -61,5 +59,5 @@ Note that this example may create resources which cost money. Run `terraform des | Name | Description | |------|-------------| -| [alb\_dns\_name](#output\_alb\_dns\_name) | n/a | +| [endpoint](#output\_endpoint) | n/a | diff --git a/examples/complete/data.tf b/examples/complete/data.tf index f288dba..edba410 100644 --- a/examples/complete/data.tf +++ b/examples/complete/data.tf @@ -1,8 +1,5 @@ +data "aws_caller_identity" "current" {} + data "aws_availability_zones" "available" { state = "available" } - -data "aws_security_group" "default" { - name = "default" - vpc_id = module.vpc.vpc_id -} diff --git a/examples/complete/main.tf b/examples/complete/main.tf index aa08ee7..bcacdd6 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -1,6 +1,9 @@ locals { container_port = 8000 image_tag = "production" + + vpc_cidr = "10.0.0.0/16" + azs = slice(data.aws_availability_zones.available.names, 0, 3) } resource "random_pet" "this" { @@ -12,15 +15,17 @@ resource "aws_ecs_cluster" "this" { } module "vpc" { - source = "registry.terraform.io/terraform-aws-modules/vpc/aws" - version = ">= 4.0" + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.0" + + azs = local.azs + cidr = local.vpc_cidr + name = random_pet.this.id + private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)] + public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)] - azs = slice(data.aws_availability_zones.available.names, 0, 3) - cidr = "10.0.0.0/16" - enable_dns_hostnames = true - name = random_pet.this.id - private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] - public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] + enable_nat_gateway = true + single_nat_gateway = true public_subnet_tags = { Tier = "public" @@ -31,95 +36,63 @@ module "vpc" { } } -// see https://docs.aws.amazon.com/AmazonECR/latest/userguide/vpc-endpoints.html for necessary endpoints to run Fargate tasks -module "vpc_endpoints" { - source = "registry.terraform.io/terraform-aws-modules/vpc/aws//modules/vpc-endpoints" - version = ">= 4.0" - - security_group_ids = [data.aws_security_group.default.id] - vpc_id = module.vpc.vpc_id - - endpoints = { - ecr_api = { - service = "ecr.api" - private_dns_enabled = true - subnet_ids = module.vpc.private_subnets - } - - ecr_dkr = { - service = "ecr.dkr" - private_dns_enabled = true - subnet_ids = module.vpc.private_subnets - } - - logs = { - service = "logs" - private_dns_enabled = true - subnet_ids = module.vpc.private_subnets - } - - s3 = { - service = "s3" - service_type = "Gateway" - route_table_ids = flatten([module.vpc.private_route_table_ids, module.vpc.public_route_table_ids]) - } - } -} - -module "alb_security_group_public" { - source = "registry.terraform.io/terraform-aws-modules/security-group/aws" - version = ">= 4.17" - name = "fargate-allow-alb-traffic" - use_name_prefix = false - description = "Security group for example usage with ALB" - vpc_id = module.vpc.vpc_id - - ingress_cidr_blocks = ["0.0.0.0/0"] - ingress_ipv6_cidr_blocks = ["::/0"] - ingress_rules = ["http-80-tcp"] - egress_rules = ["all-all"] -} +module "alb" { + source = "terraform-aws-modules/alb/aws" + version = "~> 9.0" -#tfsec:ignore:aws-elb-alb-not-public -resource "aws_lb" "public" { - drop_invalid_header_fields = true + enable_deletion_protection = false load_balancer_type = "application" name = random_pet.this.id - security_groups = [module.vpc.default_security_group_id, module.alb_security_group_public.security_group_id] subnets = module.vpc.public_subnets -} - -#tfsec:ignore:aws-elb-http-not-used -resource "aws_lb_listener" "http" { - load_balancer_arn = aws_lb.public.arn - port = 80 + vpc_id = module.vpc.vpc_id + + security_group_ingress_rules = { + all_http = { + from_port = 80 + to_port = 80 + ip_protocol = "tcp" + cidr_ipv4 = "0.0.0.0/0" + } + } + security_group_egress_rules = { + all = { + ip_protocol = "-1" + cidr_ipv4 = module.vpc.vpc_cidr_block + } + } - default_action { - type = "fixed-response" + listeners = { + http = { + port = 80 + protocol = "HTTP" - fixed_response { - content_type = "text/plain" - message_body = "Request was not routed." - status_code = 400 + fixed_response = { + content_type = "text/plain" + message_body = "Request was not routed." + status_code = 404 + } } } } module "service" { - source = "../../" + source = "../../" + depends_on = [module.vpc] cpu = 256 + cpu_architecture = "ARM64" cluster_id = aws_ecs_cluster.this.id container_port = local.container_port - create_ingress_security_group = false + create_ingress_security_group = true create_deployment_pipeline = false desired_count = 1 ecr_force_delete = true + ecr_image_tag = local.image_tag memory = 512 service_name = random_pet.this.id + security_groups = [aws_security_group.egress_all.id] vpc_id = module.vpc.vpc_id - ecr_image_tag = local.image_tag // configure autoscaling for this service appautoscaling_settings = { @@ -136,7 +109,7 @@ module "service" { // add listener rules that determine how the load balancer routes requests to its registered targets. https_listener_rules = [{ - listener_arn = aws_lb_listener.http.arn + listener_arn = module.alb.listeners["http"].arn actions = [{ type = "forward" @@ -154,7 +127,7 @@ module "service" { name_prefix = "${substr(random_pet.this.id, 0, 5)}-" backend_protocol = "HTTP" backend_port = local.container_port - load_balancer_arn = aws_lb_listener.http.load_balancer_arn + load_balancer_arn = module.alb.arn target_type = "ip" health_check = { @@ -166,18 +139,34 @@ module "service" { ] } -resource "null_resource" "initial_image" { - provisioner "local-exec" { - command = "aws ecr get-login-password --region ${var.region} | docker login --username AWS --password-stdin ${module.service.ecr_repository_url}" +resource "aws_security_group" "egress_all" { + name_prefix = "${random_pet.this.id}-egress-all-" + description = "Allow all outbound traffic" + vpc_id = module.vpc.vpc_id + + # make sure to secure traffic in production environments + # see https://avd.aquasec.com/misconfig/aws/ec2/avd-aws-0104/#Terraform + #trivy:ignore:AVD-AWS-0104 + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } + + lifecycle { + create_before_destroy = true } +} +resource "null_resource" "initial_image" { provisioner "local-exec" { - command = "docker build --tag ${module.service.ecr_repository_url}:${local.image_tag} ." - working_dir = "${path.module}/../fixtures/context" + command = "aws ecr get-login-password --region ${var.region} | docker login --username AWS --password-stdin ${data.aws_caller_identity.current.account_id}.dkr.ecr.${var.region}.amazonaws.com" } provisioner "local-exec" { - command = "docker push --all-tags ${module.service.ecr_repository_url}" + command = "docker buildx build --tag ${module.service.ecr_repository_url}:${local.image_tag} --platform linux/amd64,linux/arm64 --push ." working_dir = "${path.module}/../fixtures/context" } } diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index 0953589..521a1b9 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -1,3 +1,3 @@ -output "alb_dns_name" { - value = aws_lb.public.dns_name +output "endpoint" { + value = "http://${module.alb.dns_name}/" } diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index 96995bf..0b4c16e 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.3" required_providers { aws = { source = "hashicorp/aws" - version = ">= 4.9" + version = ">= 5.32" } random = { source = "hashicorp/random" diff --git a/examples/fixtures/context/Dockerfile b/examples/fixtures/context/Dockerfile index 78f2701..b348627 100644 --- a/examples/fixtures/context/Dockerfile +++ b/examples/fixtures/context/Dockerfile @@ -1,9 +1,15 @@ -FROM python:3.9-alpine +FROM python:3.12-alpine -ADD index.html index.html -ADD server.py server.py +RUN addgroup -S app && adduser -S app -G app +WORKDIR /home/app + +ADD index.html /home/app/index.html +ADD server.py /home/app/server.py + +RUN chown -R app:app /home/app USER app + EXPOSE 8000 ENTRYPOINT ["python3", "server.py"] diff --git a/main.tf b/main.tf index 68db84a..f654c33 100644 --- a/main.tf +++ b/main.tf @@ -1,12 +1,12 @@ data "aws_lb" "public" { - for_each = var.create_ingress_security_group ? toset([for target in var.target_groups : lookup(target, "load_balancer_arn", "")]) : [] + for_each = var.create_ingress_security_group ? { for idx, target in var.target_groups : idx => lookup(target, "load_balancer_arn", "") } : {} arn = each.value } locals { ingress_targets = flatten( [ - for target in var.target_groups : flatten( + for idx, target in var.target_groups : flatten( [ [ { @@ -14,7 +14,7 @@ locals { from_port = lookup(target, "backend_port", null) to_port = lookup(target, "backend_port", null) protocol = "tcp" - source_security_group_id = tolist(data.aws_lb.public[lookup(target, "load_balancer_arn", null)].security_groups)[0] + source_security_group_id = tolist(data.aws_lb.public[idx].security_groups)[0] prefix = "backend_port" } ], @@ -27,7 +27,7 @@ locals { from_port = target["health_check"]["port"] to_port = target["health_check"]["port"] protocol = "tcp" - source_security_group_id = tolist(data.aws_lb.public[lookup(target, "load_balancer_arn", null)].security_groups)[0] + source_security_group_id = tolist(data.aws_lb.public[idx].security_groups)[0] prefix = "health_check_port" } ] : [] @@ -64,7 +64,8 @@ module "sg" { } resource "aws_security_group_rule" "trusted_egress_attachment" { - for_each = { for route in local.ingress_targets : "${route["prefix"]}-${route["source_security_group_id"]}" => route } + depends_on = [data.aws_lb.public] + for_each = { for route in local.ingress_targets : "${route["prefix"]}-${route["protocol"]}-${route["from_port"]}-${route["to_port"]}" => route } type = "egress" from_port = each.value["from_port"] to_port = each.value["to_port"] diff --git a/modules/deployment/iam_code_pipeline.tf b/modules/deployment/iam_code_pipeline.tf index d2971ae..5338bb9 100644 --- a/modules/deployment/iam_code_pipeline.tf +++ b/modules/deployment/iam_code_pipeline.tf @@ -69,9 +69,10 @@ data "aws_iam_policy_document" "code_pipepline_permissions" { resources = [aws_codebuild_project.this.arn] } + # cloudtrail reports that codepipeline actually requires access to `*` + #trivy:ignore:AVD-AWS-0057 statement { actions = [ - # cloudtrail reports that codepipeline actually requires access to `*` "ecs:DescribeTaskDefinition", "ecs:RegisterTaskDefinition", "ecs:TagResource" diff --git a/outputs.tf b/outputs.tf index 9de82a0..16fdebe 100644 --- a/outputs.tf +++ b/outputs.tf @@ -19,8 +19,13 @@ output "ecr_repository_arn" { value = join("", module.ecr[*].arn) } +output "ecr_repository_id" { + description = "The registry ID where the repository was created." + value = join("", module.ecr[*].registry_id) +} + output "ecr_repository_url" { - description = "URL of the ECR repository." + description = "The URL of the repository (in the form `aws_account_id.dkr.ecr.region.amazonaws.com/repositoryName`)" value = join("", module.ecr[*].repository_url) }