Github Actions + Docker 로 CICD 구축하기

개발하는 구황작물·2024년 11월 26일
0

개인 프로젝트의 CICD를 Github 와의 연동이 편하고 무료인 github actions로 구축하기로 하였습니다.

대략적인 Flow

  1. Dockerfile 기반으로 이미지를 생성한 다음, Docker Hub로 Push
  2. 배포를 해주는 deploy.sh를 EC2로 전송 후, EC2에서 deploy.sh를 실행
    2-1. deploy.sh에서 Docker Image Pull 받은 후, Docker 컨테이너 실행

0. Setting

Github Actions 스크립트는 프로젝트의 루트 폴더의 .github > workflows > gradle.yml 에 작성하면 됩니다.

깃허브에서 제공하는 템플릿을 사용해도 됩니다.

1. CI

Github Actions 스크립트는 CI, CD 2단계로 나누어 진행을 하였습니다.

아래는 CI 단계 전문입니다.

name: CICD with github action and EC2 using docker

on:
  push:
    branches:
      - 'main' # main 브랜치로 푸시 이벤트 발생 시 스크립트가 실행됩니다.

env: # Github에 미리 설정해둔 Secrets 환경 변수를 가져옵니다. 이 단계는 굳이 설정하지 않고 ${{ secrets.*** }}로 secrets 환경변수를 꺼내 써도 됩니다.
  DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
  DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
  DOCKER_IMAGE_NAME: ${{ secrets.DOCKER_IMAGE }}
  EC2_HOST: ${{ secrets.HOST }}
  EC2_SSH_USER: ${{ secrets.SSH_USER }}
  PRIVATE_KEY: ${{ secrets.SSH }}

permissions:
  id-token: write

jobs: # 1
  CI:
    runs-on: ubuntu-latest # 2
    permissions: # 3
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4 # 4

      - name: Login to Docker Hub # 5
        uses: docker/login-action@v3
        with:
          username: ${{ env.DOCKER_HUB_USERNAME }}
          password: ${{ env.DOCKER_HUB_ACCESS_TOKEN }}

      - name: Set up Docker Buildx # 6
        uses: docker/setup-buildx-action@v3

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          file: ./docker/Dockerfile
          push: true
          tags: ${{env.DOCKER_HUB_USERNAME}}/${{ env.DOCKER_IMAGE_NAME }}:latest
  1. Jobs: CI: workflow 단계를 지정합니다.

  2. runs-on 으로 빌드에 사용될 ubuntu를 지정해줍니다.

  3. permissions: id-token: write 는 Github OIDC의 접근 허용을 위해 추가하였습니다.

steps에는 각 단계별 사용할 동작을 지정합니다.

  1. uses : actions/checkout@v4 : 지정한 브랜치의 코드를 내려받습니다.

- name: Login to Docker Hub # 5
  uses: docker/login-action@v3
  with:
    username: ${{ env.DOCKER_HUB_USERNAME }}
    password: ${{ env.DOCKER_HUB_ACCESS_TOKEN }}

Docker Hub로 이미지를 푸시하기 위해 로그인하는 과정입니다.

- name: Set up Docker Buildx # 6
  uses: docker/setup-buildx-action@v3

- name: Build and push
  uses: docker/build-push-action@v6
  with:
    file: ./docker/Dockerfile
    push: true
    tags: ${{env.DOCKER_HUB_USERNAME}}/${{ env.DOCKER_IMAGE_NAME }}:latest

docker/setup-buildx-action@v3 : 멀티 플랫폼 이미지를 빌드하기 위한 필요한 Buildx를 설치합니다.

이후 docker/build-push-action@v6 로 도커 이미지를 빌드한 후, 이전에 로그인했던 Dockerhub로 Push 합니다.

아래는 도커 이미지 빌드에 사용된 Dockerfile입니다.

# 1. Build Image
FROM amazoncorretto:21-alpine-jdk AS builder

WORKDIR /sources

COPY . .

RUN chmod u+w ./api-module/src/main/resources && \
    chmod +x gradlew &&  \
    ./gradlew clean &&  \
    ./gradlew :api-module:build

# ------------------------------
# 2. Production Image
FROM optimoz/openjre-21.0.3:0.4

WORKDIR /app

COPY --from=builder /sources/api-module/build/libs/api-module-0.0.1-SNAPSHOT.jar /app/quiz.jar

CMD ["java", "-Dspring.profiles.active=prod", "-jar", "quiz.jar"]

빌드를 위해 사용된 라이브러리가 최종 컨테이너 실행 시 필요없을 수 있습니다. 이런 라이브러리는 쓸모없이 공간 낭비를 합니다.

멀티스테이지 빌드를 사용하면 컨테이너 실행 시에 필요없는 빌드에 사용된 라이브러리가 모두 삭제된 상태로 컨테이너를 실행 시킬 수 있습니다. 이로 인해 좀 더 가벼운 컨테이너를 사용할 수 있습니다.


FROM amazoncorretto:21-alpine-jdk AS builder

WORKDIR /sources

COPY . .

RUN chmod u+w ./api-module/src/main/resources && \
    chmod +x gradlew &&  \
    ./gradlew clean &&  \
    ./gradlew :api-module:build

빌드 단계입니다.

amazoncorretto:21-alpine-jdk를 베이스 이미지로 지정하고 builder 라는 이름을 지정합니다.

이후 빌드를 실행할 위치를 WORKDIR /sources 로 지정해주고 gradle build 를 실행해줍니다.

FROM optimoz/openjre-21.0.3:0.4

WORKDIR /app

COPY --from=builder /sources/api-module/build/libs/api-module-0.0.1-SNAPSHOT.jar /app/quiz.jar

CMD ["java", "-Dspring.profiles.active=prod", "-jar", "quiz.jar"]

컨테이너 실행 단계입니다.

실행 환경용 Docker 의 기본 이미지는 optimoz/openjre-21를 사용해주었습니다. (JRE)

이후 빌드한 이미지 파일을 COPY 명령어로 이동 시켜줍니다. 여기서 --from 옵션을 통해 builder라는 이미지로부터 복사를 해줍니다.

2. CD

빌드가 끝났으면 이제 배포를 할 차례입니다.

아래는 CD 스크립트 전문입니다.

  CD:
    needs: [CI] # 1
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

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

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

      - name: Add Github Actions IP to SG # 4
        run: |
          aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32

      - name: Send deploy.sh to EC2 # 5
        uses: appleboy/scp-action@master
        with:
          host: ${{ env.EC2_HOST }}
          username: ${{ env.EC2_SSH_USER }}
          key: ${{ env.PRIVATE_KEY }}
          source: "deploy/deploy.sh"
          target: "/home/ec2-user"
          strip_components: 1
          overwrite: true


      - name: Docker Image Pull and Container run # 6
        uses: appleboy/ssh-action@master
        with:
          host: ${{ env.EC2_HOST }}
          username: ${{ env.EC2_SSH_USER }}
          key: ${{ env.PRIVATE_KEY }}
          script: |
            chmod +x deploy.sh
            ./deploy.sh

      - name: Remove Github Action IP from SG # 7
        run: |
          aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
  1. CD 단계가 실행되기 위해 CI 단계가 성공해야 하기 때문에 needs: [CI] 를 추가해줍니다.

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

AWS 에 접근하기 위해 추가해준 단계입니다.

이 단계는 보안 그룹의 인바운드 설정을 해주기 위해 추가해주었습니다.

배포 스크립트 deploy.sh 를 EC2 로 전달해 주기 위해서는 scp 명령어를 통해 EC2 로 복사를 해주어야 합니다.

문제는 scp 명령어는 ssh 원격 접속 프로토콜을 기반으로 하기 때문에 EC2의 보안 그룹에서 22번 포트를 열어주어야 합니다.

그런데 22번 포트를 활짝 열어놓는것은 너무 찝찝합니다. (집 들어가기 편하려고 집 현관문 열어 놓는 느낌)

이를 위해 배포 전에 github actions IP만 한정적으로 허용해주고 배포가 끝나면 github actions IP를 삭제하는 방식으로 진행하기로 했습니다.

추가적으로 accessKey, secretKey로 접근하는 방법 대신 Github OIDC를 활용하여 접근하는 방식을 활용하였습니다.

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

AWS 보안그룹에 github actions IP가 22번 포트 접근을 허용해주기 위해 github actions IP를 구하는 스크립트 입니다.

- name: Add Github Actions IP to SG # 4
  run: | 
        aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32

AWS 보안 그룹에 위에서 구한 github actions IP를 대상으로 22번 포트를 허용해주는 명령어입니다.

${{ secrets.AWS_SG_ID }} 는 보안 그룹 ID를 뜻하며(github 시크릿 변수에 미리 설정) {{ steps.ip.outputs.ipv4 }}/32는 위에서 구한 github actions IP 입니다.

- name: Send deploy.sh to EC2 # 5
  uses: appleboy/scp-action@master
  with:
    host: ${{ env.EC2_HOST }}
    username: ${{ env.EC2_SSH_USER }}
    key: ${{ env.PRIVATE_KEY }}
    source: "deploy/deploy.sh"
    target: "/home/ec2-user"
    strip_components: 1
    overwrite: true

배포 스크립트 deploy.sh를 EC2 로 전송해줍니다. 이를 위해 appleboy/scp-action를 활용했습니다. (appleboy 감사합니다...)

source: "deploy/deploy.sh" 는 배포 스크립트의 위치,
target: "/home/ec2-user" 는 원격 서버에 배포 스크립트가 위치할 디렉토리를 뜻합니다.
strip_components: 1 는 숫자 만큼source 경로의 선행 경로를 제거해줍니다. (strip_components: 1 이므로 deploy/deploy.sh 에서 선행 디렉토리인 deploy 제거 -> /deploy.sh)
overwrite: true는 원격 서버에 deploy.sh가 이미 존재하면 덮어쓰기를 해줍니다.

- name: Docker Image Pull and Container run # 6
  uses: appleboy/ssh-action@master
  with:
    host: ${{ env.EC2_HOST }}
    username: ${{ env.EC2_SSH_USER }}
    key: ${{ env.PRIVATE_KEY }}
    script: |
            chmod +x deploy.sh
            ./deploy.sh

scp 로 원격 서버로 전송한 배포 스크립트를 실행합니다.

run: |
     aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32

배포 전 AWS 보안 그룹에 추가했던 Github actions IP를 제거 합니다.

최종 Github Actions 스크립트

name: CICD with github action and EC2 using docker

on:
  push:
    branches:
      - 'main'

env:
  DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
  DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
  DOCKER_IMAGE_NAME: ${{ secrets.DOCKER_IMAGE }}
  EC2_HOST: ${{ secrets.HOST }}
  EC2_SSH_USER: ${{ secrets.SSH_USER }}
  PRIVATE_KEY: ${{ secrets.SSH }}

permissions:
  id-token: write

jobs:
  CI:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ env.DOCKER_HUB_USERNAME }}
          password: ${{ env.DOCKER_HUB_ACCESS_TOKEN }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          file: ./docker/Dockerfile
          push: true
          tags: ${{env.DOCKER_HUB_USERNAME}}/${{ env.DOCKER_IMAGE_NAME }}:latest

  CD:
    needs: [CI]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Get Github Action IP
        id: ip
        uses: haythem/public-ip@v1.3

      - name: Add Github Actions IP to SG
        run: |
          aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32

      - name: Send deploy.sh to EC2
        uses: appleboy/scp-action@master
        with:
          host: ${{ env.EC2_HOST }}
          username: ${{ env.EC2_SSH_USER }}
          key: ${{ env.PRIVATE_KEY }}
          source: "deploy/deploy.sh"
          target: "/home/ec2-user"
          strip_components: 1
          overwrite: true


      - name: Docker Image Pull and Container run
        uses: appleboy/ssh-action@master
        with:
          host: ${{ env.EC2_HOST }}
          username: ${{ env.EC2_SSH_USER }}
          key: ${{ env.PRIVATE_KEY }}
          script: |
            chmod +x deploy.sh
            ./deploy.sh

      - name: Remove Github Action IP from SG
        run: |
          aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32

+)
추가적으로 Send deploy.sh to EC2 단계는 굳이 없어도 됩니다.
대신 Docker Image Pull and Container run 에서 script 부분에 배포 스크립트를 작성해주어야 합니다.

profile
어쩌다보니 개발하게 된 구황작물

0개의 댓글