基于Terraform的AWS CI/CD管道:CodeBuild至ECS部署问题咨询
AWS CI/CD管道部署:CodeBuild构建镜像并推送至ECR的解决方案
问题描述
我正在部署一套AWS CI/CD管道,流程为:开发者提交代码至AWS CodeCommit → AWS CodeBuild构建Docker镜像并推送至ECR → 从ECR部署镜像到ECS Fargate。目前已完成部分Terraform配置,但卡在如何让CodeBuild正确获取Docker镜像URL并推送至ECR的环节,以下是我的Terraform代码:
resource "aws_codecommit_repository" "repo" { repository_name = var.repository_name description = var.description default_branch = var.default_branch # Tags tags = var.tags } # Triggers resource "aws_codecommit_trigger" "triggers" { count = length(var.triggers) repository_name = aws_codecommit_repository.repo.repository_name trigger { name = lookup(element(var.triggers, count.index), "name") events = lookup(element(var.triggers, count.index), "events") destination_arn = lookup(element(var.triggers, count.index), "destination_arn") } } resource "aws_ecr_repository" "my_sample_ecr_repo" { name = "my-sample-ecr-repo" } resource "aws_codebuild_project" "codebuild_project" { name = "sample-code" description = "Codebuild demo with Terraform" build_timeout = "120" artifacts { type = "NO_ARTIFACTS" } source { type = "CodeCommit" location = lookup(var.repository_name) } environment { image = lookup(var.codebuild_params, "IMAGE") type = lookup(var.codebuild_params, "TYPE") compute_type = lookup(var.codebuild_params, "COMPUTE_TYPE") image_pull_credentials_type = lookup(var.codebuild_params, "CRED_TYPE") privileged_mode = true dynamic "environment_variable" { for_each = var.environment_variables content { name = environment_variable.key value = environment_variable.value } } } logs_config { cloudwatch_logs { status = "DISABLED" } s3_logs { status = "DISABLED" } } } resource "aws_ecs_cluster" "my_cluster" { name = "my-cluster" # Naming the cluster } resource "aws_ecs_task_definition" "my_sample_task" { family = "my-sample-task" # Naming the task container_definitions = <<DEFINITION [ { "name": "my-sample-task", "image": "${aws_ecr_repository.my_sample_ecr_repo.repository_url}", "essential": true, "portMappings": [ { "containerPort": 3000, "hostPort": 3000 } ] "memory": 512, "cpu": 256 } ] DEFINITION requires_compatibilities = ["FARGATE"] # Stating that we are using ECS Fargate network_mode = "awsvpc" # Using awsvpc as our network mode as this is required for Fargate memory = 512 # Specifying the memory our container requires cpu = 256 # Specifying the CPU our container requires execution_role_arn = "${aws_iam_role.ecsTaskExecutionRole.arn}" } resource "aws_iam_role" "ecsTaskExecutionRole" { name = "ecsTaskExecutionRole" assume_role_policy = "${data.aws_iam_policy_document.assume_role_policy.json}" } data "aws_iam_policy_document" "assume_role_policy" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } } } resource "aws_iam_role_policy_attachment" "ecsTaskExecutionRole_policy" { role = "${aws_iam_role.ecsTaskExecutionRole.name}" policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } resource "aws_ecs_service" "my_sample_service" { name = "my-sample-service" # Naming our first service cluster = "${aws_ecs_cluster.my_cluster.id}" # Referencing our created Cluster task_definition = "${aws_ecs_task_definition.my_sample_task.arn}" # Referencing the task our service will spin up launch_type = "FARGATE" desired_count = 1 # Setting the number of containers to 3 load_balancer { target_group_arn = "${aws_lb_target_group.target_group.arn}" # Referencing our target group container_name = "${aws_ecs_task_definition.my_sample_task.family}" container_port = 3000 # Specifying the container port } network_configuration { subnets = ["${aws_default_subnet.default_subnet_a.id}", "${aws_default_subnet.default_subnet_b.id}", "${aws_default_subnet.default_subnet_c.id}"] assign_public_ip = true # Providing our containers with public IPs security_groups = ["${aws_security_group.service_security_group.id}"] # Setting the security group } } resource "aws_default_vpc" "default_vpc" { } # Providing a reference to our default subnets resource "aws_default_subnet" "default_subnet_a" { availability_zone = "ap-south-1c" } resource "aws_default_subnet" "default_subnet_b" { availability_zone = "ap-south-1b" } resource "aws_default_subnet" "default_subnet_c" { availability_zone = "ap-south-1a" } resource "aws_security_group" "service_security_group" { ingress { from_port = 0 to_port = 0 protocol = "-1" # Only allowing traffic in from the load balancer security group security_groups = ["${aws_security_group.load_balancer_security_group.id}"] } egress { from_port = 0 # Allowing any incoming port to_port = 0 # Allowing any outgoing port protocol = "-1" # Allowing any outgoing protocol cidr_blocks = ["0.0.0.0/0"] # Allowing traffic out to all IP addresses } } resource "aws_alb" "application_load_balancer" { name = "test-lb-tf" # Naming our load balancer load_balancer_type = "application" subnets = [ # Referencing the default subnets "${aws_default_subnet.default_subnet_a.id}", "${aws_default_subnet.default_subnet_b.id}", "${aws_default_subnet.default_subnet_c.id}" ] # Referencing the security group security_groups = ["${aws_security_group.load_balancer_security_group.id}"] } resource "aws_security_group" "load_balancer_security_group" { ingress { from_port = 80 # Allowing traffic in from port 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] # Allowing traffic in from all sources } egress { from_port = 0 # Allowing any incoming port to_port = 0 # Allowing any outgoing port protocol = "-1" # Allowing any outgoing protocol cidr_blocks = ["0.0.0.0/0"] # Allowing traffic out to all IP addresses } } resource "aws_lb_target_group" "target_group" { name = "target-group" port = 80 protocol = "HTTP" target_type = "ip" vpc_id = "${aws_default_vpc.default_vpc.id}" # Referencing the default VPC health_check { matcher = "200,301,302" path = "/" } } resource "aws_lb_listener" "listener" { load_balancer_arn = "${aws_alb.application_load_balancer.arn}" # Referencing our load balancer port = "80" protocol = "HTTP" default_action { type = "forward" target_group_arn = "${aws_lb_target_group.target_group.arn}" # Referencing our tagrte group } }
解决步骤
1. 为CodeBuild配置ECR访问权限
当前Terraform未配置CodeBuild的服务角色,需要创建具备ECR访问权限的IAM角色:
# CodeBuild服务角色 resource "aws_iam_role" "codebuild_role" { name = "codebuild-ci-cd-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "codebuild.amazonaws.com" } } ] }) } # 附加ECR全访问策略 resource "aws_iam_role_policy_attachment" "codebuild_ecr_policy" { role = aws_iam_role.codebuild_role.name policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess" } # 附加CodeBuild基础管理权限 resource "aws_iam_role_policy_attachment" "codebuild_base_policy" { role = aws_iam_role.codebuild_role.name policy_arn = "arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess" }
将该角色关联到CodeBuild项目,修改aws_codebuild_project.codebuild_project:
resource "aws_codebuild_project" "codebuild_project" { name = "sample-code" description = "Codebuild demo with Terraform" build_timeout = "120" service_role = aws_iam_role.codebuild_role.arn # 添加此行关联角色 # 其余原有配置保持不变 }
2. 在CodeCommit仓库中添加buildspec.yml
CodeBuild需要通过buildspec.yml定义构建流程,在仓库根目录创建该文件:
version: 0.2 phases: pre_build: commands: # 获取AWS账号ID和区域 - ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) - REGION=$(aws configure get region) # 拼接ECR仓库URI - ECR_REPO_URI="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/my-sample-ecr-repo" # 登录ECR - aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ECR_REPO_URI # 用提交ID生成唯一镜像标签 - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) build: commands: # 构建Docker镜像 - docker build -t $ECR_REPO_URI:$IMAGE_TAG . post_build: commands: # 推送镜像至ECR - docker push $ECR_REPO_URI:$IMAGE_TAG # 输出镜像地址(供后续部署使用) - echo "推送完成,镜像地址:$ECR_REPO_URI:$IMAGE_TAG"
3. 修复现有Terraform代码的错误
- 修正CodeBuild源地址:
source.location应使用CodeCommit的HTTP克隆地址,而非仓库名称:
source { type = "CodeCommit" location = aws_codecommit_repository.repo.clone_url_http }
- 修复任务定义语法错误:
container_definitions中portMappings后缺少逗号:
[ { "name": "my-sample-task", "image": "${aws_ecr_repository.my_sample_ecr_repo.repository_url}", "essential": true, "portMappings": [ { "containerPort": 3000, "hostPort": 3000 } ], // 添加此逗号 "memory": 512, "cpu": 256 } ]
4. 可选:自动触发ECS服务更新
若要在镜像推送后自动更新ECS服务,需给CodeBuild角色添加ECS权限:
resource "aws_iam_role_policy_attachment" "codebuild_ecs_policy" { role = aws_iam_role.codebuild_role.name policy_arn = "arn:aws:iam::aws:policy/AmazonECS_FullAccess" }
同时修改buildspec.yml的post_build阶段,添加更新命令(需在仓库中放置任务定义模板task-definition.json,将镜像地址替换为IMAGE_PLACEHOLDER):
post_build: commands: - docker push $ECR_REPO_URI:$IMAGE_TAG - echo "推送完成,镜像地址:$ECR_REPO_URI:$IMAGE_TAG" # 替换任务定义中的镜像地址并注册新任务定义 - sed -i "s|IMAGE_PLACEHOLDER|$ECR_REPO_URI:$IMAGE_TAG|" task-definition.json - aws ecs register-task-definition --cli-input-json file://task-definition.json # 强制更新ECS服务 - aws ecs update-service --cluster my-cluster --service my-sample-service --force-new-deployment
内容的提问来源于stack exchange,提问作者Karthik Raja
相关产品推荐
相关产品推荐

