[CI/CD] Github Actions & AWS Elastic Beanstalk (2)

Noah·2022년 2월 9일
0

CI/CD

목록 보기
2/3

Github Actions를 이용한 CI

선정 이유?

많은 CI 툴이 있고 조사해 보니 가장 많이 쓰이는 CI 툴로는 Jenkins와 Travis CI가 있었다. 나 역시 Jenkins와 Travis CI를 사용하려 했지만 다음과 같은 이유로 Github Actions를 사용하게 되었다.

먼저 Jenkins는 별도의 서버를 필요로 한다. 이미 완성된 프로젝트를 혼자 리팩토링 하는 프로젝트라 로컬 서버에서 CI를 진행해도 무슨 상관이냐 싶겠지만 일단 로컬의 운영체제는 mac OS이다. 즉 프로덕션 서버와의 다른 환경이라는 문제가 있다.

Travis CI는 서버를 제공한다. 하지만 서버 비용을 내야 한다. 가난한 대학생인 나는 무료로 쓸 수 있는 툴을 사용하고 싶었다. 물론 처음 회원가입하면 어느 정도 Credit을 준다. 하지만 많이 주진 않는다.

Github Actions는 Public 리포지토리라면 완전히 무료이고 Private의 경우 제한적으로 무료이다. 나의 경우엔 Public 리포지토리라 완전히 무료였다. 또한 Github Actions의 경우 깃허브에서 제공하는 자체 서비스이므로 외부 서비스라면 필요할 번거로운 연동 설정에 신경 쓸 필요가 없다.

CI/CD 과정

  1. main branch에 push or pull request를 통해 코드를 병합한다.
  2. Github Actions는 반영된 코드에 대한 테스트를 수행한다.
  3. 테스트가 모두 성공하면 Dockerfile을 토대로 docker build 명령어를 통해 정상적으로 image가 만들어지는지 확인한다.
  4. 3번이 성공하면 프로젝트를 deploy.zip으로 압축한다.
  5. 압축한 deploy.zip을 AWS Elastic Beanstalk에 배포한다.
  6. Elastic Beanstalk은 압축 파일을 압축 해제하여 애플리케이션 내의 Dockerfile과 docker-compose file을 토대로 서버를 재배포한다.

CI/CD pipeline의 과정은 위와 같다. 자세하게 알아보기 전에 Docker와 AWS Elastic Beanstalk에 대해 간단하게 언급하고 넘어가자.


AWS Elastic Beanstalk

AWS Elastic Beanstalk는 Java, PHP, Node.js, Python, Docker 등으로 작성된 애플리케이션을 쉽게 배포할 수 있는 Amazon Web Services의 서비스이다. 코드를 AWS Elastic Beanstalk에 간단히 업로드할 수 있으며 용량 프로비저닝, 로드 밸런싱, 자동 크기 조정 및 애플리케이션 상태 모니터링과 같은 작업을 자동으로 처리할 수 있다. AWS Elastic Beanstalk 자체는 애플리케이션을 실행하기 위해 EC2 인스턴스를 사용하고 데이터를 저장하기 위해 S3 스토리지를 사용하지만 사용자는 이러한 문제에 신경 쓸 필요가 없다. 애플리케이션을 업로드하기만 하면 AWS Elastic Beanstalk에서 나머지 작업을 자동으로 처리한다.


Docker

인터넷에서 흥미로운 Python 코드를 발견하고 로컬에서 실행하고 싶다고 가정하자. 그러나 그 이전에 Python 환경이 필요하다. 예를 들어 Anaconda는 이러한 용도로 사용될 수 있다.

Python이 설치된 경우 가장 먼저 해야 할 일은 새 환경을 만들고 개발자가 프로그램 실행을 위해 명시한 dependencies를 설치하는 것이다. 설치가 제대로 되지 않은 경우, 어떤 패키지가 누락되었는지 알려주는 일부 오류 메시지 후에 필수 Python 패키지를 하나씩 설치해야 한다. Conda 또는 Pip를 통해 일부 패키지를 설치할 수 없으면 상황은 더욱 악화된다. 그런 다음 많은 노력으로 수동으로 설치해야 한다. 따라서 Python 프로그램을 실행할 수 있을 때까지 몇 시간이 걸릴 수도 있다.

그리고 이것이 Docker가 등장한 배경이다. Docker는 애플리케이션용 독립된 컨테이너를 생성할 수 있도록 하여 이 문제를 개선하고자 한다. 컨테이너에는 필요한 모든 dependencies와 Python 버전이 포함되므로 최종 사용자는 필요한 모든 dependencies에 대해 걱정할 필요 없이 이 컨테이너를 실행할 수 있다.

컨테이너는 호스트에서 실행되는 자체 리소스가 있는 격리된 환경으로 볼 수 있다. 아래의 그림은 세 개의 서로 다른 컨테이너가 있는 설정의 예다. 여기서 각 컨테이너는 하나의 애플리케이션을 실행하고 각 애플리케이션은 하드 드라이브에 자체 메모리 영역을 가지고 있다.


Github Actions File

이제 마스터에 푸시가 실행될 때마다 Docker 컨테이너를 자동으로 빌드하고 AWS Elastic Beanstalk에 애플리케이션을 배포해야 함을 Github Actions에 알리는 코드가 포함된 YAML 파일은 다음과 같다. 이 파일은 ".github/workflows"라는 폴더에 생성되어야 Github Actions에서 인식할 수 있다.

name: knumarket-auth-server CI/CD

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    env:
      JWT_SECRET: ${{ secrets.JWT_SECRET }}
      NODE_ENV: ${{ secrets.NODE_ENV }}
      DB: ${{ secrets.DB }}
      DB_HOST: ${{ secrets.DB_HOST }}
      DB_PORT: ${{ secrets.DB_PORT }}
      DB_USER: ${{ secrets.DB_USER }}
      DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
      S3_BUCKET: ${{ secrets.S3_BUCKET }}
      KEY: ${{ secrets.KEY }}
      KEYID: ${{ secrets.KEYID }}
      REGION: ${{ secrets.REGION }}
      REDIS: ${{ secrets.REDIS }}


    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x]
        redis-version: [6]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
    - uses: actions/checkout@v2

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v2
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'

    - name: Start Redis
      uses: supercharge/redis-github-action@1.4.0
      with:
        redis-version: ${{ matrix.redis-version }}

    - run: npm install
    - run: npm run test

    - name: Create .env file
      run: |
          touch .env
          echo JWT_SECRET=${{ secrets.JWT_SECRET }} >> .env
          echo NODE_ENV=${{ secrets.NODE_ENV }} >> .env
          echo DB=${{ secrets.DB }} >> .env
          echo DB_HOST=${{ secrets.DB_HOST }} >> .env
          echo DB_PORT=${{ secrets.DB_PORT }} >> .env
          echo DB_USER=${{ secrets.DB_USER }} >> .env
          echo DB_PASSWORD=${{ secrets.DB_PASSWORD }} >> .env
          echo S3_BUCKET=${{ secrets.S3_BUCKET }} >> .env
          echo KEY=${{ secrets.KEY }} >> .env
          echo KEYID=${{ secrets.KEYID }} >> .env
          echo REGION=${{ secrets.REGION }} >> .env
          echo REDIS=${{ secrets.REDIS }} >> .env
          cat .env

    - name: Build the Docker image
      run: docker build -t noah/knumarket-api-server -f Dockerfile .
    
    - name: Generate Deployment Package
      run: zip -r deploy.zip *

    - name: Add .env to deploy.zip
      run: zip deploy.zip .env
    
    - name: Get timestamp
      uses: gerred/actions/current-time@master
      id: current-time
        
    - name: Run string replace
      uses: frabert/replace-string-action@master
      id: format-time
      with:
        pattern: '[:\.]+'
        string: "${{ steps.current-time.outputs.time }}"
        replace-with: '-'
        flags: 'g'

    - name: Deploy to EB
      uses: einaregilsson/beanstalk-deploy@v14
      with:
        aws_access_key: ${{ secrets.KEYID }}
        aws_secret_key: ${{ secrets.KEY }}
        application_name: knumarket-api-server
        environment_name: Knumarketapiserver-env-1
        version_label: "knumarket-api-server${{ steps.format-time.outputs.replaced }}"
        region: ${{ secrets.REGION }}
        deployment_package: deploy.zip

Github Actions File에 대한 각 속성이 의미하는 바는 깃헙 공식 문서에 자세히 나와있으니 참고하길 바란다. 여기서는 간단히 각 step에서 수행하는 역할만 언급하겠다.

Step 1

내 프로젝트 버전과 같은 Node.js 14 버전을 사용하는 환경을 구축했다.

Step 2

마찬가지로 redis 6 버전을 사용하는 환경을 구축했다.

Step 3 & 4

npm install 명령어를 통해 의존하는 모듈을 다운로드하고 npm run test를 통해 테스트를 수행한다.

Step 5

배포될 애플리케이션의 환경 변수 설정을 위해 .env 파일을 만든다.

Step 6

Docker build 명령어를 통해 이미지가 정상적으로 생성되는지 확인한다.

Step 7

zip 명령어를 통해 애플리케이션을 Deploy.zip으로 압축한다.

Step 8

Step 5에서 만든 .env 파일을 Deploy.zip에 추가한다.

Step 9

현재 시간을 구한 다음 나중에 배포된 새 응용 프로그램에 버전을 추가하는 데 사용된다.

Step 10

만들어진 타임스탬프를 가져오고 일부 형식을 적용한다.

Step 11

압축된 애플리케이션 폴더를 최종적으로 AWS Elastic Beanstalk에 배포한다.

이제 Github Actions File을 통해 main branch에 push나 pull request가 일어날 때마다 위 Step 1 ~ 11까지 모든 스텝이 수행되고 모두 통과되면 새로운 애플리케이션의 배포를 적용할 수 있다.


Dockerfile

FROM node:14

WORKDIR /usr

COPY package*.json ./

RUN npm install -g pm2 && pm2 install typescript && npm install

ADD . .

EXPOSE 5000

CMD ["pm2-runtime", "start", "ecosystem.config.js"]

Dockerfile은 DockerImage를 생성하기 위한 스크립트다. 여러 가지 명령어를 토대로 Dockerfile을 작성한 후 빌드 하면 Docker는 Dockerfile에 나열된 명령문을 차례대로 수행하며 DockerImage를 생성해 준다. Dockerfile을 읽을 줄 안다는 것은 해당 이미지가 어떻게 구성되어 있는지 알 수 있다는 의미이다.

마찬가지로 Dockerfile의 각 명령어 또한 공식 문서에 자세히 나와있으니 궁금하면 검색해서 알아보길 바란다. 간단하게만 언급하고 넘어가도록 하겠다.

FROM

FROM은 docker image를 생성할 base 이미지를 지정한다. 버전 명을 지정하지 않으면 가장 최신 이미지를 이용해 이미지를 생성한다.

WORKDIR

컨테이너가 실행된 후 exec로 컨테이너 내부로 들어갔을 때 작업되는 기본 디렉터리다.

COPY

호스트 컴퓨터의 파일을 컨테이너 내부의 특정 디렉터리로 복사하는 역할을 한다. ADD 명령어와 달리 URL에 등록된 파일은 복사가 안된다.

RUN

FROM으로 생성된 이미지 내에서 실행할 명령어를 입력한다. RUN 명령어는 Docker Container를 build 할 때 실행하는 명령어다.

ADD

호스트 컴퓨터의 파일을 컨테이너 내부의 특정 디렉터리로 복사하는 역할을 한다. COPY 명령어와 유사하나 ADD는 http:// URL에 등록된 파일도 복사할 수 있는 기능이 있다.

EXPOSE

Docker image로부터 외부로 노출할 TCP 포트 번호를 입력한다.

CMD

FROM으로 생성된 이미지 내에서 실행할 명령어를 입력한다. CMD 명령어는 Docker container가 실행될 때 실행되는 명령어이다.


docker-compose.yml

Docker compose란, 여러 개의 컨테이너로부터 이루어진 서비스를 구축, 실행하는 순서를 자동으로 하여, 관리를 간단히 하는 기능이다.

Docker compose에서는 compose 파일을 준비하여 커맨드를 1회 실행하는 것으로, 그 파일로부터 설정을 읽어들여 모든 컨테이너 서비스를 실행시키는 것이 가능하다.

version: '3'
services:
  deploy: 
    env_file:
      - .env
    build: 
      context: .
      dockerfile: Dockerfile
    ports:
      - '80:5000'

여기에서 빌드 컨텍스트로 Dockerfile이 연결되고 루트 디렉터리로 현재 작업 디렉터리가 선택된다. 즉, "docker-compose.yml" 파일은 Dockerfile과 동일한 디렉터리에 위치해야 한다. 또한 포트 80은 컨테이너의 포트 5000에 매핑된다. 이는 AWS Elastic Beanstalk에서 호스팅 될 웹 서버가 기본적으로 포트 80에서 수신 연결을 수신하고 생성된 컨테이너가 포트 5000에서 수신하기 때문이다.


마무리

Github Actions와 AWS Elastic Beanstalk을 이용하여 직접 CI/CD 파이프라인을 한 번 만들어놓으니 너무 편했다. 테스트와 배포 자동화를 구축해놓음으로써 개발자는 말 그대로 개발에만 신경 쓸 수 있었다. 직접 만들어봄으로써 더욱 필요성을 느낄 수 있었다. 하지만 아직 Docker에 대한 공부가 부족함을 많이 깨달았다.

참고한 블로그

https://towardsdatascience.com/continuous-deployment-pipeline-using-github-actions-docker-and-aws-185bb3bf41b

https://engineering.linecorp.com/ko/blog/pm2-nodejs/

https://yannmjl.medium.com/what-is-docker-in-simple-english-a24e8136b90b

profile
개발 공부는 🌳 구조다…

0개의 댓글