이것이 점프 투 공작소

CI/CD Pipeline - AWS CodePipeline을 통한 CICD 해보기 with Terraform 본문

aws

CI/CD Pipeline - AWS CodePipeline을 통한 CICD 해보기 with Terraform

겅겅겅 2023. 3. 5. 16:24

CodePipeline이란?

AWS에서 서버리스(serverless)로 제공하는 배포자동화 도구입니다.

CodeCommit, CodeBuild, CodeDeploy 같은 서비스들과 함께 완전한 CICD파이프라인을 구성 할 수 있습니다.

 

CodePipeLine의 구성요소 

- Source 

배포할 소스를 지정합니다. 소스의 리포지토리로 S3 , ECR, CodeCommit, CodeStartSourceConnection (Bigbucket, Github, Github Enterprise Server Actions) 를 사용 할 수 있습니다.

- Build 

CodeBuild, Custom Jenkins 등을 사용해 코드를 빌드 할 수 있습니다. 

- Approval

사용자가 검토 및 승인하는 구간입니다.

- Deploy

S3 , CodeDeploy, ECS, CloudFormation 등의 리소스에 배포 할 수 있습니다. 

- Test

AWS CodeBuild, Selenium, Appium 등의 도구를 사용하여 테스트를 진행 할 수 있습니다.

- Notification 

AWS SNS, Slack등을 활용하여 알림을 보낼 수 있습니다.

 

- Artifacts 

각 단계별 산출물을 의미합니다.

빌드 단계에서 만들어진 빌드 아티팩트, 배포 단계에서 만들어진 패키지, 모든 파일들이 아티팩트가 될 수 있습니다.

CodePipeLine에서 아티팩트를 저장할 저장소(S3, AWS CodeArtifact)를 지정해주면 다음 단계로 자동으로 전달됩니다, 생성된 아티팩트들은 codePipeLine의 성공여부와 관계없이 저장되기에 추후에 참조 할 수도 있습니다.

 

 

CodePipeLine을 이용한 CICD 아키텍쳐 구상도입니다.

1. 깃허브에 소스가 push되면

2. codePipeLine이 동작하여

3. CodeBuild로 이미지를 만들어 ECR에 업로드하고

4. CodeDeploy로 EC2에 배포합니다.

 

 

배포할 소스 코드

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", helloHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, World!")
}

8080포트로 Hello, World를 출력하는 간단한 go코드입니다.

 

CodePipeLine

CodePipeLine.tf

resource "aws_codepipeline" "codepipeline" {
  name     = ${파이프라인명}
  role_arn = ${역할}

  artifact_store {
    location = ${파이프라인에서 사용할 버킷명}
    type     = "S3"
  }
  
  # 깃허브를 소스로 이용합니다.
  stage {
    name = "Source"
    action {
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["source_output"]

      configuration = {
        ConnectionArn    = aws_codestarconnections_connection.this.arn
        FullRepositoryId = ${깃 레포지토리}
        BranchName       = ${브랜치}
      }
    }
  }

  stage {
    name = "Build"
    action {
      name             = "Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      input_artifacts  = ["source_output"]
      output_artifacts = ["build_output"] # 다음 스테이지로 넘길 아웃풋을 지정합니다.
      version          = "1"

      configuration = {
        ProjectName = ${코드빌드명}   # 생성할 코드빌드를 이용합니다.
      }
    }
  }

 # pipeLine에서 관리자가 직접 승인해주는 단계를 생성합니다
  stage {
    name = "Approve"
    action {
      name     = "Approval"
      category = "Approval"
      owner    = "AWS"
      provider = "Manual"
      version  = "1"
    }
  }

 
  stage {
    name = "Deploy"
    action {
      name            = "Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "CodeDeploy"
      input_artifacts = ["build_output"]
      version         = "1"

      configuration = { # 생성할 codeDeploy를 사용합니다.
        ApplicationName     = ${디플로이앱}
        DeploymentGroupName = ${디플로이그룹}
      }
    }
  }
}

# AWS CodeStar를 외부 서비스와 연결시켜주는 테라폼 리소스
resource "aws_codestarconnections_connection" "this" {
  name          = ${커넥션명}
  provider_type = "GitHub"
}

# iam
module "iam" {
  source                  = "terraform-aws-modules/iam/aws//modules/iam-assumable-role"
  version                 = "~> 4.3"
  create_role             = true
  create_instance_profile = true
  role_name               = ${역할명}
  role_requires_mfa       = false
  trusted_role_services = ["codepipeline.amazonaws.com"]
  custom_role_policy_arns = [
    "arn:aws:iam::aws:policy/AmazonS3FullAccess",
    "arn:aws:iam::aws:policy/AWSCodeStarFullAccess",
    "arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess",
    "arn:aws:iam::aws:policy/AWSCodeDeployFullAccess",
  ]
}

# s3 codePipeLine에서 사용할 버킷
module "s3_artifact" {
  source        = "terraform-aws-modules/s3-bucket/aws"
  bucket        = ${버킷명}
  acl           = "private"
  force_destroy = true
  versioning    = { enabled = false }
}

각 스테이지별 action에 존재하는 input_artifact는 이전 스테이지의 산출물을 의미하고, output_artifacts는 현재 스테이지의 산출물을 의미합니다.

즉 다음 스테이지에 input_artifact은 이전 스테이지의 output_artifacts를 의미합니다.

CodeBuild

codeBuild는 buildspec.yml 파일을 읽어서 소스코드를 빌드합니다.

codePipeLine을 사용하게 되면 CodeBuild를 위한 소스 버킷을 사용할 필요는 없습니다. 

(codePipeLine.tf에서 source로 깃허브 지정)

빌드 결과물 또한 codePipeLine에서 지정한 버킷에 저장하기에 아티팩트버킷도 사용하지 않습니다.

 

CodeBuild.tf

# iam
module "iam" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-assumable-role"
  version = "~> 4.3"

  create_role             = true
  create_instance_profile = true
  role_name               = local.role_name
  role_requires_mfa       = false

  trusted_role_services = ["codebuild.amazonaws.com"]
  custom_role_policy_arns = [
    "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess",
    "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess",
    "arn:aws:iam::aws:policy/AmazonVPCFullAccess",
    "arn:aws:iam::aws:policy/AmazonS3FullAccess"
  ]
}

# codebuild
resource "aws_codebuild_project" "this" {
  name          = ${코드디플로이명}
  build_timeout = ${타임아웃시간}
  service_role  = module.iam.iam_role_arn
  
  source {
    type      = "S3"
    location  = ${소스파일 위치}  
    # 빌드 스펙의 위치 기본적으로 CodeBuild는 소스 코드 루트 디렉터리에서 buildspec.yml 파일을 찾습니다.
    buildspec = ${빌드 스펙 위치}
  }

  environment {
    compute_type    = "BUILD_GENERAL1_SMALL"
    image           = "aws/codebuild/standard:4.0"
    type            = "LINUX_CONTAINER"
    privileged_mode = false
  }

  # vpc 설정
  vpc_config {
    vpc_id             = local.vpc_id
    subnets            = local.private_subnet_ids
    security_group_ids = [local.default_sg_id]
  }

  # codePipeLine을 사용하기에 따로 codeBuild용 아티팩트 버킷을 사용하지않습니다.
  artifacts {
    type = "NO_ARTIFACTS"
  }

}

buildspec.yml

코드 빌드 명령어와 S3에 저장 및 다음 스테이지로 넘길 아티팩트를 지정합니다

version: 0.2 # 
env: # 환경변수를 정의합니다.
  variables:
    AWS_DEFAULT_REGION: "ap-northeast-2"
    ECR_URL: "${ECR주소}"
    ECR_DOCKER_IMAGE: "${이미지명}"
    ECR_DOCKER_TAG: "${태그}"

phases: # 빌드 단계를 정의합니다.
  pre_build:
    commands:
      - aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${ECR_URL}
  build:
    on-failure: ABORT # ABORT or CONTINUE 두가지 명령어 중 하나를 선택 가능합니다.
    commands:
      - docker build -t ${ECR_URL}/${ECR_DOCKER_IMAGE}:${ECR_DOCKER_TAG} .
  post_build:
    commands:
      - docker push ${ECR_URL}/${ECR_DOCKER_IMAGE}:${ECR_DOCKER_TAG}
artifacts:
  files:
    - appspec.yml
    - kill_process.sh
    - start_process.sh
    - docker-compose.yml
  discard-paths: yes
  • on-failure: ABORT는 빌드가 실패한 경우 빌드프로세스를 중단합니다.
  • on-failure: CONTINUE 는 빌드가 실패한 경우 빌드프로세스를 중단합니다.

 

CodeDeploy

배포 대상의 환경, 배포 구성, 배포 그룹을 지정합니다.

CodeDeploy를 EC2에서 사용할 경우 해당 EC2에 codeDeploy에이전트가 설치 및 실행되어 있어야 합니다.

실제로는 CodeDeploy-Agent가 아티팩트(appspec.yml)를 읽어서 배포를 하게됩니다. 따라서 EC2에 S3를 읽을 수 있는 권한을 주어야합니다.

 

구성요소

- Application : 배포할 어플리케이션

- Deployment Group : 배포 대상이 되는 리소스를 의미합니다. 다양한 배포환경(stage, dev, prod)을 지정하여 구성 할 수 있습니다.

- Deployment Configuration : 배포전략을 설정 할 수 있습니다.

 

appspec.yml

appspec.yml 파일은 CodeDeploy에서 배포되는 각 인스턴스에 배포되는 파일 및 디렉토리의 위치와 권한, 배치 작업, 스크립트 및 후행 작업의 실행 명령을 지정합니다.

EC2에서 최초 appspec.yml 파일이 실행될때는 hooks의 ApplicationStop이 실행되지 않습니다.

배포 중 스크립트 관련하여 에러가 날 경우 /var/log/aws/codedeploy-agent/codedeploy-agent.log 파일을 확인해보시면 보다 정확한 정보를 얻으실 수 있습니다.

version: 0.0
os: linux
files:
  - source: / # 아티팩트를 가지고있는 버킷의 소스 위치
    destination: /home/ec2-user # 옮기고자하는 대상 디렉토리
    overwrite: yes
permission:
  - object: /home/ec2-user
    pattern: "**" # 위 디렉토리에 있는 전체 파일
    owner: ec2-user
    group: ec2-user
hooks: # 어떤 상황에 어떤 스크립트를 동작시킬지
  ApplicationStop:
    - location: kill_process.sh
      timeout: 100
      runas: ec2-user
  ApplicationStart:
    - location: start_process.sh
      timeout: 3600
      runas: ec2-user

start_process.sh

도커 컴포즈 파일을 실행시키는 스크립트입니다.

#!/bin/bash

ECR_REPOSITORY="${ACCOUNT_ID}.${ecr레포지토리주소}"
ECR_DOCKER_IMAGE="${ECR_REPOSITORY}/${이미지명}"
ECR_DOCKER_TAG="${태그}"

aws ecr get-login-password --region ap-northeast-2 \
  | docker login --username AWS --password-stdin ${ECR_REPOSITORY};

export IMAGE=${ECR_DOCKER_IMAGE};
export TAG=${ECR_DOCKER_TAG};
docker-compose -f /home/ec2-user/docker-compose.yml u

kill_process.sh

도커 컴포즈 파일을 다운시키는 스크립트입니다.

#!/bin/bash

docker-compose -f /home/ec2-user/docker-compose.yml down || true

 

CodeDeployApp.tf

어플리케이션 이름과 배포대상 플렛폼을 지정하는 파일입니다.

compute_platform 속성의 경우 Server(EC2), Lambda, ECS, ECS_EC2, Serverless, ARM, Fargate를 지정 할 수 있습니다.

resource "aws_codedeploy_app" "this" {
  compute_platform = "Server" # EC2
  name             = ${디플로이app명}
}

CodeDeployConfig.tf

배포구성을 정의하는 파일입니다.

minimum_healthy_hosts 요소를 사용하여 배포 중 대상그룹의 50퍼센트의 실행이 유지되도록 합니다.

resource "aws_codedeploy_deployment_config" "this" {
  deployment_config_name = ${디플로이구성명}
  compute_platform = "Server"

  minimum_healthy_hosts {
    type = "FLEET_PERCENT"
    value = 50
  }
}

CodeDeployGroup.tf

배포 그룹을 지정하는 파일입니다.

ec2_tag_filter : 필터링 하고자 하는 EC2의 조건을 지정합니다. 

auto_rollback_configuration가 배포가 실패하면 자동으로 rollback합니다.

해당 코드에서는 ec2태그 name이 ec2-* 인 모든 인스턴스에 배포합니다.

resource "aws_codedeploy_deployment_group" "this" {
  app_name              = ${디플로이앱명}
  deployment_group_name = ${디플로이그룹명}
  service_role_arn      = ${iam_role_arn}
  deployment_config_name = aws_codedeploy_deployment_config.this.id

  ec2_tag_set {
    ec2_tag_filter {
      key   = "Name"
      type  = "KEY_AND_VALUE"
      value = "ec2-*"
    }
  }

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE"]
  }
}

 

CodePipeLine 테스트 

 

Target서버 접속 후 확인

정상적으로 컨테이너가 실행되고있음을 확인했습니다