AWS load balancer group error (target_id)

Hello everyone,

I am new to the world of Terraform, and I am stuck on deploying my infrastructure on AWS. When I run the terraform plan command, here is the error I encounter:

╷
│ Error: Unsupported attribute
│
│   on .terraform\modules\alb\main.tf line 537, in resource "aws_lb_target_group_attachment" "this":
│  537:   target_id         = each.value.target_id
│     ├────────────────
│     │ each.value is object with 4 attributes
│
│ This object does not have an attribute named "target_id".
╵

and this is my main.tf file:

# * Part 1 - Setup.
locals {
  container_name = "hono-container"
  container_port = 3000
  example        = "hono-ecs"
}

provider "aws" {
  region = "eu-central-2"
}

# * Give Docker permission to pusher Docker Images to AWS.
data "aws_caller_identity" "this" {}
data "aws_ecr_authorization_token" "this" {}
data "aws_region" "this" {}

locals {
  ecr_address = format("%v.dkr.ecr.%v.amazonaws.com", data.aws_caller_identity.this.account_id, data.aws_region.this.name)
}

provider "docker" {
  registry_auth {
    address  = local.ecr_address
    password = data.aws_ecr_authorization_token.this.password
    username = data.aws_ecr_authorization_token.this.user_name
  }
}

# * Part 2 - Build and push Docker image.
module "ecr" {
  source  = "terraform-aws-modules/ecr/aws"
  version = "1.6.0"

  repository_force_delete     = true
  repository_name             = local.container_name
  repository_lifecycle_policy = jsonencode({
    rules = [
      {
        action       = { type = "expire" }
        description  = "Delete all images except a handful of the newest images"
        rulePriority = 1
        selection    = {
          countNumber = 3
          countType   = "imageCountMoreThan"
          tagStatus   = "any"
        }
      }
    ]
  })
}

# * Build our Image locally with the appropriate name to push our Image
# * to our Repository in AWS.
resource "docker_image" "this" {
  name = format("%v:%v", module.ecr.repository_url, formatdate("YYYY-MM-DD'T'hh-mm-ss", timestamp()))
  build { context = "./hono" }
  # Build our Docker Image from the Dockerfile in the § directory.
}

# * Push our Image to our Repository.
resource "docker_registry_image" "this" {
  keep_remotely = true # Do not delete the old image when a new image is built
  name          = docker_image.this.name
}

# * Part 3 - Create VPC
data "aws_availability_zones" "available" { state = "available" }

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.2"
  name    = "hono-vpc"

  azs                = slice(data.aws_availability_zones.available.names, 0, 2)
  # Span subnetworks across multiple avalibility zones
  cidr               = "10.0.0.0/16"
  create_igw         = true # Expose public subnetworks to the Internet
  enable_nat_gateway = true # Hide private subnetworks behind NAT Gateway
  private_subnets    = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets     = ["10.0.101.0/24", "10.0.102.0/24"]
  single_nat_gateway = true
}

# * Part 4 - Create application load balancer
module "alb" {
  source  = "terraform-aws-modules/alb/aws"
  version = "9.7.0"

  name    = "hono-alb"
  vpc_id  = module.vpc.vpc_id
  subnets = module.vpc.public_subnets

  security_group_ingress_rules = {
    all_http = {
      from_port   = 80
      to_port     = 80
      ip_protocol = "tcp"
      description = "HTTP web traffic"
      cidr_ipv4   = "0.0.0.0/0"
    }
  }
  security_group_egress_rules = {
    all = {
      ip_protocol = "-1"
      cidr_ipv4   = "0.0.0.0/0"
    }
  }

  listeners = {
    default_http = {
      port     = 80
      protocol = "HTTP"
      forward  = {
        target_group_key = "default"
      }
    }
  }

  target_groups = {
    default = {
      name_prefix = "hono"
      protocol    = "HTTP"
      port        = local.container_port
      target_type = "instance"
    }
  }

  tags = {
    Project = "Hono"
  }
}

resource "aws_lb_target_group_attachment" "this" {
  target_group_arn = module.alb.target_groups["default"].arn
  target_id        = aws_ecs_service.this.id
  port             = local.container_port
}

# * Step 5 - Create our ECS Cluster.
module "ecs" {
  source  = "terraform-aws-modules/ecs/aws"
  version = "5.9.1"

  cluster_name = local.container_name

  # * Allocate 20% capacity to FARGATE and then split
  # * the remaining 80% capacity 50/50 between FARGATE
  # * and FARGATE_SPOT.
  fargate_capacity_providers = {
    FARGATE = {
      default_capacity_provider_strategy = {
        base   = 20
        weight = 50
      }
    }
    FARGATE_SPOT = {
      default_capacity_provider_strategy = {
        weight = 50
      }
    }
  }
}

# * Step 6 - Create our ECS Task Definition
data "aws_iam_policy_document" "this" {
  version = "2012-10-17"

  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"

    principals {
      identifiers = ["ecs-tasks.amazonaws.com"]
      type        = "Service"
    }
  }
}

resource "aws_iam_role" "this" {
  assume_role_policy = data.aws_iam_policy_document.this.json
}

resource "aws_iam_role_policy_attachment" "this" {
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
  role       = aws_iam_role.this.name
}

resource "aws_ecs_task_definition" "this" {
  container_definitions = jsonencode([
    {
      environment : [
        { name = "NODE_ENV", value = "production" }
      ],
      essential    = true,
      image        = docker_registry_image.this.name,
      name         = local.container_name,
      portMappings = [{ containerPort = local.container_port }],
    }
  ])
  cpu                      = 256
  execution_role_arn       = aws_iam_role.this.arn
  family                   = "family-of-${local.container_name}-tasks"
  memory                   = 512
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
}

# * Step 7 - Run our application.
resource "aws_ecs_service" "this" {
  cluster         = module.ecs.cluster_id
  desired_count   = 1
  launch_type     = "FARGATE"
  name            = "${local.container_name}-service"
  task_definition = aws_ecs_task_definition.this.arn

  lifecycle {
    ignore_changes = [desired_count]
    # Allow external changes to happen without Terraform conflicts, particularly around auto-scaling.
  }

  load_balancer {
    container_name   = local.container_name
    container_port   = local.container_port
    target_group_arn = module.alb.target_groups["default"].arn
  }

  network_configuration {
    security_groups = [module.vpc.default_security_group_id]
    subnets         = module.vpc.private_subnets
  }
}

# * Step 8 - See our application working.
# * Output the URL of our Application Load Balancer so that we can connect to
# * our application running inside  ECS once it is up and running.
output "lb_url" { value = "http://${module.alb.dns_name}" }

I understand that the error is coming from this piece of code:

target_groups = {
    default = {
      name_prefix = "hono"
      protocol    = "HTTP"
      port        = local.container_port
      target_type = "instance"
    }
  }

I’ve read the documentation, but it’s still not entirely clear for me. So, I added this code snippet, thinking it would solve the issue:

resource "aws_lb_target_group_attachment" "this" {
  target_group_arn = module.alb.target_groups["default"].arn
  target_id        = aws_ecs_service.this.id
  port             = local.container_port
}

If I add target_id with a constant value, it works. However, during deployment on AWS, it obviously doesn’t work since it’s not a valid IP address.

I don’t know what to do… anyone has an idea ?

I ran into the same issue, did you find answer for this?

Target_id is a required parameter in target_groups and the doc does not say anything about it. This should solve the problem.

Just ran into this error and found the solution by looking into the source code:

There is an undocumented flag called create_attachment that we need to turn off.

  target_groups = {
    "${var.project_name}-${var.environment}-alb-tg-nginx" = {
      backend_protocol = "HTTP"
      backend_port     = 80
      target_type      = "instance"
      create_attachment = false     # <<<<<<<<<<< 
    }
}