github action으로 CICD 모듈화하기 !

개발 끄적끄적 .. ✍️·2024년 3월 31일
0

CICD를 옮기고 나니 알게된 문제점 🤔

많은 팀에서 cicd 도구로 github action을 사용합니다. 저희 팀에서는 최근까지 젠킨스를 통한 CICD를 운영하다가 github action으로 CICD 작업을 옮기고 있는데요. 대부분의 프로젝트의 CICD를 옮기고 나서 운영하다보니 아래와 같은 문제점을 발견했습니다.

  1. 대부분의 cicd flow가 동일하고, 개발/상용 환경의 일부 입력값만 변경하여 운영되고 있었습니다.
  2. cicd flow를 하나라도 변경하게 되면 개발/상용 각각 수정 사항을 반영 해야하고 이는 곧 휴먼에러의 가능성과 냄새나는 코드라고 판단했습니다.
  3. 현재 여러 프로젝트에서 각자의 CICD를 운영하고 있었기 때문에 동일한 build / deploy를 진행한다고 하더라도 서로 다른 진행 과정을 거치게 되고, 다수의 프로젝트를 운영하는 팀의 입장에서는 서비스 운영 난이도가 높아진다고 판단했습니다.

우선 이전에 운영되던 github action script는 아래와 같습니다. jobs는 크게 builddeploy로 구분되며, build에서는 checkout - install dependency - run test code - build and push 를 진행하고, deploy에서는 ecs service update - notification을 진행합니다.

Build and Deploy (DEV)

on:
  push:
    branches: [develop]

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: project
  ECS_CLUSTER: project-dev
  ECS_SERVICE: project-dev

jobs:
  build:
    runs-on: ubuntu-latest
    environment: DEV
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Go 1.21.4
        uses: actions/setup-go@v3
        with:
          go-version: "1.21.4"

      - name: Install dependencies
        run: go mod download

      - name: Display Go version
        run: go version

      - name: Run Test Code
        run: go test -v ./...

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_DEV }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Create env file
        run: |
          echo "${{ secrets.ENV_FILE_DEV }}" > .env;
          chmod 644 .env
          
      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: develop
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
          
  deploy:
    runs-on: ubuntu-latest
    environment: DEV
    needs: [build]
    if: always()
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_DEV }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Deploy to ECS
        run: |
          aws ecs update-service --cluster=${{ env.ECS_CLUSTER }} --service=${{ env.ECS_SERVICE }} --task-definition=${{ env.ECS_SERVICE }} --force-new-deployment;
      
      - name: Set Status
        id: set_status
        run: |
          if [ "${{ needs.build.result }}" == 'success' ] && [ "${{ job.status }}" == 'success' ]; then
            echo "color=good" >> $GITHUB_OUTPUT
            echo "status=success" >> $GITHUB_OUTPUT
            echo "emoji=:gopher-dance:" >> $GITHUB_OUTPUT
          else
            echo "color=danger" >> $GITHUB_OUTPUT
            echo "status=failure" >> $GITHUB_OUTPUT
            echo "emoji=:gopherlift:" >> $GITHUB_OUTPUT
          fi
          
      - name: Slack Notification
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_COLOR: "${{ steps.set_status.outputs.color }}"
          SLACK_ICON: https://s3.ap-northeast-2.amazonaws.com/slack/gopher.jpeg
          SLACK_TITLE: Message
          SLACK_MESSAGE: "deploy ${{ steps.set_status.outputs.status }} `develop` ${{ steps.set_status.outputs.emoji }}"
          SLACK_USERNAME: "CICD Bot"
          SLACK_WEBHOOK:  ${{ secrets.SLACK_WEBHOOK_URL_DEV }}


github에서 지원하는 action UI상에서는 매우 간단해보이지만, 실제 스크립트를 보게되면 언뜻 보더라도 전체적인 CICD의 전체적인 맥락을 파악하기 어렵습니다. 만약 기존 플로우에서 push 하는 이미지의 tag 정책이 변경되거나, git tagging 같은 새로운 job이 들어간다면 개발/상용환경 스크립트를 동시에 수정해야하는 위험과 번거로움이 수반됩니다.

CICD 모듈화 하기 ! ⚒️

이러한 배경으로 각 작업을 모듈화하는 CICD 모듈화 작업을 진행하게되었습니다.
github action의 job은 workflow_call trigger로 동작 시킬 수 있습니다. uses 통해 action을 custom action을 호출하게 되는데 이 때 inputssecrets를 통해 가변 인자를 넘길 수 있습니다. 각 작업을 모듈화 한 뒤, workflow_call를 통해 호출하기로 했습니다.

먼저 큰 단계를 구성했습니다.
1. test
2. build-and-push
3. deploy
4. notification
5. git tagging / generate release

test

test는 아래와 같습니다. checkout 후, 배포할 코드의 테스트 도커 컨테이너를 생성하고 테스트 코드를 실행시킵니다.

on:
  workflow_call:
    inputs:
      service:
        required: true
        type: string

    secrets:
      repository_access_token:
        required: true
        description: github private repository access token

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Build
        run: |
          docker build --no-cache --build-arg REPOSITORY_ACCESS_TOKEN=${{ secrets.repository_access_token }} --target test-stage --tag ${{ inputs.service }}:test .

      - name: Run Test Code
        run: docker run --rm ${{ inputs.service }}:test

      - name: Clean up
        run: docker rmi -f ${{ inputs.service }}:test

build-and-push

테스트 코드를 통과하게 되면 이미지를 빌드하고 빌드된 이미지를 ecr로 push하게 됩니다. 이 때 push되는 image의 tag는 latest입니다(task definition에서 latest 이미지를 바라보고 있습니다)

on:
  workflow_call:
    inputs:
      service:
        required: true
        type: string
        description: 서비스 이름

      image_tag:
        required: true
        type: string
        description: 이미지 태그

    secrets:
      env_file:
        required: true
        description: env file

      repository_access_token:
        required: true
        description: github private repository access token

      role_arn:
        required: true
        description: AWS ecr role arn

permissions:
  id-token: write
  contents: read

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Create env file
        run: |
          echo "${{ secrets.env_file }}" > .env;
          chmod 644 .env

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.role_arn }}
          aws-region: ap-northeast-2

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build & Push image to Amazon ECR
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/arm64
          push: true
          tags: ${{ steps.login-ecr.outputs.registry }}/${{ inputs.service }}:${{ inputs.image_tag }}
          build-args:
            REPOSITORY_ACCESS_TOKEN=${{ secrets.repository_access_token }}

deploy

정상적으로 image가 push된 후 해당 이미지를 통해 ecs 서비스 업데이트를 진행합니다. 기존에 사용 중이던 script를 실행시켜 진행했습니다.

on:
  workflow_call:
    inputs:
      env:
        required: true
        type: string

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: deploy
        run: |
          make deploy env=${{ inputs.env }}

notification

배포 결과를 슬랙 채널에 공유합니다.

on:
  workflow_call:
    inputs:
      result:
        required: true
        type: string
        description: 빌드 및 배포 결과. 성공, 실패(스킵-빌드 실패)

      service:
        required: true
        type: string
        description: 서비스 이름. slack 메시지에 서비스 이름 표시

      branch:
        required: true
        type: string
        description: 브랜치 이름. slack 메시지에 배포 브랜치 표시

    secrets:
      slack_webhook_url:
        required: true
        description: slack webhook url

jobs:
  nofitication:
    runs-on: ubuntu-latest
    steps:
      - name: Set Status
        id: set_status
        run: |
          if [ "${{ inputs.result }}" == 'success' ]; then
            echo "color=good" >> $GITHUB_OUTPUT
            echo "status=success" >> $GITHUB_OUTPUT
            echo "emoji=:gopher-dance:" >> $GITHUB_OUTPUT
          else
            echo "color=danger" >> $GITHUB_OUTPUT
            echo "status=failure" >> $GITHUB_OUTPUT
            echo "emoji=:gopherlift:" >> $GITHUB_OUTPUT
          fi

      - name: Slack Notification
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_WEBHOOK: ${{ secrets.slack_webhook_url }}
          SLACK_COLOR: ${{ steps.set_status.outputs.color }}
          SLACK_TITLE: Message
          SLACK_MESSAGE: ${{ inputs.service }} deploy ${{ steps.set_status.outputs.status }} `${{ inputs.branch }}` ${{ steps.set_status.outputs.emoji }}
          MSG_MINIMAL: ref, event

git tagging / generate release

배포까지 성공하게 되면 배포된 코드의 tagging과 변경 사항에 대한 release를 자동으로 생성하게 됩니다. 그리고 이 작업의 결과로 새로운 tag를 output으로 내보내게 됩니다.

on:
  workflow_call:
    secrets:
      repository_access_token:
        required: true
        description: github private repository access token

    outputs:
      new_tag:
        value: ${{ jobs.auto-tagging-and-release.outputs.new_tag }}

jobs:
  auto-tagging-and-release:
    runs-on: ubuntu-latest
    outputs:
      new_tag: ${{ steps.set_output.outputs.new_tag }}
    steps:
      - name: Bump version and push tag
        id: tag_version
        uses: mathieudutour/github-tag-action@v6.1
        with:
          github_token: ${{ secrets.repository_access_token }}
          custom_release_rules: hotfix:patch:preminor

      - name: Create a GitHub release
        uses: ncipollo/release-action@v1
        with:
          tag: ${{ steps.tag_version.outputs.new_tag }}
          name: Release ${{ steps.tag_version.outputs.new_tag }}
          body: ${{ steps.tag_version.outputs.changelog }}

      - name: Set output
        id: set_output
        run: echo "new_tag=${{ steps.tag_version.outputs.new_tag }}" >> $GITHUB_OUTPUT

모듈화한 작업들을 하나의 스크립트에서 호출하기 !

이렇게 나눈 각 기능들을 아래와 같이 필요한 동작만 조합해서 사용할 수 있습니다.

name: CI/CD-PROD

on:
  push:
    branches: [ main ]

jobs:
  test:
    uses: ./.github/workflows/test.yml
    with:
      service: mock-project
    secrets:
      repository_access_token: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}

  build-and-push:
    needs: [ test ]
    uses: ./.github/workflows/build-and-push.yml
    with:
      service: mock-project
      image_tag: latest
    secrets:
      env_file: ${{ secrets.ENV_FILE }}
      repository_access_token: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}
      role_arn: ${{ secrets.AWS_ROLE_ARN }}

  deploy:
    needs: [ build-and-push ]
    uses: ./.github/workflows/deploy.yml
    with:
      env: prod

  tag-and-release:
    needs: [ deploy ]
    if: ${{ needs.deploy.result == 'success' }}
    uses: ./.github/workflows/tag-and-release.yml
    secrets:
      repository_access_token: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}

  notification:
    needs: [ deploy ]
    if: always()
    uses: ./.github/workflows/notification.yml
    with:
      result: ${{ needs.deploy.result }}
      service: mock-project
      branch: main
    secrets:
      slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}

  build-new-tag-image-and-push:
    needs: [ tag-and-release ]
    uses: ./.github/workflows/build-and-push.yml
    with:
      service: mock-project
      image_tag: ${{ needs.tag-and-release.outputs.new_tag }}
    secrets:
      env_file: ${{ secrets.ENV_FILE }}
      repository_access_token: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}
      role_arn: ${{ secrets.AWS_ROLE_ARN }}

모듈화를 통해 얻게 된 점 🚀

모듈화 하기 전 스크립트와 비교해보면 어떤가요 ? action UI상 단계는 늘어났지만, 코드 상으로 각 세부 단계나 전체적인 플로우를 파악하기 용이해진 것 같네요. 또한 만약 개발환경 CICD를 구성한다고 했을 때는 개발환경에서 필요없는 tag-and-releasebuild-new-tag-image-and-push 는 제외하고 입력 파라미터만 수정하고, 필요한 구성으로만 각 동작을 조합하여 새로운 CICD를 쉽게 구성 할 수 있습니다.

또한 새로운 스크립트의 마지막을 보면 build-new-tag-image-and-push 동작이 추가되었는데요. tag-and-release 의 결과로 산출된 새로운 태그를 붙여 ecr 이미지로 push하는 동작입니다. 이는 이미지 히스토리를 남기고, 롤백 상황 발생시 최대한 빠르게 전 코드를 반영하게 하기 위함입니다. (build-and-push는 latest tag로 ecr에 푸쉬합니다. task definition에서는 latest를 바라보고 있습니다)

  build-new-tag-image-and-push:
    needs: [ tag-and-release ]
    uses: ./.github/workflows/build-and-push.yml
    with:
      service: mock-project
      image_tag: ${{ needs.tag-and-release.outputs.new_tag }}
    secrets:
      env_file: ${{ secrets.ENV_FILE }}
      repository_access_token: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}
      role_arn: ${{ secrets.AWS_ROLE_ARN }}

만약 위의 동작을 모듈화하지 않았다면, 거의 동일한 코드가 중복되어 사용되어 유지보수의 난이도가 높아졌을 것입니다. 하지만 모듈화를 통해 image tag만 변경하여 해당 모듈을 호출하게 되어 보다 쉽게 새로운 동작을 추가했습니다. 무엇보다 CICD 스크립트 내에서 각 동작의 목적이나 의도도 확실하게 나타나고, 중복 코드도 줄일 수 있다는 것이 가장 큰 얻음이었습니다 !

0개의 댓글