DockerNaver Cloud PlatformGitHub ActionsCI/CD

NCP Container Registry를 활용하여 CI·CD 환경 구축하기

2022.12.05


이 글에서 하는 것들 (요약)

  • NCP Container Registry 생성
  • GitHub action 다루기
    • Docker image를 빌드하여 NCP Container Registry에 push
    • 개발 서버에서 NCP Container Registry에 올라간 이미지를 pull
    • 해당 이미지 run (docker run / docker compose up)

Container Registry란?

  • Docker Registry v2 스펙의 프라이빗 도커 컨테이너 이미지 저장소
  • 컨테이너 이미지를 손쉽게(?) 저장, 관리 및 배포할 수 있다.
  • 즉, private docker hub라고 할 수 있다.
  • Container Registry 요금은 무료이나, Private 도커 컨테이너 이미지는 Object Storage에 저장되므로 사용량에 따른 Object Storage 요금이 발생한다.
    • Object storage 요금(매월) : 1PB 이하 28원/GB, 1PB 이상 26원/GB
  • 서비스에 대한 더 자세한 설명은 NCP 공식 문서를 참고하기 바란다.

GitHub Actions vs Jenkins

  1. GitHub Actions 장점: 별도의 서버가 불필요함 / Jenkins에 비해 낮은 러닝커브 단점: 빌드에 사용할 수 있는 자원에 제한이 있음
  2. Jenkins 장점: 내가 구축한 서버 내에서 빌드 환경을 다양하게 만들 수 있음 단점: 별도의 서버 구축이 필요함 / 다양한 기능을 사용하기 위해서는 러닝커브가 높음

이 외에도 정말 많은 CI/CD 도구들이 존재한다.

  • 만약, Jenkins를 사용해서 CI/CD 환경을 만든다면, 레포지토리에 push가 일어날 때 Jenkins 서버로 webhook을 걸어주면 된다.
  • NCP에서는 Jenkins 이미지로 서버를 바로 생성할 수 있다. 참고

GitHub Actions의 제한사항

공식문서의 제한사항을 살펴보면, 왠만한 작업들은 무료 범위 내에서 충분히 사용할 수 있겠다는 판단을 할 수 있다.

  • 사용 시간 및 작업량의 제한
    • workflow 내의 각 job들은 6시간 안에 종료되어야한다. (6시간 넘어가면 fail)
    • 각 workflow는 35일안에 종료되어야한다. (넘어가면 cancle)
    • 한개의 repository 안에서는 1시간에 1000개의 github api 요청을 수행할 수 있다. (추가적인 요청은 fail)
    • job들은 병렬처리 될 수 있다. (GitHub plan에 따라 다름)
    • 각 repository 별로 10초에 500개 이상의 workflow는 queue 될 수 없다.
  • 요금
    • 무료 사용 범위
      • Free의 경우 500MB의 storage & 1달에 2,000분 run 가능
      • Pro의 경우 1GB의 storage & 1달에 3,000분 run 가능

Container Registry 생성 방법

  1. Object Storage 이용 신청
  2. Object Storage 버킷 생성 - 계정별 권한 설정
  3. Container Registry 생성 - 생성한 Object Storage 버킷 선택
    • 퍼블릭 엔드포인트 (활성화)
      • 외부 네트워크(인터넷) 이용이 가능한 환경에서 네이버 클라우드 플랫폼의 Container Registry를 이용하고자 할 때 사용할 수 있는 엔드포인트
      • 비활성화하여 컨테이너 레지스트리를 공개하지 않도록 설정할 수 있다.
    • 퍼블릭 레지스트리 (비활성화)
      • 누구든지 해당 registry로부터 이미지를 다운로드(Pull)할 수 있도록 설정
      • 기본적으로 네이버 클라우드 플랫폼의 Container Registry는 권한이 있는 사용자만 이미지 다운로드(Pull)를 수행할 수 있다.

github action에서 Container Registry에 접근할 수 있어야하기 때문에 퍼블릭 엔드포인트는 설정하지만, 권한이 있는 사용자만 이미지를 다운로드 할 수 있도록 하기 위해 퍼블릭 레지스트리로는 설정하지 않았다.


이제 생성이 완료되었다면, 해당 레지스트리로 이미지를 build 하고 push하는 과정을 CLI에서 수행해보자.

1. 로그인

해당 repository에 접근하기 위해서는 로그인이 필요하다. (퍼블릭 레지스트리가 아니기 때문)

docker login <CONTAINER_REGISTRY_URL>

NCP container registry에 로그인하기 위해서는 마이페이지에서 인증키 생성이 필요하다.

  • 마이페이지 - 계정관리 - 인증키관리 - 신규 API 인증키 생성 - Access Key ID, Secret Key가 각각 ID, PW로 로그인시 사용된다.

2. 이미지 build → tag → push

docker build -t <CONTAINER_REGISTRY_URL>/<TARGET_IMAGE[:TAG]>
docker push <CONTAINER_REGISTRY_URL>/<TARGET_IMAGE[:TAG]>

tag명은 <레지스트리이름>.kr.ncr.ntruss.com/<이미지이름>:latest 와 같이 registry 주소의 형태이다.

즉, 태그명이 해당 이미지의 registry 주소가 되는 것이다.

GitHub Actions 코드와 함께 살펴보기

CLI에서 빌드 후 push 한 것처럼, 해당 작업을 GitHub Actions이 코드가 특정 브랜치에 push 될 때마다 대신 해주도록 하면 된다.

GitHub Actions와 Container Registry 를 사용한 배포 자동화 흐름(예)

flow example

  • GitHub Actions 이해하기 Understanding GitHub Actions - GitHub Docs

  • workflow 문법 Workflow syntax for GitHub Actions - GitHub Docs

  • (예시) deploy.yml 전체 코드

    name: auto deploy
    
    on:
      push:
        branches:
          - dev
    
    jobs:
      push_to_registry:
        name: Push to ncp container registry
        runs-on: ubuntu-latest
        steps:
          - name: Checkout
            uses: actions/checkout@v3
          - name: Set up Docker Buildx
            uses: docker/setup-buildx-action@v2
          - name: Login to NCP Container Registry
            uses: docker/login-action@v2
            with:
              registry: ${{ secrets.NCP_CONTAINER_REGISTRY }}
              username: ${{ secrets.NCP_ACCESS_KEY }}
              password: ${{ secrets.NCP_SECRET_KEY }}
          - name: build and push
            uses: docker/build-push-action@v3
            with:
              context: .
              file: ./Dockerfile
              push: true
              tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/tag-name:latest
                        cache-from: type=registry,ref=${{ secrets.NCP_CONTAINER_REGISTRY }}/prv-frontend:latest
              cache-to: type=inline
              secrets: |
                GIT_AUTH_TOKEN=${{ secrets.GIT_TOKEN }}
    
      pull_from_registry:
        name: Connect server ssh and pull from container registry
        needs: push_to_registry
        runs-on: ubuntu-latest
        steps:
          - name: connect ssh
            uses: appleboy/ssh-action@master
            with:
              host: ${{ secrets.DEV_HOST }}
              username: ${{ secrets.DEV_USERNAME }}
              password: ${{ secrets.DEV_PASSWORD }}
              port: ${{ secrets.DEV_PORT }}
              script: |
                docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/tag-name
                docker stop $(docker ps -a -q)
                docker rm $(docker ps -a -q)
                docker run -d -p 3000:80 --env-file ${{ secrets.ENV_FILENAME_FRONTEND }} ${{ secrets.NCP_CONTAINER_REGISTRY }}/tag-name
                docker image prune -f
    
  • workflow의 이름은 auto deploy 이다. (GitHub에서 Actions 탭에 들어갔을 때, 좌측에 나오는 이름)

    name: auto deploy
    
  • dev 브랜치로 push 될 때, workflow가 실행된다.

    on:
      push:
        branches:
          - dev
    
  • push_to_registrypull_from_registry job이 각각 존재하고, 각 job은 step들로 이루어져있다. 각 step은 차례대로 진행된다.

    jobs:
      push_to_registry:
            ...
            steps:
                - name: Checkout
            ...
        pull_from_registry:
        name: Connect server ssh and pull from container registry
        needs: push_to_registry
            ...
            steps:
                - name: connect ssh
            ...
    
    • 하지만, job은 순서가 보장되지 않을 수 있다. 어떤 job A가 job B가 실행된 이후에 실행되어야할 경우, job A 내부에 needs: {job B 이름} 형태로 작성한다.
    • 위에서는 이미지가 build 되고, registry로 push 까지 완료된 후에, 배포 서버에서 해당 이미지를 registry로부터 pull 해야하기 때문에 먼저 실행되어야하는 job을 명시해주었다.

Job - push_to_registry 에 대한 설명

  • push_to_registryubuntu 환경에서 실행한다.

    jobs:
      push_to_registry:
        name: Push to ncp container registry
        runs-on: ubuntu-latest
    
  • actions/checkout 은 CI 서버(ubuntu-latest)에 현재 해당 branch에 있는 코드를 가져온다.

    jobs:
        push_to_registry:
            ...
            steps:
          - name: Checkout
            uses: actions/checkout@v3
    
    • @v2 : node v12 사용 / @v3 : node v16 사용
  • buildx-action을 사용하는 이유

                ...
          - name: Set up Docker Buildx
            uses: docker/setup-buildx-action@v2
                ...
    
    • 멀티 아키텍처 플랫폼을(예: amd64, arm64) 지원하기 위해서다.
    • buildx-action을 사용함으로써, 다음 단계에서 build 할 때 buildx 를 사용하게 되고, 다양한 아키텍쳐에서 해당 이미지를 사용할 수 있다.

    멀티 플랫폼 빌드를 위한 Docker Buildx

  • ncp registry에 로그인

                ...
          - name: Login to NCP Container Registry
            uses: docker/login-action@v2
            with:
              registry: ${{ secrets.NCP_CONTAINER_REGISTRY }}
              username: ${{ secrets.NCP_ACCESS_KEY }}
              password: ${{ secrets.NCP_SECRET_KEY }}
            ...
    
  • Dockerfile에 작성된 내용으로 이미지를 빌드하고 registry에 push

                ...
          - name: build and push
            uses: docker/build-push-action@v3
            with:
              context: .
              file: ./Dockerfile
              push: true
              tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/tag-name:latest
                        cache-from: type=registry,ref=${{ secrets.NCP_CONTAINER_REGISTRY }}/prv-frontend:latest
              cache-to: type=inline
              secrets: |
                GIT_AUTH_TOKEN=${{ secrets.GIT_TOKEN }}
            ...
    
    • context : 이미지가 빌드되어야하는 root 위치

    • file : dockerfile이 존재하는 위치

    • tags

      tags: |
          ${{ secrets.NCP_CONTAINER_REGISTRY }}/tag-name:latest
          ${{ secrets.NCP_CONTAINER_REGISTRY }}/tag-name:${{ github.run_number }}
      
      • 위와 같이 여러 태그를 사용할 수 있다.

      • 예시에서는 latest 태그만 사용하고있지만, 이런식으로 여러개의 태그를 사용하여 registry에 push 한다면, 이전에 배포했던 이미지들이 차곡자곡 기록되기 때문에, 배포된 이미지를 과거로 되돌리기 편리하다.

        container registry

      • github.run_number 말고도, git tag 명령을 사용할 수도 있겠다.

        Contexts - GitHub Docs

    • cache-from / cache-to

      • docker image를 빌드할 때마다, npm i 를 실행한다면, 시간이 오래걸릴 것이다. 이를 캐싱할 수 있는 방법이 존재한다.
      • github action을 사용한다면, cache-from, cache-to에 type=gha 를 입력해줄 수 있으나 아직 experimental 단계이다.
      • github repository가 아닌, registry에 cache할 수 있다. buildx에서는 registry 이미지 안에 cache 정보를 저장할 수 있는 inline 옵션을 제공한다.
    • secrets

      • private repository 안에서 action이 실행된다면 github token이 주어져야한다.

Job - pull_from_registry 에 대한 설명

  • 배포 서버에서 이미지 pull 하기

    
    ---
    pull_from_registry:
      name: Connect server ssh and pull from container registry
      needs: push_to_registry
      runs-on: ubuntu-latest
      steps:
        - name: connect ssh
          uses: appleboy/ssh-action@master
          with:
            host: ${{ secrets.DEV_HOST }}
            username: ${{ secrets.DEV_USERNAME }}
            password: ${{ secrets.DEV_PASSWORD }}
            port: ${{ secrets.DEV_PORT }}
            script: |
              docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/tag-name
              docker stop $(docker ps -a -q)
              docker rm $(docker ps -a -q)
              docker run -d -p 3000:80 --env-file ${{ secrets.ENV_FILENAME_FRONTEND }} ${{ secrets.NCP_CONTAINER_REGISTRY }}/tag-name
              docker image prune -f
    

    여기서는 appleboy/ssh-action 을 사용해서 직접 배포 서버에 접속하여 이미지를 가져오는 방식을 선택했다. GitHub actions 하나만으로 배포의 모든 과정을 다루기 위함이 목적이었다.

    배포 서버의 host, username, password, port(default 22)를 전달해주면, script를 실행할 수 있다.

    (별도의 CI 서비스가 있다면 이 부분에서 curl 로 해당 서비스에 요청을 보낼 수도 있을 것이다)

    docker image를 지속적으로 pull 해와서 실행한다면, 서버 안에 사용하지 않는 이미지들이 소중한 용량을 잡아먹을 것이다. 따라서 사용하지 않는 이미지들에 대한 삭제가 필요하다.

workflow가 제대로 실행되는지 로컬에서 확인하기

workflow가 잘 작동하는지 여부를 repository에 push할 때마다 확인하는 것은 생각보다 번거로울 수 있다.

https://github.com/nektos/act

이 때, nektos/act 를 사용하면 로컬 CLI 환경에서 내가 작성한 workflow가 잘 작동하는지 확인해볼 수 있다.

secret이 필요할 때는 별도의 secret 파일을 만들어서 필요한 정보들을 입력해놓은 후,

act --secret-file <secret 파일명>

위와 같이 실행할 수 있다. (해당 secret 파일은 GitHub에 올라가지 않도록 주의)

아무런 옵션을 주지 않으면 push 이벤트에 대해 작동하므로, 다른 이벤트에 대해서는 공식 Repository의 리드미를 참고바란다.

docker-compose

(여러개의 이미지를 한번에 빌드하고 싶을 때)

예를들어, 백엔드 서버가 Nest.JS를 사용하고, Nest는 Redis와 MySQL에 연결하여 사용한다고 해보자.

Docker를 사용하지 않는다면 직접 Node 설치, Redis 설치, MySQL 설치를 각각 진행하고, npm build 를 실행하여 빌드된 main.js 파일을 실행시킬 것이다.

docker compose를 쓰지않으면 Dockerfile로 Node 앱의 이미지를 만들고, Redis, MySQL 이미지를 각각 실행시키면서, docker network에 연결시켜줘야한다.

예) MySQL

docker run -it --network some-network --rm mysql mysql -hsome-mysql -uexample-user -p

위와 같은식으로 각각의 이미지들을 실행시켜서 서비스를 만들 수 있지만, 한번에 관리되기보다는, 서비스의 개수에 따라 더욱 복잡해진다는 느낌이 든다.

이를 해결하기위해 docker compose를 사용할 수 있다.

docker-compose.yml 파일을 별도로 생성한 후, 내용으로는 어떤 이미지를 사용하는지, 각 이미지들에게 어떤 옵션을 주어야하는지를 하나의 파일안에 작성해놓을 수 있다. 그리고 서비스들을 docker compose up 로 한번에 실행할 수 있다.

# docker-compose.yml (before)
version: "3"

services:
  node_app:
        build: .
    links:
      - redis
      - mongo
        network:
            - new
    ports:
      - 3000:3000

  mongo:
    image: mongo:6.0.2
    ports:
      - 27017:27017
        network:
            - new

  redis:
    image: redis:7.0.5
    ports:
      - 6379:6379
    command: redis-server --save 60 1000 --loglevel notice --requirepass ${REDIS_PASSWORD}
        network:
            - new

networks:
    new:

위의 예시를 살펴보면, node_app은 현재 위치(.)에 있는 Dockerfile로 빌드한 이미지를 사용하고, mongoredis 는 docker hub에 올라가있는 이미지를 사용하게 된다.

즉, node application에서 node-redis를 사용하여 연결한다면, host에는 redis 를 써줘야한다.

  • link로 연결한다는 의미는, 해당 서비스 이름을 host로 하여, 그 서비스로 접근할 수 있다는 것이다.

    • node_app에서 link된 redis를 예를들자면, node-redis로 redis와 연결한다고 했을 때, config에서 전달받아야하는 hostname은 redis 다.
  • link는 legacy 옵션이며, link와 network를 동시에 사용하려면, link로 연결된 서비스들은 한개 이상의 공통된 네트워크를 갖고있어야한다.

    Compose file version 3 reference

  • docker document에서는 network만 사용하는 것을 권장하고 있다.

  • 기본적으로 docker-compose 내에 있는 서비스들은 default 네트워크로 연결되어있다.

따라서 위의 예시에서 link와 network를 제거해도 서비스 간 연결에 문제가 없다.

# docker-compose.yml (after)
version: "3"

services:
  node_app:
        build: .
    ports:
      - 3000:3000

  mongo:
    image: mongo:6.0.2
    ports:
      - 27017:27017

  redis:
    image: redis:7.0.5
    ports:
      - 6379:6379
    command: redis-server --save 60 1000 --loglevel notice --requirepass ${REDIS_PASSWORD}

이제 서버에서는 node_app의 Dockerfile과 docker-compose.yml 파일이 있는 위치에서 docker compose up -d 로 실행하면 된다! (물론 docker가 설치되어있어야한다.)

docker container ls 를 입력해보면, node_app, mongo, redis가 실행되고 있을 것이다.

추가 참고 자료