git/github actions

github action을 통한 CI/CD 배포 자동화 (feat.fastAPI)

최쌈장 2023. 7. 18. 09:15

 

 

 

CI/CD를 활용하는 이유 - 간략한 설명

CI/CD란, continous integration/continous delivery로 애플리케이션 개발 단계를 자동화하여 애플리케이션을 더욱 짧은 주기로 고객에게 제공하는 방법이다. 

출처 : https://www.redhat.com/ko/topics/devops/what-is-ci-cd

CI란, 지속적 통합을 의미한다. 많은 회사들이 병합데이를 설정할 만큼 branch를 병합하는 데 시간을 할애한다.  CI는 변경 사항을 적용할 시점에 애플리케이션을 손상시키지 않도록 자동적으로 병합하는 것을 의미한다. 이때 새로운 의존성을 build하고, test하여 손상시키는지를 자동적으로 확인할 수 있으며 더불어 Merge까지 하는 것을 의미한다.

CD는 CI의 과정을 통해서 나타나는 베이스 코드를 배포할 준비가 되어있도록 하는 것이다. 즉, 하나의 Repository에 적용된 코드 베이스를 설정할 수 있게 되는 것을 의미한다.

마지막으로 continous Deployment, 지속적 배포이다. 실제로 AWS와 같은 서버에 애플리케이션을 배포하기 위해 많은 시간이 필요하며, 해당 과정을 통해서 나타나는 공백 기간동안 사용자가 겪게 되는 불편함이 있을 수 밖에 없다. 해당 과정을 짧은 시간 내로 진행할 수 있게 해주는 것이 continous Deployment로, 자동적으로 배포 과정을 코드로 작성하고 해당 과정을 자체적으로 진행될 수 있도록 하는 것이다.

 

 

이렇게 배포했다!

일단 배포할 EC2 instance를 만들자.

 

 

- Dockerfile

배포를 위해서 git clone를 진행하기 보다는 docker에 직접 담고, 이후에 가져오는 방법을 취하기 위해서 dockerfile를 작성했다. 

코드 설명을 하자면 

FROM python:3.10.10

WORKDIR /code

COPY ./requirement.txt /code/requirement.txt

RUN pip install --no-cache-dir -r /code/requirement.txt

COPY ./app /code/app

CMD uvicorn app.main:app --host 0.0.0.0 --port 3000

# python 3.10.10 버전으로 코드를 작성했고,

 

# docker 컨테이너의 /code 디렉토리에서 작업을 할 것이고(/bin/bash해서 나타나는 파일에 하나를 만든다.)

 

# 현재 위치에 있는 requirement.txt를 컨테이너 내부에 넣고,

 

# 내부에서 requirement.txt의 의존성을 주입하고,

 

# 나의 app 디렉토리에 있는 것을 컨테이너의 작업 환경으로 옮긴다.

 

 

 

- EC2 내부에 Runners를 가동

1) 세팅을 시작한다.

settings > Actions > Runners

 

 

2. 어디서 깔 것인지 확인한다. EC2는 대부분 Linux에서 하니까 리눅스를 채택한다.

- 주어진 대로 그대로 복붙하면 된다. 물론 EC2 console에서 진행해야 한다.

- 라벨 설정하는 부분 매우 중요하니까 절대로 쓴 거 까먹으면 안된다.

 

해당 과정을 거치게 되면 actions-runner이라는 것이 생기게 되고, 안에 들어가서 ./run.sh 를 실행하면

이렇게 배포가 되는 것을 기다리게 된다.

 

 

- workflow.yml 파일 생성

하기 전에!

 

※ 환경 변수 설정

settings > security > secrets and variables > actions

 yml 파일에 설정할 환경 변수를 설정해주기 위해서 이렇게 들어가준다. GH_TOKEN를 설정해주었는데, 도커에 접속을 해야 하기 도커에 접속할 수 있는 패스워드인 토큰을 작성해주었다.

 

 

- workflow.yml

name: Deploy fastAPI server

on:
  push:
    branches: [ main ]


env:
  DOCKER_IMAGE: ghcr.io/swm-99-degree/jaribean-fastapi
  VERSION: ${{ github.sha }}
  NAME: jaribean-matching


jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      
      - name: Setup docker buildx
        id: buildx
        uses: docker/setup-buildx-action@v1
        
      - name: Cache docker layers
        uses: actions/cache@v2
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ env.VERSION }}
          restore-keys: |
            ${{ runner.os }}-buildx-
      - name: Login to ghcr
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GH_TOKEN }}
          # 우리가 방금 복사해서 setting secrets 에 붙여줬던 token 이다. 이름을 기억해 넣어주자.
          # 우리의 ghcr.io 에 접근하기 위함이다.
        
      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          builder: ${{ steps.buildx.outputs.name }}
          push: true
          tags: ${{ env.DOCKER_IMAGE }}:latest
  
  deploy:
    needs: build
    name: Deploy
    runs-on: [ self-hosted, label-jaribean ]

    steps:
      - name: Login to ghcr
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GH_TOKEN }}
      
      - name: Docker run
        run: |
          docker stop ${{ env.NAME }} && docker rm ${{ env.NAME }} && docker rmi ${{ env.DOCKER_IMAGE }}:latest
          docker create -p 3000:3000 --name ${{ env.NAME }} ${{ env.DOCKER_IMAGE }}:latest
          docker cp ~/fastAPI_env/.env ${{ env.NAME }}:/code/.env
          docker cp ~/fastAPI_env/fb_key.json ${{ env.NAME }}:/code/jaribean-3af6f-firebase-adminsdk-voaca-c380f36f12.json
          docker start ${{ env.NAME }}

하나씩 보자구용

 

- event : main branch에 push 명령이 올때마다 밑의 workflow.yml 파일을 변경하겠다

on:
  push:
    branches: [ main ]

 

- build : 빌드 단계

Step 1 : repo에서 나의 정보를 가져온다.

Step 2 : Docker를 깔아준다. actions/cache@v2 를 통해서 docker를 조금 더 빨리 다운로드 받을 수 있다.

Step 3 : Docker에 로그인을 해준다.

Step 4 : Docker hub에 작업한 이미지를 빌드하고 올려준다.

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

// Step 1
      - name: Checkout
        uses: actions/checkout@v2

// Step 2
      - name: Setup docker buildx
        id: buildx
        uses: docker/setup-buildx-action@v1
        
      - name: Cache docker layers
        uses: actions/cache@v2
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ env.VERSION }}
          restore-keys: |
            ${{ runner.os }}-buildx-
            
 // Step 3
      - name: Login to ghcr
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GH_TOKEN }}
          # 우리가 방금 복사해서 setting secrets 에 붙여줬던 token 이다. 이름을 기억해 넣어주자.
          # 우리의 ghcr.io 에 접근하기 위함이다.
 
 // Step 4
      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          builder: ${{ steps.buildx.outputs.name }}
          push: true
          tags: ${{ env.DOCKER_IMAGE }}:latest

참고로 설명하자면 {{ env }} 는 위에서 설정한 코드이고,{{ secrets }}로 시작하는 부분은 내가 따로 환경 변수로 설정한 부분을 가져오는 것이다. 아까 설정한 환경변수에다가 넣은 값이다.

 

 

 

- deploy : 배포 단계

1. runs-on : 나는 어떤 runner에서 작동시킬 것이다! 라는 것이다. 아까 EC2에서 runner 설정을 할 때 label 설정을 했을 텐데 나는 label-jaribean 으로 설정해서 그렇게 놔줬다.

 

Step 1 : 도커에 로그인

Step 2 : 원래 작업 중이던 도커 이미지를 삭제하고, 컨테이너를 삭제하고, 새로운 이미지를 설정하고, 다시 컨테이너를 작동시킨다.

Step 3 : 미리 EC2에 넣어둔 환경 변수를 가져오고, 해당 환경 변수를 적용할 수 있도록 한다. 아래의 코드에서는 EC2 의 ~/fastAPI_env 디렉토리에 있는 파일들을 저렇게 옮겨주었다.

 deploy:
    needs: build
    name: Deploy
    runs-on: [ self-hosted, label-jaribean ]
    
    steps:
    
 // Step 1
      - name: Login to ghcr
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GH_TOKEN }}
      
      - name: Docker run
        run: |
        
 // Step 2
          docker stop ${{ env.NAME }} && docker rm ${{ env.NAME }} && docker rmi ${{ env.DOCKER_IMAGE }}:latest
          docker create -p 3000:3000 --name ${{ env.NAME }} ${{ env.DOCKER_IMAGE }}:latest
          
 // Step 3
          docker cp ~/fastAPI_env/.env ${{ env.NAME }}:/code/.env
          docker cp ~/fastAPI_env/fb_key.json ${{ env.NAME }}:/code/jaribean-3af6f-firebase-adminsdk-voaca-c380f36f12.json
          docker start ${{ env.NAME }}

 

※ 당연한 말이지만...dockerignore이나 .gitignore에 .env 파일을 넣지 않고 git-repo에 올릴 수는 없기 때문에 해당 환경 변수를 다른 방식으로 가져오는 것에서 많은 고민을 요구로 했다. 그래서 미리 EC2에 넣어놓고 docker cp 로 컨테이너 내부로 파일을 옮길 수 있도록 하는 것이 최선의 방식이라고 생각했다.

 

드디어 설정이 끝났다!!

 

그렇다면 pr을 설정한 브랜치로 보내보자

 

 

사실 build나 deploy 하는데 꽤 많은 시간이 걸리기 때문에 이 부분은 좀 이해를 해주고, 그 사이에 딴거 하면 된다.

꽤 많은 시간이 걸린 이후...

 

이렇게 연결해놓았던 디스코드에도 알림이 뜬다. 연결해주신 00 님 감사 ㅠㅠ

 

 

 

 

 

 

CICD 해보니 어떤가요?

시간을 확실히 절약해주는 느낌이 있다. 특히 요즘 컨테이너 기반의 배포를 요구하는 회사가 많았는데, 왜 그렇게 요구를 했는지를 알 수 있었다. (효율성 개미침 ;;)