Terraform部署ECS任务报错STOPPED:CannotPullECRContainerError权限问题
问题描述
我正尝试通过Terraform部署网站容器,所有流程均正常执行,但任务最终失败并处于STOPPED状态,报错信息为CannotPullECRContainerError: AccessDeniedException。以下是我的Terraform脚本:
# Specify the provider and access details provider "aws" { region = "${var.aws_region}" access_key = "${var.access_key}" secret_key = "${var.secret_key}" } ## EC2 ### Network data "aws_availability_zones" "available" {} resource "aws_vpc" "main" { cidr_block = "10.10.0.0/16" } resource "aws_subnet" "main" { count = "${var.az_count}" cidr_block = "${cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)}" availability_zone = "${data.aws_availability_zones.available.names[count.index]}" vpc_id = "${aws_vpc.main.id}" } resource "aws_internet_gateway" "gw" { vpc_id = "${aws_vpc.main.id}" } resource "aws_route_table" "r" { vpc_id = "${aws_vpc.main.id}" route { cidr_block = "0.0.0.0/0" gateway_id = "${aws_internet_gateway.gw.id}" } } resource "aws_route_table_association" "a" { count = "${var.az_count}" subnet_id = "${element(aws_subnet.main.*.id, count.index)}" route_table_id = "${aws_route_table.r.id}" } ### Compute resource "aws_autoscaling_group" "app" { name = "tf-website-asg" vpc_zone_identifier = ["${aws_subnet.main.*.id}"] min_size = "${var.asg_min}" max_size = "${var.asg_max}" desired_capacity = "${var.asg_desired}" launch_configuration = "${aws_launch_configuration.app.name}" } data "template_file" "cloud_config" { template = "${file(\"${path.module}/cloud-config.yml\")}" vars { aws_region = "${var.aws_region}" ecs_cluster_name = "${aws_ecs_cluster.main.name}" ecs_log_level = "info" ecs_agent_version = "latest" ecs_log_group_name = "${aws_cloudwatch_log_group.ecs.name}" } } data "aws_ami" "stable_coreos" { most_recent = true filter { name = "description" values = ["CoreOS Container Linux stable *"] } filter { name = "architecture" values = ["x86_64"] } filter { name = "virtualization-type" values = ["hvm"] } owners = ["595879546273"] # CoreOS } resource "aws_launch_configuration" "app" { security_groups = [ "${aws_security_group.instance_sg.id}", ] key_name = "${var.key_name}" image_id = "${data.aws_ami.stable_coreos.id}" instance_type = "${var.instance_type}" iam_instance_profile = "${aws_iam_instance_profile.app.name}" user_data = "${data.template_file.cloud_config.rendered}" associate_public_ip_address = true lifecycle { create_before_destroy = true } } ### Security resource "aws_security_group" "lb_sg" { description = "controls access to the application ELB" vpc_id = "${aws_vpc.main.id}" name = "tf-ecs-lbsg" ingress { protocol = "tcp" from_port = 80 to_port = 80 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_security_group" "instance_sg" { description = "controls direct access to application instances" vpc_id = "${aws_vpc.main.id}" name = "tf-ecs-instsg" ingress { protocol = "tcp" from_port = 22 to_port = 22 cidr_blocks = [ "${var.admin_cidr_ingress}", ] } ingress { protocol = "tcp" from_port = 80 to_port = 80 security_groups = [ "${aws_security_group.lb_sg.id}", ] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } ## ECS resource "aws_ecs_cluster" "main" { name = "terraform_website_ecs_cluster" } data "template_file" "task_definition" { template = "${file(\"${path.module}/task-definition.json\")}" vars { image_url = "xxxxx.dkr.ecr.eu-west-1.amazonaws.com/nginx:latest" container_name = "website" log_group_region = "${var.aws_region}" log_group_name = "${aws_cloudwatch_log_group.app.name}" } } resource "aws_ecs_task_definition" "website" { family = "tf_website_td" container_definitions = "${data.template_file.task_definition.rendered}" } resource "aws_ecs_service" "test" { name = "tf-ecs-website" cluster = "${aws_ecs_cluster.main.id}" task_definition = "${aws_ecs_task_definition.website.arn}" desired_count = 1 iam_role = "${aws_iam_role.ecs_service.name}" load_balancer { target_group_arn = "${aws_alb_target_group.test.id}" container_name = "website" container_port = "80" } depends_on = [ "aws_iam_role_policy.ecs_service", "aws_alb_listener.front_end", ] } ## IAM resource "aws_iam_role" "ecs_service" { name = "tf_website_ecs_role" assume_role_policy = <<EOF { "Version": "2008-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "ecs.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF } resource "aws_iam_role_policy" "ecs_service" { name = "tf_website_ecs_policy" role = "${aws_iam_role.ecs_service.name}" policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:Describe*", "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", "elasticloadbalancing:DeregisterTargets", "elasticloadbalancing:Describe*", "elasticloadbalancing:RegisterInstancesWithLoadBalancer", "elasticloadbalancing:RegisterTargets", "ecr:GetAuthorizationToken" ], "Resource": "*" } ] } EOF } resource "aws_iam_instance_profile" "app" { name = "tf-ecs-instprofile" role = "${aws_iam_role.app_instance.name}" } resource "aws_iam_role" "app_instance" { name = "tf-ecs-website-instance-role" assume_role_policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF } data "template_file" "instance_profile" { template = "${file(\"${path.module}/instance-profile-policy.json\")}" vars { app_log_group_arn = "${aws_cloudwatch_log_group.app.arn}" ecs_log_group_arn = "${aws_cloudwatch_log_group.ecs.arn}" } } resource "aws_iam_role_policy" "instance" { name = "TfEcsInstanceRole" role = "${aws_iam_role.app_instance.name}" policy = "${data.template_file.instance_profile.rendered}" } ## ALB resource "aws_alb_target_group" "test" { name = "tf-website-ecs-website" port = 80 protocol = "HTTP" vpc_id = "${aws_vpc.main.id}" } resource "aws_alb" "main" { name = "tf-website-alb-ecs" subnets = ["${aws_subnet.main.*.id}"] security_groups = ["${aws_security_group.lb_sg.id}"] } resource "aws_alb_listener" "front_end" { load_balancer_arn = "${aws_alb.main.id}" port = "80" protocol = "HTTP" default_action { target_group_arn = "${aws_alb_target_group.test.id}" type = "forward" } } ## CloudWatch Logs resource "aws_cloudwatch_log_group" "ecs" { name = "tf-ecs-group/ecs-agent" } resource "aws_cloudwatch_log_group" "app" { name = "tf-ecs-group/app-website" }
解决方案
这个问题我之前也碰到过,核心原因是运行ECS任务的EC2实例没有足够权限拉取你的ECR镜像。虽然你的ECS服务角色配置了ecr:GetAuthorizationToken,但真正负责拉取镜像的是EC2实例上的ECS Agent,它使用的是EC2实例关联的IAM角色(也就是你配置的aws_iam_role.app_instance),这个角色目前缺少必要的ECR访问权限。
具体修复步骤
1. 给EC2实例角色补充ECR权限
修改你的instance-profile-policy.json文件,或者直接在Terraform的aws_iam_role_policy.instance资源中,添加以下ECR相关权限:
{ "Version": "2012-10-17", "Statement": [ // 保留你原本的CloudWatch日志相关权限 { "Effect": "Allow", "Action": [ "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage" ], "Resource": "*" } ] }
这四个权限是拉取ECR镜像的必备项:获取授权令牌、检查镜像层可用性、获取镜像层下载链接、批量获取镜像信息。
2. (可选)最小权限优化
如果想遵循最小权限原则,可以把Resource替换成你的ECR仓库的ARN,比如:
"Resource": "arn:aws:ecr:eu-west-1:xxxxx:repository/nginx"
测试阶段用*会更方便,后续再根据需求收紧权限即可。
3. 重新部署并验证
更新配置后,执行Terraform部署命令:
terraform plan terraform apply
部署完成后,建议重启ECS服务或者替换Auto Scaling Group里的EC2实例,确保新的IAM权限生效。之后再查看ECS任务状态,应该就能正常拉取镜像启动了。
额外检查点
- 确认你的ECR仓库资源策略没有拒绝该实例角色的访问(默认ECR仓库允许账户内所有IAM实体访问,除非你自定义了拒绝规则)。
- 可以登录到EC2实例,执行
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/tf-ecs-website-instance-role,验证实例能否正常获取IAM角色凭证。
内容的提问来源于stack exchange,提问作者Wanaoishi
相关产品推荐
相关产品推荐

