HwangHub

[CD] 깃허브 액션으로 배포 자동화 시스템 구축하기 본문

DEV-STUDY/Infra

[CD] 깃허브 액션으로 배포 자동화 시스템 구축하기

HwangJerry 2023. 10. 30. 17:45
항상 코드를 조금만 수정해도 반복해줘야 하는 배포 작업이 번거롭고 부담스러운 작업이라 느꼈다. 또한 여러 스크립트를 입력해야 하므로 보통 복붙을 하게 되는데, 만약 손으로 타이핑할 필요가 있을 때마다 오타 등의 휴먼 에러가 종종 발생하기도 했다. 따라서 이를 최소화하기 위해 배포 자동화에 관심을 갖게 되었고, 깃허브를 소스코드 보관 플랫폼으로 이용하고 있는 김에 젠킨스나 다른 툴 보다도 접근성도 좋고 무료인 깃허브 액션으로 먼저 해당 기술을 익혀보고자 했다.
다음 내용은 송곳매 프로젝트 진행 중에 발생한 이슈를 해결한 내용입니다.

 

배포 자동화 스크립트 전문을 우선 첨부한 뒤에, 차근차근 작성했던 것들을 설명해보고자 한다.

배포 자동화 스크립트 전문은 다음과 같다.

name: stage-server-deploy

on:
  push:
    branches:
      - stage

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          distribution: 'corretto'
          java-version: '17'
      - name: permission injection
        run: chmod +x ./gradlew

      - name: build jar
        run: ./gradlew clean build -x test

      - name: docker login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}

      - name: docker build & push
        run: |
          docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest .
          docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest

      - name: ec2-deploy
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.PEM_KEY }}
          script: |
            docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME}}
            docker-compose up -d
            docker image prune -f

jobs를 왜 세분화하지 않았는지 궁금할 수 있다. 나도 처음에는 다른 포스팅을 참고하면서 우선 작성한 뒤에 차근차근 이해하고 나서, 실행해보니 jobs를 세분화하면 더 좋지 않을까 하는 궁금증을 가지게 되었었다. 하지만 아래 사진을 보면 왜 하나의 job으로 연결한지 이해할 수 있다.

만약 위에서 내가 deploy라고 명명한 job을 3개의 단계로 세분화하여 설정하면 비동기적으로 모든 job이 운영된다. 즉, 순서를 보장하지 않는다는 것이다. 따라서 jar파일을 빌드한 뒤에 이미지를 만들고, 그 이미지를 ec2에 배포하기 위한 스크립트로 설정하기에는 job을 나누는 것은 부적절하다고 판단하였다. 따라서 deploy라는 하나의 job으로 구성하게 되었다.

 

자, 이제 차근차근 스크립트를 해석해보자.

가장 먼저, 액션 이름을 지어주었다.

name: stage-server-deploy

진행하던 프로젝트에서 정식 프로덕션 서버 배포 전 테스트 서버로서 stage 라는 레벨을 설정하였기에, 브랜치가  prod - stage - dev 로 구성되었다. 따라서 stage 에 배포되는 경우에는 자동으로 배포되도록 하여 불편함을 없게 구성하였다. 위에서 언급했듯이 자동화하게 되면 휴먼 에러를 최소화할 수 있으므로 유사한 로직으로 추후 prod-server-deploy도 구성할 계획이다.

 

다음은 어떤 조건에서 이 액션이 자동으로 이루어지게 할 것인가를 설정해 주었다. 나는 stage 브랜치에 pull request로 merge가 발생했을 때, 배포가 자동으로 실행되도록 의도하였다.

on:
  push:
    branches:
      - stage
  • 여기서 분명 merge되었을 때라고 언급했는데, 왜 on push인가 할 수 있다. 왜냐하면, pull_request_merged 라던지 merge 와 같은 명령어는 지원하지 않는다. (pull_request 까지는 있는 걸로 보이는데, reject되는 pull request까지도 깃허브 액션이 돌아가므로 원하는 효과를 구현했다고 보기 어렵다.)
  • pull request를 통한 merge가 이루어졌을 때 push 이벤트가 내부적으로 일어나는 점을 이용하여, 깃 브랜치 보호 전략으로 직접 push를 막아두고 위와 같이 on push branches로 설정해주면 pull request로 merge를 할 때에만 해당 깃허브 액션이 동작하도록 설정할 수 있게 된다.

위에까지는 기본적인 작업의 이름과 발생 이벤트 등 설정값과 같다고 이해하면 되고, 지금부터는 자동화 한 작업의 내용을 담고 있다.

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

 

  • jobs: 는 일련의 작업들을 나열하겠다 선언하는 스크립트이고, deploy는 내가 action하는 job으로 명명한 이름이다. (자유롭게 설정 가능)
  • runs-on: ubuntu-latest는 github action의 runner(실행 환경)을 의미한다. 기업에서는 직접 Runner를 만들어서 사용한다고 하지만, 우리들은 보통 그러기엔 너무 무겁고 어쩌면 불필요하므로 기본적으로 제공되는 Virtual Machine을 사용하게 되며, 저기 ubuntu-latest는 그 가상 머신의 운영체제를 설정해주는 것이다. (흔히 우리가 아는 윈도우나 macOS도 가능하다. 하지만 당연히 대부분 ubuntu 환경을 사용하는 것으로 보인다.) 
  • 그리고 steps는 deploy라고 선언한 job의 작업들을 step by step으로 나열하는 division이다.

 

다음은 자바 소스코드를 build하기 위한 준비 작업이다.

- uses: actions/checkout@v3
- uses: actions/setup-java@v3
  with:
    distribution: 'corretto'
    java-version: '17'
  • github action은 가상 머신 위에서 워크플로우가 진행된다고 서술한 바 있다. 따라서 나의 레포지토리 소스 코드 또한 가상 환경으로 복사해주어야 한다. 하지만 이 과정에서 나의 레포지토리에 접근하는 토큰 권한 작업도 해주어야 하고, 복사 작업도 해주어야 하니 개인이 작업하기에 번거롭다. 따라서 이 작업을 github action에선s checkout이라는 이름으로 지원하고 있다. 따라서 이를 끌어다 사용하였다.
  • 이와 같은 논리로, jar 파일을 빌드하기 위해서는 java가 가상 환경에 있어야 한다. 따라서 이를 설정하기 위해서도 github action에서는 setup-java라는 걸로 라이브러리를 지원하고 있으며, 나는 여기서 aws에서 제공하는 jdk인 correto 17을 이용하였다.

 

나는 jar 파일을 이미지로 빌드하기 때문에 소스코드를 바탕으로 최신 jar 파일을 가공해야 했다. 따라서 jar build를 자동화하였다.

- name: build jar
  run: ./gradlew clean build -x test
  • gradle wrapper(gradlew)를 이용하여 clean을 수행하면 기존에 빌드할 때 발생했던 build artifact를 포함하여 gradle cache와 directory 등의 모든 빌드 산출물을 삭제(빌드 초기화)하는 작업을 수행한다. 혹시 모를 빌드 충돌 문제나 여러 jar 중 구 버전의 jar가 이미지로 빌드되는 것을 막기 위해 clean을 반드시 먼저 수행하고 build될 수 있도록 하였다.
  • -x test를 통해서 테스트 코드를 제외하고 빌드를 수행하도록 하였다. 아직까지 테스트 코드 작성이 익숙하지 않아 아직 테스트 코드가 미완성이기도 해서 임시적으로 선언해 두었다. 추후에 정말 프로덕션을 진행하고 싶다면 가급적 테스트코드로 어플리케이션이 정상으로 동작하는지 테스트하는 것도 자동화할 수 있도록 해볼 예정이다.

+++

위 과정을 수행하면서 이슈가 발생했다. build jar 과정에서 자꾸 멈추길래 봤더니 permission denied라고 나온다.

따라서 실행 권한을 주기 위해 build 스크립트 수행 전에 아래처럼 chmod 코드를 추가해줬다.

- name: permission injection
  run: chmod +x ./gradlew // 추가

- name: build jar
  run: ./gradlew clean build -x test

---

 

이제 build된 jar 파일을 이미지화할 준비를 해야 한다. docker를 사용하기 위해 로그인을 자동화해주었다.

- name: docker login
  uses: docker/login-action@v2
  with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_PASSWORD }}

docker username과 password와 같은 정보는 민감정보이므로 github action의 환경변수와 같이 사용할 수 있도록 repository의 settings에 가보면 아래와 같이 github action에서 환경변수처럼 설정할 수 있는 메뉴가 있다. 이를 이용하려면 secrets라는 변수에서 접근할 수 있다.

 

그리고 나서 이제 이미지를 빌드한 뒤에 도커 허브로 푸쉬해주면 된다. ec2에서 허브로부터 이미지를 다운받은 뒤에 사용할 예정이다.

- name: docker build & push
  run: |
    docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest .
    docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest

별다를 것 없이 위에서 언급한 것 처럼 github action secrets를 이용하여 민감 정보는 가려주고, 이미지 build 및 push 과정을 도커 명령어로 진행해 주었다.

 

+++

이슈가 발생했다. 도커 push를 하는 과정에서 tag를 latest로 자동 생성하지 않아 발생한 이슈로 보인다.

따라서 아래와 같이 코드를 입력해주었더니 통과했다.

---

 

이제 ec2에 ssh로 접근하여 docker 스크립트를 수행하면 배포가 끝난다.

- name: ec2-deploy
  uses: appleboy/ssh-action@master
  with:
    host: ${{ secrets.EC2_HOST }}
    username: ubuntu
    key: ${{ secrets.PEM_KEY }}
    script: |
      docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME}}
      docker-compose up -d
      docker image prune -f

ec2에 ssh로 접근하는 과정은 appleboy라고 하는 ssh-aciton 라이브러리를 이용하였다. (링크 : https://github.com/appleboy/ssh-action) 이 라이브러리를 많이 활용하는 것 같아서 나도 같이 활용해 주었다.

 

마지막 prune 명령어는 gradlew clean을 했던 것처럼 잉여 이미지가 혹시나 발생하면 쳐내기 위해 넣어주었고, 나는 redis 등의 다른 이미지도 컨테이너로 띄워야 하기 때문에 docker-compose를 사용하였다.

 

+++

이슈가 발생했다. 

ec2에서 배포를 하는 과정에서 ssh 에러가 발생한걸로 보인다.

pem키가 제대로 입력되지 않은 것 같아서 깃허브 액션의 환경변수를 한번 더 체크해주었다.

-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA5VcUy25AQlm6Jvkm5LJ5TC+qIzXXi1Ll95bssOJU26yWz8
2P0Gz3iqeZ4tmKXUCp1YOIQEiGvWUWfPwrx0fscqAqEfr+vXUh9zLNk8eJ2HbQ
tYhVktUQLHtDm5G7QWsPkl3hqKJv9P2h2EVXeHTdY+18eRifHuh9mcj4eeSps2
xUoCK7sY7zVWmyPQ9XwHd90KOegKWhY4D1c24OHoiR5K40uj6SEgA8VE4AdbHc
....
... (중략) ...
....
MwIIhBQyKl3AxwAebR9hA0MCyr4XQLhcmHX1jgOBNCa7cITqTl+mAqaALpOzE
mTThlD2k0KwT0X/+UPpShKED1FogX4Qf
-----END RSA PRIVATE KEY-----

알아보니 pem키의 위 ---begin rsa private key--- 부분과 ---end rsa private key---부분을 포함하여 붙여넣기를 해줘야 하는 것이었다. 당연히 제거해야 하는 줄 알았는데, 이를 전부 같이 복사하여 넣어준 결과 정상적으로 동작하였다.

---

 

느낀 점

라이브러리화 된 부분들은 나중에 꼭 전부 생짜로 해봐야 겠다는 생각이 들지만, 우선 빠르게 작업을 쳐내기 위해 사용하게 되었다.

항상 말로만 듣던 CICD중 CD에 대한 첫 발걸음을 내딛은 것 같아서 즐거웠다. 아직 쌓여있는 기술 부채가 많지만, 열심히 정진해보도록 하자.

 

 

 

참고:

https://velog.io/@leeeeeyeon/Github-Actions%EA%B3%BC-Docker%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%EA%B5%AC%EC%B6%95

https://velog.io/@alsgus92/Android-Gradle-Task%EB%8A%94-%EB%8F%84%EB%8C%80%EC%B2%B4-%EC%96%B4%EB%96%A4-%EC%97%AD%ED%95%A0%EC%9D%84-%EC%88%98%ED%96%89-%ED%95%98%EB%8A%94-%EA%B1%B8%EA%B9%8C#:~:text=gradle%20clean%20%EB%8A%94%20%EC%95%9E%EC%84%9C%20gradle,%EB%A1%9C%20%EB%90%98%EB%8F%8C%EB%A6%AC%EB%8A%94%20%EC%97%AD%ED%95%A0%EC%9D%84

Comments