You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何切换到公有子网后ECS任务无法拉取ECR镜像?

ECS任务在公有子网无法拉取ECR镜像问题排查

问题背景

ECS任务在带NAT网关的私有子网中可正常拉取ECR镜像,切换到公有子网后无法访问Docker镜像,触发AWS报错。以下是相关组件的Terraform配置:

VPC配置

locals {
  region = "eu-central-1" // frankfurt
}

// define AWS VPC
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "staging"
  cidr = "172.30.16.0/21"

  azs            = ["${local.region}a", "${local.region}b", "${local.region}c"]
  public_subnets = ["172.30.16.0/24", "172.30.17.0/24", "172.30.18.0/24"]

  enable_vpn_gateway   = false
  enable_dns_support   = true
  enable_dns_hostnames = true
  enable_nat_gateway   = false
}

// identify our central AWS TGW
data "aws_ec2_transit_gateway" "this" {
  id = "tgw-0f6f58192xxxxxx"
}

resource "aws_ec2_transit_gateway_vpc_attachment" "this" {
  subnet_ids         = module.vpc.public_subnets
  transit_gateway_id = data.aws_ec2_transit_gateway.this.id
  vpc_id             = module.vpc.vpc_id
}

resource "aws_route" "name" {
  depends_on = [
    aws_ec2_transit_gateway_vpc_attachment.this,
  ]

  route_table_id         = module.vpc.public_route_table_ids[0]
  gateway_id             = data.aws_ec2_transit_gateway.this.id
  destination_cidr_block = "192.168.1.0/24"
}

ECR配置

locals {
  region = "eu-central-1"
}
resource "aws_ecr_repository" "app-backend" {
  name = "backend"
}

ECS及负载均衡配置

locals {
  docker_registry = "redacted.dkr.ecr.eu-central-1.amazonaws.com/backend"
  backend_image   = "3"
  region          = "eu-central-1"
  prefix          = "backend"
}

resource "aws_ecs_service" "backend" {
  name            = local.prefix
  cluster         = aws_ecs_cluster.backend.id
  task_definition = aws_ecs_task_definition.backend.arn
  launch_type     = "FARGATE"
  desired_count   = 1

  network_configuration {
    subnets         = data.terraform_remote_state.vpc.outputs.public_subnets
    security_groups = [aws_security_group.backend_security_group.id]
  }

  load_balancer {
    target_group_arn = tolist(module.alb.target_group_arns)[0]
    container_name   = local.prefix
    container_port   = 3000
  }
}

resource "aws_ecs_cluster" "backend" {
  name = "${local.prefix}-cluster"
}

resource "aws_security_group" "backend_security_group" {
  name        = "allow_all_traffic"
  description = "Allow outbound traffic to ECR registry"
  vpc_id      = data.terraform_remote_state.vpc.outputs.vpc_id

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_iam_role" "ecs_execution_role" {
  name               = "${local.prefix}-ecs-execution-role"
  assume_role_policy = data.aws_iam_policy_document.ecs_execution_role_assume_policy.json
}

data "aws_iam_policy_document" "ecs_execution_role_assume_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type = "Service"
      identifiers = [
        "ecs-tasks.amazonaws.com",
      ]
    }
  }
}

data "aws_iam_policy_document" "ecs_execution_role" {
  statement {
    actions = [
      "ecr:GetAuthorizationToken",
      "ecr:BatchCheckLayerAvailability",
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchGetImage",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
    effect    = "Allow"
    resources = ["*"]
  }
}

resource "aws_iam_role_policy" "ecs_execution_role_iam_policy" {
  name   = "${local.prefix}-ecs-execution-role"
  role   = aws_iam_role.ecs_execution_role.id
  policy = data.aws_iam_policy_document.ecs_execution_role.json
}

resource "aws_iam_role_policy_attachment" "ecs_execution_role_ecr_policy" {
  role       = aws_iam_role.ecs_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}

resource "aws_ecs_task_definition" "backend" {
  family                   = local.prefix
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 2048
  memory                   = 4096
  task_role_arn            = aws_iam_role.backend_task_role.arn
  execution_role_arn       = aws_iam_role.ecs_execution_role.arn

  container_definitions = jsonencode([
    {
      name      = local.prefix
      image     = "${local.docker_registry}:${local.backend_image}"
      essential = true
      cpu       = 2048
      memory    = 4096
      portMappings = [
        {
          containerPort = 3000
          hostPort      = 3000
        }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = aws_cloudwatch_log_group.backend_ecs_task_log_group.name
          awslogs-region        = local.region
          awslogs-stream-prefix = "stream"
        }
      }
      environment : []
    }
  ])


  runtime_platform {
    cpu_architecture        = "X86_64"
    operating_system_family = "LINUX"
  }
}

resource "aws_cloudwatch_log_group" "backend_ecs_task_log_group" {
  name = "${local.prefix}-ecs-task-log-group"
}



# Load Balancer

module "alb_security_group" {

  source  = "terraform-aws-modules/security-group/aws"
  version = "~> 4.0"

  name   = "${local.prefix}-alb-security-group"
  vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id

  ingress_with_cidr_blocks = [
    {
      from_port   = 80
      to_port     = 80
      protocol    = "TCP"
      description = "HTTP access from the internet"
      cidr_blocks = "0.0.0.0/0"
    },
    {
      from_port   = 443
      to_port     = 443
      protocol    = "TCP"
      description = "HTTPS access from the internet"
      cidr_blocks = "0.0.0.0/0"
    }
  ]

  egress_with_cidr_blocks = [
    {
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      cidr_blocks = "0.0.0.0/0"
    }
  ]
}

module "alb" {
  source  = "terraform-aws-modules/alb/aws"
  version = "~> 8.0"

  name = "app-staging-alb"

  load_balancer_type = "application"

  vpc_id          = data.terraform_remote_state.vpc.outputs.vpc_id
  subnets         = data.terraform_remote_state.vpc.outputs.public_subnets
  security_groups = [module.alb_security_group.security_group_id]

  target_groups = [
    {
      name             = "app-staging-alb-tg"
      backend_protocol = "HTTP"
      backend_port     = 3000
      target_type      = "ip"
      health_check = {
        path                = "/"
        healthy_threshold   = 3
        interval            = 60
        unhealthy_threshold = 10
      }
    }
  ]

  http_tcp_listeners = [
    {
      port               = 80
      protocol           = "HTTP"
      target_group_index = 0
    }
  ]
}

resource "aws_iam_role" "backend_task_role" {
  name               = "${local.prefix}-task-role"
  assume_role_policy = data.aws_iam_policy_document.backend_app_assume.json
}


resource "aws_iam_role_policy" "backend_app_iam_policy" {
  role   = aws_iam_role.backend_task_role.id
  policy = data.aws_iam_policy_document.backend_app_permissions.json
}


resource "aws_iam_instance_profile" "backend_instances_profile" {
  name = "${local.prefix}-instances-profile"
  role = aws_iam_role.backend_task_role.name
}

data "aws_iam_policy_document" "backend_app_assume" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type = "Service"
      identifiers = [
        "ec2.amazonaws.com",
        "eks-fargate-pods.amazonaws.com",
        "ecs-tasks.amazonaws.com",
      ]
    }
  }
}

data "aws_iam_policy_document" "backend_app_permissions" {
  statement {
    actions = [
      "s3:*",
    ]
    resources = ["*"]
  }
}

问题排查与解决方案

核心问题分析

  1. 公有子网路由表缺失互联网默认路由:当前仅给第一个公有路由表添加了TGW路由,其余两个公有子网的路由表无互联网访问路径;且未配置指向互联网网关(IGW)的0.0.0.0/0默认路由。
  2. Fargate任务未分配公网IP:公有子网中的Fargate任务需要公网IP才能直接访问ECR等互联网服务,当前配置未开启该选项。

修复步骤

  1. 添加公有子网默认路由
    为所有公有路由表添加指向IGW的默认路由,确保互联网访问路径:

    resource "aws_route" "public_default" {
      count                = length(module.vpc.public_route_table_ids)
      route_table_id       = module.vpc.public_route_table_ids[count.index]
      destination_cidr_block = "0.0.0.0/0"
      gateway_id           = module.vpc.internet_gateway_id
    }
    
  2. 开启Fargate任务公网IP分配
    在ECS服务的network_configuration中添加assign_public_ip = true:

    resource "aws_ecs_service" "backend" {
      // 其他配置保持不变
      network_configuration {
        subnets         = data.terraform_remote_state.vpc.outputs.public_subnets
        security_groups = [aws_security_group.backend_security_group.id]
        assign_public_ip = true // 添加此行
      }
      // 其他配置保持不变
    }
    
  3. 统一TGW路由配置
    为所有公有路由表添加TGW路由,确保内部网络访问:

    resource "aws_route" "tgw_public" {
      count                = length(module.vpc.public_route_table_ids)
      route_table_id       = module.vpc.public_route_table_ids[count.index]
      gateway_id           = data.aws_ec2_transit_gateway.this.id
      destination_cidr_block = "192.168.1.0/24"
      depends_on = [aws_ec2_transit_gateway_vpc_attachment.this]
    }
    

验证方法

应用Terraform配置后,重新启动ECS任务,通过AWS控制台查看任务状态,或检查CloudWatch日志确认镜像拉取是否成功。


内容的提问来源于stack exchange,提问作者finn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 17:24:50