본문으로 바로가기

GitHub Actions + AWS ECR + Docker CI CD 구축하기

category DevOps/CI CD 2023. 9. 11. 22:31
반응형

DDD 9기 동아리 활동이 끝나고 (팀원들과는 출시까지 더 하기로함 !!) AWS 에 구축해두었던 jenkins 서버를 중지시키고 GitHub Actions 로 이동하기 위해 과정들을 정리하고자 한다. (AWS 너무 비싸)


👉 GitHub Organization 생성

먼저 DDD Organization 에서 탈주했다. 이후에 추가될 내용은 Sikdorok(서비스명 이하 식도록) 커뮤니티로 옮겨서 작업할 예정이였다. 그 이유는 Jira 를 연동해서 사용하고 싶었는데 동아리 활동 기간 중 운영진에게 요청하였으나 뭔가 제대로 연결이 이루어 지지 않았다. 아마도 운영진 계정이 직접 Jira 에 참여해야 하는 듯 하다. 그래서 신규로 조직을 구성하고 Jira 까지 연동 깔끔하게 성공 !!


👉 GitHub Actions yml 생성

GitHub Actions yml 을 생성하기 위해 신규로 워크플로우를 추가한다. 간단하게 Java with Gradle Configure로 시작하기 위해 해당 설정값을 선택한 후 커밋 및 푸시한다.


👉 Actions secrets and variables 설정

파이프라인 내에서 사용할 보안처리가 필요한 설정값들을 저장하는 곳이다.
레포지토리의 Settings 에 들어가면 좌측 메뉴 중 Secrets and variables > Actions 가 있으며 New repository secret 을 클릭하고 원하는 값들을 설정하면 된다.

파이프라인에서 호출할 때는 다음처럼 호출한다.

${{ secrets.변수값 }}

👉 gradle.yml 파이프라인 작성

우선 전체 폼은 하단과 같다. 이후 Step 단위로 살펴보자.

name: Deploy

on:
  push:
    branches: [ "dev" ] # multiple - [ 'dev', 'prod' ]

# env
env:
  SPRING_PROFILES_ACTIVE: ${{ github.ref == 'refs/heads/dev' && 'dev' || 'prod' }}
  SLACK_CHANNEL: "#slack-deploy"
  IMAGE_TAG: "latest"
  ECR_REPOSITORY: "ecr-repository"
  CONFIG_SERVER_ENCRYPT_KEY: ${{ secrets.CONFIG_SERVER_ENCRYPT_KEY }}
  CONFIG_SERVER_USERNAME: ${{ secrets.CONFIG_SERVER_USERNAME }}
  CONFIG_SERVER_PASSWORD: ${{ secrets.CONFIG_SERVER_PASSWORD }}

jobs:
  build:
    runs-on: ubuntu-latest
    steps:

    # Slack Notification
    - name: Slack Notification - Actions Start
      uses: 8398a7/action-slack@v3
      with:
        status: custom
        custom_payload: |
          {
            attachments: [{
              color: 'good',
              text: `==================================================================\n배포 파이프라인이 시작되었습니다.`,
            }]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: always() # Pick up events even if the job fails or is canceled.

    # Checkout
    - name: Checkout
      uses: actions/checkout@v3

    # JDK Setup
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'

    # Gradle Permission
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    # Gradle clean build test
    - name: Build with Gradle
      uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
      env:
        SPRING_PROFILES_ACTIVE: ${{ env.SPRING_PROFILES_ACTIVE }}
      with:
        arguments: :app-api:clean :app-api:build :app-api:test

    # Slack Notification - Build Test Failure
    - name: Slack Notification - Build Test Failure
      uses: 8398a7/action-slack@v3
      with:
        status: failure
        custom_payload: |
          {
            attachments: [{
              color: 'danger',
              text: `Build Test에 실패하였습니다.\n==================================================================`,
            }]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: failure()

    # Slack Notification - Build Test Success
    - name: Slack Notification - Build Test Success
      uses: 8398a7/action-slack@v3
      with:
        status: custom
        custom_payload: |
          {
            attachments: [{
              color: 'good',
              text: `Build Test에 성공하였습니다.`,
            }]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: success()

    # AWS 자격 인증 설정
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
        aws-secret-access-key: ${{ secrets.AWS_SECRETS_ACCESS_KEY }}
        aws-region: ${{ secrets.AWS_REGION }}

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

    # ECR 도커 이미지 Push
    - name: Push docker image to ECR
      id: build-image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
      run: |
        docker build --build-arg SPRING_PROFILES_ACTIVE=$SPRING_PROFILES_ACTIVE --build-arg CONFIG_SERVER_ENCRYPT_KEY=$CONFIG_SERVER_ENCRYPT_KEY --build-arg CONFIG_SERVER_USERNAME=$CONFIG_SERVER_USERNAME --build-arg CONFIG_SERVER_PASSWORD=$CONFIG_SERVER_PASSWORD -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG ./app-api
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

    # Slack Notification - Container Image Build And Push Failure
    - name: Slack Notification - Container Image Build And Push Failure
      uses: 8398a7/action-slack@v3
      with:
        status: failure
        custom_payload: |
          {
            attachments: [{
              color: 'danger',
              text: `Container Image Build And Push에 실패하였습니다.\n==================================================================`,
            }]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: failure()

    # Slack Notification - Container Image Build And Push Success
    - name: Slack Notification - Container Image Build And Push Success
      uses: 8398a7/action-slack@v3
      with:
        status: custom
        custom_payload: |
          {
            attachments: [{
              color: 'good',
              text: `Container Image Build And Push에 성공하였습니다.`,
            }]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: success()

    # GitHub Action IP
    - name: Get Github Actions IP
      id: ip
      uses: haythem/public-ip@v1.2

    # AWS API Server EC2 Inbound Authorize
    - name: Add Github Actions IP to API Server Security group
      run: |
        aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32

    # SSH 연결
    - name: SSH Remote Commands
      uses: appleboy/ssh-action@v1.0.0
      env:
        AWS_REGION: ${{ secrets.AWS_REGION }}
        AWS_REGISTRY_URL_SIKDOROK: ${{ secrets.AWS_REGISTRY_URL_SIKDOROK }}
        ECR_REPOSITORY: ${{ env.ECR_REPOSITORY }}
        IMAGE_TAG: ${{ env.IMAGE_TAG }}
      with:
        host: ${{ env.SPRING_PROFILES_ACTIVE == 'prod' && '' || '3.37.134.103' }}
        username: ubuntu
        key: ${{ env.SPRING_PROFILES_ACTIVE == 'prod' && secrets.PROD_KEY || secrets.DEV_KEY }}
        port: 22
        envs: AWS_REGION, AWS_REGISTRY_URL_SIKDOROK, ECR_REPOSITORY, IMAGE_TAG
        script: |
          aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_REGISTRY_URL_SIKDOROK
          IMAGE_NAME=$ECR_REPOSITORY IMAGE_STORAGE=$AWS_REGISTRY_URL_SIKDOROK BUILD_NUMBER=$IMAGE_TAG ./deploy.sh

    # Slack Notification - Server Run Failure
    - name: Slack Notification - Server Run Failure
      uses: 8398a7/action-slack@v3
      with:
        status: failure
        custom_payload: |
          {
            attachments: [{
              color: 'danger',
              text: `배포에 실패하였습니다.\n==================================================================`,
            }]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: failure()

    # Slack Notification - Server Run Success
    - name: Slack Notification - Server Run Success
      uses: 8398a7/action-slack@v3
      with:
        status: custom
        custom_payload: |
          {
            attachments: [{
              color: 'good',
              text: `배포에 성공하였습니다.\n==================================================================\n`,
            }]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: success()

    # AWS API Server EC2 Inbound Revoke
    - name: Remove Github Actions IP From API Server Security Group
      if: always()
      run: |
        aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32

    # Slack Notification - Deploy Done
    - name: Slack Notification - Deploy Done
      uses: 8398a7/action-slack@v3
      with:
          status: ${{ job.status }}
          fields: repo,message,commit,author,action,eventName,ref,workflow,job,took,pullRequest # selectable (default: repo,message)
      env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required
      if: always()

엄청 길지만 Slack Notification 때문에 중간에 많이 들어가서 그렇지 총 4단계로 이루어져 있다.


👉 OS 선택

jobs:
  build:
    runs-on: ubuntu-latest
    steps:

    # Slack Notification
    - name: Slack Notification - Actions Start
      uses: 8398a7/action-slack@v3
      with:
        status: custom
        custom_payload: |
          {
            attachments: [{
              color: 'good',
              text: `==================================================================\n배포 파이프라인이 시작되었습니다.`,
            }]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: always() # Pick up events even if the job fails or is canceled.

해당 파이프라인은 서버와 동일하게 ubuntu 에서 시작한다. 그리고 배포 파이프라인 시작을 알리는 Slack 알림도 전송한다.
(여기서 Slack Webhook URL 을 생성하는 과정은 생략한다.)


👉 Build Test

    # Checkout
    - name: Checkout
      uses: actions/checkout@v3

    # JDK Setup
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'

    # Gradle Permission
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    # Gradle clean build test
    - name: Build with Gradle
      uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
      env:
        SPRING_PROFILES_ACTIVE: ${{ env.SPRING_PROFILES_ACTIVE }}
      with:
        arguments: :app-api:clean :app-api:build :app-api:test

    # Slack Notification - Build Test Failure
    - name: Slack Notification - Build Test Failure
      uses: 8398a7/action-slack@v3
      with:
        status: failure
        custom_payload: |
          {
            attachments: [{
              color: 'danger',
              text: `Build Test에 실패하였습니다.\n==================================================================`,
            }]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: failure()

    # Slack Notification - Build Test Success
    - name: Slack Notification - Build Test Success
      uses: 8398a7/action-slack@v3
      with:
        status: custom
        custom_payload: |
          {
            attachments: [{
              color: 'good',
              text: `Build Test에 성공하였습니다.`,
            }]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: success()
  1. Checkout
  2. JDK Setup
  3. Gradle Permission
  4. Gradle clean build test
  5. Slack Notification

위와 같은 순서대로 쭉 진행하여 빌드과정을 거친다. 이후 단계에서는 step 사이에 Slack Notification 단계는 설명을 생략한다.


👉 AWS 로그인 및 ECR Docker Image Push

    # AWS 자격 인증 설정
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
        aws-secret-access-key: ${{ secrets.AWS_SECRETS_ACCESS_KEY }}
        aws-region: ${{ secrets.AWS_REGION }}

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

    # ECR 도커 이미지 Push
    - name: Push docker image to ECR
      id: build-image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
      run: |
        docker build --build-arg SPRING_PROFILES_ACTIVE=$SPRING_PROFILES_ACTIVE --build-arg CONFIG_SERVER_ENCRYPT_KEY=$CONFIG_SERVER_ENCRYPT_KEY --build-arg CONFIG_SERVER_USERNAME=$CONFIG_SERVER_USERNAME --build-arg CONFIG_SERVER_PASSWORD=$CONFIG_SERVER_PASSWORD -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG ./app-api
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
  1. AWS 자격 인증 설정
  2. ECR 로그인
  3. ECR 도커 이미지 Push

1번 과정에서 필요한 AWS_ACCESS_KEY, AWS_SECRETS_ACCESS_KEY 는 AWS 의 IAM 에서 사용자를 생성하고 키를 발급받아 설정해야한다. ECR 서비스를 이용하기 위해선 사용자가 ECR 권한이 필요하다.

2번 과정에서 로그인을 처리한다. (실제로 로그인하는 과정의 내용은 하단의 첨부 글 중에서 확인 가능)

3번 과정에서 ECR_REGISTRY 값을 실제 ECR URL을 사용하지 않는 이유는 2번 과정에서 로그인을 하고 나면 Token 값을 전달받는데 이는 임시로 사용가능한 상황이기 때문에 로그인 후 나온 Token 값을 통해서 하는 것 같다. (GitHub Actions 에서 저 Actions 은 어떤건지는 확인해보지 않았음. 궁금하면 직접 Actions 파헤쳐보길)
이후 docker build 를 하고 docker push 를 한다.

위에서는 Cloud Config 를 사용하기 때문에 추가된 옵션이 있으나 필요하지 않은 경우 삭제할 것!
그리고 build 과정에서 Dockerfile 위치를 찾기 때문에 ./app-api 가 아닌 . 으로 작성해야한다. 글쓴이는 멀티모듈이기 때문에 해당 경로를 직접 지정해주었다.


👉 GitHub Ip > Inbound Ingress

    # GitHub Action IP
    - name: Get Github Actions IP
      id: ip
      uses: haythem/public-ip@v1.2

    # AWS API Server EC2 Inbound Authorize
    - name: Add Github Actions IP to API Server Security group
      run: |
        aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
  1. GitHub Action IP 를 획득
  2. 획득한 IP로 배포하려는 EC2 Server 의 Inbound 에 IP 추가

👉 SSH 연결을 통해 배포

    # SSH 연결
    - name: SSH Remote Commands
      uses: appleboy/ssh-action@v1.0.0
      env:
        AWS_REGION: ${{ secrets.AWS_REGION }}
        AWS_REGISTRY_URL_SIKDOROK: ${{ secrets.AWS_REGISTRY_URL_SIKDOROK }}
        ECR_REPOSITORY: ${{ env.ECR_REPOSITORY }}
        IMAGE_TAG: ${{ env.IMAGE_TAG }}
      with:
        host: ${{ env.SPRING_PROFILES_ACTIVE == 'prod' && '배포 서버 IP' || '배포 서버 IP' }}
        username: ubuntu
        key: ${{ env.SPRING_PROFILES_ACTIVE == 'prod' && secrets.PROD_KEY || secrets.DEV_KEY }}
        port: 22
        envs: AWS_REGION, AWS_REGISTRY_URL_SIKDOROK, ECR_REPOSITORY, IMAGE_TAG
        script: |
          aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_REGISTRY_URL_SIKDOROK
          IMAGE_NAME=$ECR_REPOSITORY IMAGE_STORAGE=$AWS_REGISTRY_URL_SIKDOROK BUILD_NUMBER=$IMAGE_TAG ./deploy.sh
  1. 배포하는 서버에서 ECR 로그인을 한다. (이전 jenkins 구축 당시 한번해두어도 자꾸 풀려서 배포 단계에서 항상 로그인되게 처리함)
  2. 배포 스크립트를 작성해둔 것을 실행

참고로 script 는 2줄이다 ! 1줄이 아님

aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_REGISTRY_URL_SIKDOROK
IMAGE_NAME=$ECR_REPOSITORY IMAGE_STORAGE=$AWS_REGISTRY_URL_SIKDOROK BUILD_NUMBER=$IMAGE_TAG ./deploy.sh

위와 같은 과정의 일부분을 이해하고 싶다면 첨부한 글들을 잠시 보고오면 좋을 것 같다.


👉 GitHub Ip > Inbound Revoke

    # AWS API Server EC2 Inbound Revoke
    - name: Remove Github Actions IP From API Server Security Group
      if: always()
      run: |
        aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32

배포 과정이 끝났으니 성공하든 실패하든 잠시 넣어두었던 Ip 를 제거한다.


🚨 에러대응

  1. Inbound 에 넣으려고 해당 명령줄을 실행했더니 인코딩된 Exception Message 를 받아서 해당 값을 디코딩하기 위해선 다음 정책의 권한이 필요했다.
    aws sts decode-authorization-message --encoded-message {인코딩 메시지}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowStsDecode",
            "Effect": "Allow",
            "Action": "sts:DecodeAuthorizationMessage",
            "Resource": "*"
        }
    ]
}
  1. 1번에서 디코딩한 메시지를 확인해보니 GitHub Ip 를 Inbound 에 추가, 수정 및 삭제를 하기 위해서 다음 정책의 권한이 필요했다.
    {
     "Version": "2012-10-17",
     "Statement": [
         {
             "Effect": "Allow",
             "Action": [
                 "ec2:AuthorizeSecurityGroupIngress",
                 "ec2:RevokeSecurityGroupIngress"
             ],
             "Resource": "*"
         }
     ]
    }
반응형

'DevOps > CI CD' 카테고리의 다른 글

AWS ECR + Jenkins CI CD 구축하기  (0) 2023.07.14
Bitbucket SSH key Change  (0) 2023.06.21
Docker + Nginx + Jenkins CI / CD 무중단 배포  (0) 2023.03.20