[CI/CD] GitHub Actions로 배포하기

General Dong·2025년 8월 16일
0

CI/CD

목록 보기
11/12
post-thumbnail

AWS EC2, Docker (GHCR 사용), GitHub Actions CI 과정이 준비된 상태를 전제

기존에는 Jenkins 서버를 따로 띄운 다음에 CD 과정를 구성했다.
그러나 Jekins 서버 운영하던 AWS 계정의 프리티어 기간이 곧 종료되기 때문에, 무료로 사용 가능한 GitHub Actinos로 전환하기로 했다.

GitHub Actions 선택 이유는 단순히 가격 뿐만이 아니다.
이미 CI 작업은 GitHub Actions로 구성했고, CI/CD 환경을 하나로 통일하면 관리하기가 편해진다.
또한 GitHub Actions는 이미 만들어져 있는 Action을 가져다 사용할 수 있고, 별도의 버전 관리에 크게 신경쓰지 않아도 되기 때문에, 편리성 측면의 이점이 높다고 판단되어 선택했다.

AWS 접근을 위한 설정

GitHub Actions Runner는 여러 개의 가상 머신으로 존재하며, Workflow가 실행될 때마다 사용되는 가상 머신과 IP 값이 변경된다.
그렇기 때문에 배포 작업을 할 때마다, Runner의 IP 값을 알아내어 SSH로 서버에 접근하는 것을 임시로 허용해줄 것이다.
임시 허용이 귀찮다고 모든 IP에 대하여 SSH를 허용해준다면, 나만의 서버가 아니라 모두의 서버가 될 위험이 있다!

GitHUb Actions에서 AWS 계정에 비교적 안전하게 접근하기 위해서는 다음과 같은 과정이 선행되어야 한다.

IAM 사용자 생성

1. 사용자 생성 버튼 클릭

2. 사용자 이름 설정

3. AmazonEC2FullAccess 정책 선택

4. 정책 생성으로 EC2 모든 권한 정책 추가하기

정책 생성 버튼을 클릭하자.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "ec2:AuthorizeSecurityGroupEgress",
            "Resource": "*"
        }
    ]
}

위의 JSON 값을 복사하고, 아래와 같이 정책 편집기에 넣어주자.

정책 이름을 설정하고 허용된 설정 값이 맞는지 확인한 뒤, 정책 생성 버튼을 클릭한다.

방금 생성한 정책 이름을 검색하고 선택한다.

5. 사용자 생성 완료

위에서 선택한 정책이 맞는지 확인하고 사용자 생성을 하면 된다.

Access Key 생성

1. 액세스 키 만들기 클릭

위에서 만든 IAM 사용자를 클릭하면 아래와 같은 화면이 나타난다.
이때 액세스 키 만들기를 클릭하면 된다. (아직 안 만든 상태라면 액세스 키 1 밑에 "액세스 키 만들기"가 존재한다.)

2. 액세스 키 모범 사례 및 대안

3. 설명 태그 설정 - 선택 사항

4. 액세스 및 비밀 액세스 키 확인

액세스 키와 비밀 액세스 키는 반드시 알고 있어야 함으로 csv 파일로 받거나 따로 적어두자.

보안 그룹 ID 확인

서버로 사용하고 있는 EC2 인스턴스에 적용 중인 보안 그룹 ID를 확인해두자.


사용할 Secrets 구성

이름선택적
EC2_HOSTEC2 Public IP
EC2_USERNAMEEC2 유저 이름
EC2_SSH_KEYEC2 SSH Key (.pem 파일의 값)
AWS_IAM_ACCESS_KEY_IDAWS IAM 사용자 비밀 액세스 키
AWS_IAM_SECRET_ACCESS_KEYAWS IAM 사용자 비밀 액세스 키
AWS_REGIONAWS IAM 사용자의 리전 (서울이면 ap-northeast-2)
AWS_SECURITY_GROUP_ID보안 그룹 ID
DOCKER_REGISTRY사용할 Docker Registry 이름
GIT_TOKENGitHub Access Token 값
GIT_IDGitHub ID (GitHub Container Registry 로그인에 사용)O
SUBMODULE_DIRECTORY서브모듈 디렉토리 경로O

작업 간의 값 전달

나는 Docker Image를 만들 때, Tag로 Commit Hash 값을 사용하여 버전을 관리하고 있다.
그러나 Docker Image를 만드는 단계와 배포 작업은 분리하고 있어, docker pull을 사용하기 위해서는 Tag 값을 가져와야 한다.

작업 간의 값을 전달하는 방법은 다음과 같다. (참고)

  1. 값을 전달할 step에 id 명시
  2. echo "변수명=값" >> "GITHUB_OUTPUT" 명령어로 출력값 생성
  3. 해당 job 출력 목록에 2단계에서 생성한 값 등록하기
jobs:
  job이름:
    outputs:
      출력명: ${{ steps.id명.outputs.변수명 }}
  1. 사용할 다른 job에 need: 전달한_job 추가
  2. 사용할 다른 job에서는 ${{ needs.전달한_job.outputs.출력명 }}로 가져오기

예시로 아래 코드를 확인하면 이해하기 쉬울 것이다.

jobs:
  build:
    runs-on: ubuntu-latest

    outputs:
      docker_tag: ${{ steps.docker.outputs.tag }}

    steps:
      { 생략 }

      - name: Docker Image Tag 설정
        id: docker
        run: |
          echo "tag=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"

      - name: Docker Image 생성
        run: |
          docker build -t test -f submodule/docker/dev.Dockerfile .
          DOCKER_IMAGE=${{ secrets.DOCKER_REGISTRY }}/signal-buddy:${{ steps.docker.outputs.tag }}
          docker tag test $DOCKER_IMAGE
          docker push $DOCKER_IMAGE

      { 생략 }

  deploy:
    runs-on: ubuntu-latest
    needs: build

    concurrency:
      group: deploy
      cancel-in-progress: true  # 기존 실행 중인 워크플로우는 취소, 새 커밋 기준으로 실행

    steps:
      { 생략 }

      - name: AWS EC2 SSH 접속 및 배포
        uses: appleboy/ssh-action@v1
        env:
          DOCKER_IMAGE: ${{ secrets.DOCKER_REGISTRY }}/signal-buddy:${{ needs.build.outputs.docker_tag }}
            
      { 생략 }

output 값에 secret 변수 주의

위에 예시를 보면 build 작업에서 output으로 deploy 작업에 값을 넘겨주었다.
이때 넘겨주는 값에 ${{ secrets.* }}인 값이 들어있다면 output 값을 받아오지 못한다. (이거 때문에 하루종일 삽질했다...)
secret 값을 사용해야 한다면, secret값을 제외한 값만 output으로 넘겨주고 받는 곳에서 secret 값을 따로 추가해주자.

예시

# 전달 가능
echo "output1=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"

# 전달 불가 (전달받은 job에서는 빈 값을 받게 됨)
echo "output2=${{ secrets.DOCKER_REGISTRY }}/signal-buddy:$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"

AWS EC2 SSH 접속

위 과정에서 만든 IAM 액세스 키들과 보안 그룹 ID 등을 활용하여 배포를 해보자.

GitHub Actions Runner의 Public IP 값 가져오기

Runner의 IP는 여러 단계에서 사용하기 때문에 환경변수로 저장해두었다. (참고)

deploy:
  runs-on: ubuntu-latest
  needs: build

  steps:
    - name: GitHub Actions Runner의 Public IP 가져오기
      run: |
        echo "IPV4=$(curl -s ifconfig.me)" >> "$GITHUB_ENV"
  • curl -s ifconfig.me : IPv4 가져오기

동작이 비슷한 haythem/public-ip Action은 사용 불가, 참고

AWS credentials 설정

AWS IAM 사용자로 서버 계정에 접근할 수 있게 한다.

- name: AWS credentials 설정
  uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-access-key-id: ${{ secrets.AWS_IAM_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_IAM_SECRET_ACCESS_KEY }}
    aws-region: ${{ secrets.AWS_REGION }}

Runner의 IP를 인바운드 룰에 임시 추가

Runner가 SSH (22번 Port)로 접근할 수 있게, 인바운드에 해당 IP를 추가해주는 작업이다.

- name: GitHub Actions IP를 인바운드 룰에 임시 추가
  run: |
    aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SECURITY_GROUP_ID }} \
    --protocol tcp --port 22 --cidr ${{ env.IPV4 }}/32

SSH 접속 및 배포

build 작업에서 전달한 값을 사용하여 Docker Image 이름을 만든 것을 확인할 수 있다.
해당 단계에서만 사용하도록 env에 환경변수를 등록하고 DOCKER_IMAGE라는 이름으로 서버를 Docker 환경에서 실행되도록 구성했다. (참고)

deploy.sh 파일은 Blue/Green 방식의 무중단 배포 스크립트다.

- name: AWS EC2 SSH 접속 및 배포
  uses: appleboy/ssh-action@v1
  env:
    DOCKER_IMAGE: ${{ secrets.DOCKER_REGISTRY }}/signal-buddy:${{ needs.build.outputs.docker_tag }}
  with:
    host: ${{ secrets.EC2_HOST }}
    username: ${{ secrets.EC2_USERNAME }}
    key: ${{ secrets.EC2_SSH_KEY }}
    envs: DOCKER_IMAGE
    script: |
      set -e
      echo ${{ secrets.GIT_TOKEN }} | docker login ghcr.io -u ${{ secrets.GIT_ID }} --password-stdin
      docker pull "$DOCKER_IMAGE"
      echo "DOCKER_IMAGE=$DOCKER_IMAGE" > ${{ secrets.SUBMODULE_DIRECTORY }}/docker/.env
      sudo sh ${{ secrets.SUBMODULE_DIRECTORY }}/script/deploy.sh
      docker system prune -a -f || true

임시로 추가된 Runner의 IP를 인바운드 룰에서 삭제

위에서 SSH 접근을 허용한 IP를 삭제해주는 작업이다.
if: always() 설정을 추가함으로써 임시로 추가된 IP는 인바운드 룰에서 반드시 삭제하도록 한다.

- name: GitHub Actions Runner의 IP를 인바운드에서 삭제
  if: always()
  run: |
    aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SECURITY_GROUP_ID }} \
    --protocol tcp --port 22 --cidr ${{ env.IPV4 }}/32

Workflow 동시성 제어

CI/CD를 실행하다보면, 불필요하게 동작이 중복되는 경우가 발생하기도 한다.
예를 들어 main 브랜치에 merge가 되면 배포를 진행할 때, 여러 커밋이 짧은 시간에 연속적으로 merge 되는 경우가 발생하기도 한다.
이런 경우에는 마지막(최신) 커밋만 배포하는 게 불필요하게 중복되는 작업을 줄이는 일이 된다.
이런 문제를 막고자 GitHub Actions에서 제공하는 동시성 제어를 적용할 것이다.

deploy 작업만 동시성 제어를 하여 배포 단계의 자원 낭비를 줄이고, build 단계에서 생성되는 이전 커밋의 Docker Image 버전은 기록해두고자 했다. (참고)

deploy:
  runs-on: ubuntu-latest
  needs: build

  # 동시성 제어
  concurrency:
    group: deploy
    cancel-in-progress: true # 기존 실행 중인 워크플로우는 취소, 새 커밋 기준으로 실행

GitHub Actions에서 제공하는 concurrency는 그룹별로 제어를 할 수 있다.
위 설정은 deploy 작업 수준에서 하나의 Workflow만 동작함을 보장해준다.


위와 같은 단계를 순차적으로 적용하면 GitHub Actions로 AWS EC2에 서버를 자동 배포할 수 있다.
더 확실한 과정을 보고 싶다면 아래 링크를 눌러 yaml 파일 내용을 확인해보면 이해가 바로 될 수 있다.

작성한 yaml 파일 전체


소감

사실 Jenkins에서 GitHub Actions로 전환하는 작업은 프로젝트 초기부터 했어야 했다. 그러나 당시에는 DevOps 쪽에 투자할 시간이 많지 않아, 미리 만들어둔 Jenkins Script를 살짝 수정하여 CI/CD를 적용했다. CI와 CD 환경이 다르다 보니, 끼워 맞추는 느낌으로 자동화 과정을 만들어서 타이밍이 맞지 않아 종종 오류가 발생하기도 했다.

조금은 늦었지만 GitHub Actions로 CI/CD 환경을 통합하여, 일관적이고 한 곳에서 결과를 확인한다는 건 큰 장점으로 다가왔다. 프로젝트 개발 중에 중복된 작업은 자동화 시키는 건 굉장한 장점으로 체감되었다. 서버 개발과 마찬가지로 DevOps 분야에 대한 공부도 지속하며 성장하고 싶다.


참고

작업 간에 정보 전달 | GitHub Docs
작업 간에 정보 전달 상세내용 | GitHub Docs
워크플로 및 작업의 동시 실행 제어 | GitHub Docs
환경 변수 설정 | GitHub Docs
SSH for GitHub Actions | appleboy GitHub
Artifact, AWS IAM - GitHub Actions 배포 자동화 (2) | 인용
GitHub Actions에서 AWS 인증 | aws-actions GitHub

profile
개발에 대한 기록과 복습을 위한 블로그 | Back-end Developer

0개의 댓글