[CI/CD] Github Actions & Docker & Docker Compose & NCP registry

suhwani·2024년 3월 20일
0
post-thumbnail

‼️ 글 읽기 전 주의사항 ‼️

✅ 너무 힘들었고, 시행착오도 많았어요. 내용이 많이 깁니다. 결과 코드만 보고 싶으신 분들은 바로 아래로 가시면 됩니다! Docker 만 사용하는 경우, Docker compose 를 사용하는 경우 모두 다뤘습니다!!

✅ CI/CD 환경


  • NCP Server: centOS
  • NCP container registry 사용
    • Docker Hub 대신 사용
  • Docker + Docker Compose 사용
  • Github Actions 로 관리

😡 마주한 역경 순서대로 글을 작성하였습니다. 도움이 되셨으면 합니다 ㅠㅠ 저와 같은 고통을 겪지마세요ㅠㅠ

✅ 전체 시퀀스 설명


Docker 만 사용하는 경우 + Docker compose 까지 사용하는 경우를 모두 다뤘습니다.
필요하신 부분만 보셔도 괜찮습니다!! 둘 다 사용할 수 있어야 할 것 같아서 같이 진행했습니다.

시퀀스

  • 특정 브랜치(ex. main, deploy…)에 Push 발생 시, Github actions 가 작동한다.
  • 원격 서버에서 Docker image 를 Build 한다
  • NCP Container registry 에 Docker image 를 Push 한다.
  • NCP Server 에 SSH 접속한다.
  • Server 내부에서 Docker image 를 Pull 한다.
  • 둘 중 하나로 나눠진다.
    • 1️⃣ docker-compose 실행한다.
    • 2️⃣ docker run 을 실행한다.

✅ [고난 1] .env 파일을 어떻게 서버에 올려야할까


😡 문제의 발단

  • 기본적으로 .env 파일에는 보안이 중요한, 노출이 되면 안되는 정보(ex.DataBaseUrl 등)이 포함된다.
  • 그렇기 때문에 gitignore 파일을 이용해서, GitHub 에 올라가지 않도록 보관한다.
  • 그렇지만 프로그램 실행에 필수적인 정보를 담은 파일이다.

❓자. Github 에 올리지 않고, Local → Github Actions → cloud Server 옮길 수 있는 방법은 뭘까?

❌ 해결법 1. 수동으로 한다

  • Server 내에 직접 접속해서 .env 파일을 배포 or 내용 변경 시 수동으로 수정한다.

❓ 그럴거면 Github actions 를 왜 써. 그냥 코드도 clone 으로 받지. 탈락!!!

❌ 해결법 2. Dockerfile 이용하기

  • Dockerfile 에 입력을 할 수 있다.
FROM your_base_image

WORKDIR /usr/src/app

COPY .env ./

ENV DATABASE_URL=<your_database_url>
ENV JWT_SECRET=<your_jwt_secret>

❓ Dockerfile 도 Github 에 올려야 Github Actions 로 실행을 시키는 거잖아. env 에 있는 정보가 노출될까봐 안올렸는데, env 정보를 Dockerfile 에 옮기고, 이걸 올리면 env 를 올리는 것과 같은거잖아.

⭕ 해결법 3. env 내용을 Github Actions Secret 으로 등록하자

  • Github Actions 에서 사용할 변수들을 Secret 으로 등록할 수 있다.
  • Github Repo 상단에 Settings 에 들어가면, 왼쪽 메뉴에서 Security → Secrets and variables 가 있다.
  • 여기에 DB url, jwt password …. 등을 설정해놓고 Github Actions 작동할 때 해당 변수들을 받아온다.
# SSH 내부에서 Github Actions Secrets 을 받아서, .env 파일에 변수를 직접 넣는다.
if [ ! -f .env ]; then
  echo "DOCKER_IMAGE_TAG=${{ secrets.DOCKER_IMAGE_TAG }}" > .env
  echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env
  echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
fi
  • 이후, 생성한 env 파일을 Docker Container 가 사용할 수 있도록 볼륨을 마운트해줘야 합니다~
    마지막에 전체 코드를 드릴게요 보시면 이해가 될 겁니다
# 아래처럼 Server 내 .env 경로를 찾아서 Docker 가 실행되는 폴더 내부에 마운트 해야합니다.
docker run -d -p 443:443 -v /root/app/.env:/usr/src/app/.env:ro -v /etc/letsencrypt:/etc/letsencrypt:ro
  • 만약 진짜 다 해줬는데, 왜 안되는지 모르겠다!
    내가 docker image 가 실행되는 컨테이너 안으로 들어가봐야겠다!! 파일이 뭐 잘 있는지!! 봐야겠다!!!
# 볼륨을 생성해서 마운트된 파일도 볼 수 있습니다~
docker run -it -v /host/path:/container/path <이미지 이름 또는 ID> /bin/bash

✅ [고난 2] docker-compose.yml 파일을 어떻게 서버에 올려야할까


😡 문제의 발단

  • .env 파일을 올리는 법을 해결했더니, docker-compose.yml 은 어떻게 올려야하지?
    💬 docker-compose.yml 에는 중요한 정보를 올리지 않는 게 좋습니다.

  • .env → Github Actions Secrets 사용

    • Github 에 올리지 못한다.
    • Docker image 에 포함되지 않는다.
    • 올려야 할 정보가 몇 개 없다.
  • docker-compose.yml

    • Github 에 올릴 수 있다.
    • Docker image 에 포함된다.
    • 올려야 할 정보가 많다.
  • docker-compose.yml 은 Github 에 올려도 된다는 점을 활용하자!!

❌ 해결법 1. Github artifact 를 이용해보자

  • Github Actions 에서 Artifact 를 이용하면, 원격서버에 파일이나 출력물 등등을 업로드 할 수 있다.
    살짝 음… 카카오톡 느낌? 올려놓고 다운받을 수 있는 느낌?
  • Github → Github Actions Artifact → 원격 서버에 파일을 저장한다. → NCP Server 로 가져온다.

❓ NCP Server 에서 원격서버로부터 가져오려고 할 때, Artifact 의 URL 로 가져오는데,
사실 이 과정은 잘 모르기도 하고, 어려워서 깊이는 안 찾아봤지만, 딱히 자료가 없어서…. Pass!!

⭕ 해결법 2. Github 에서 다운로드를 받자!

  • docker-compose.yml 은 github 에 올라가니까 NCP Server 에서 다운 받을 수 있다!!
  • 주의점!! 클라우드 서버 내에 docker 는 많이 설치하지만,
    docker-compose 를 설치하는 일은 적어서 따로 다운로드 받는 코드를 진행해야한다.
# 만약 docker-compose.yml 이 없다면 다운을 받고 있다면 Pass!
if [ ! -f docker-compose.yml ]; then
  curl -o docker-compose.yml https://raw.githubusercontent.com/zzub-zzub-bak-sa/backend/deploy/docker-compose.yml
fi
# docker compose 를 설치하는 코드, 이후에 docker-compose run 실행 가능!
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

✅ [고난 3] NCP registry 로그인을 서버 스크립트로 어떻게 할까


😡 문제의 발단 ( NCP Server 를 “서버” 로 칭하겠습니다! )

  • 내 목표는 서버 내에서 실행을 시키는건데, 그러려면 서버에 SSH 접속 후 script 로 실행을 시켜야한다.
  • NCP registry 로그인을 아래와 같이 진행하면, 다시 SSH 접속을 해야하는데,
    이 때, NCP registry 에 접근하면 Access denied 가 발생한다!!
  • 그렇다고, NCP registry 로그인 이후 바로 image 를 Pull 받으면,
    그 위치는 Github Actions Runner 내부에 다운받아지기 때문에, 내 서버에 다운 받아지는 게 아니다!!
# 로그인은 가능하지만, 이후 SSH 접속을 하면 다시 권한이 없어진다. 
- 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 }}

⭕ 해결법 1. SSH 내부 Script 를 통해서 registry 로그인을 진행하자!

  • Docker CLI 를 통해서 로그인을 할 경우, UserName, Password → AccessKey, SecretKey 를 이용!!
  • 로그인 이후 pull 을 받아옵니다!!
# 기존 컨테이너, image 를 멈추거나 삭제 등 여러 과정이 필요합니다. 마지막 부분에 코드 드릴게요
echo ${{ secrets.NCP_SECRET_KEY }} | docker login -u ${{ secrets.NCP_ACCESS_KEY }} ${{ secrets.NCP_CONTAINER_REGISTRY }} --password-stdin
docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }}:latest

✅ [고난 4] 다 해줬어! .env 도 잘 받아왔어 근데 안돼!!!


😡 문제의 발단

  • 모든 문제를 해결해주고, 난 다 해줬는데 계속 .env 를 코드단에서 못 읽어와서 실행이 안된다.
  • 하아….

⭕ 해결법 1. main 파일에서 env 파일 읽어오는 코드를 맨 위로!

  • 프로젝트 팀원 중 프론트 담당하신 분이 백엔드 repo 를 클론한 후 실행을 하는데,
    계속 .env 를 못 읽어온다고 에러가 났던 적이 있다.
  • 당시 해결방법으로 dotenv.config(); 를 main.ts 에서 함수 맨 위로 올려서 실행을 했더니
    제대로 작동했었다. 이유는 아무도 모르지만, 하여튼 그랬었다…
// 이런 식으로 맨 위로 올려야함. 
async function bootstrap() {
  dotenv.config();

✅ [고난 5] 실행도 다 됐는데, https 접속이 안되네…


😡 문제의 발단

  • 내가 main 에서 port 80, 443, 8000을 열었다.
  • 근데 왜 https 로 접속이 안되는걸까?? 같은 443인데…

⭕ 해결법 1. Docker 구조를 이해하자.

  • Docker 는 가상의 컨테이너를 만들어주는 역할이다.
  • image 는 컨테이너를 만드는 재료 정도로 생각해주면 된다. 또한 코드의 스냅샷으로 이해해도 괜찮다.
  • Docker 이해가 안되는 분들은 아래의 과정을 천천히 읽어보시길 바란다.
    • 내가 작성한 Code 를 Docker Build 를 하는 시점에 도장을 쾅하고 찍고, 사진으로 기록한다.
    • 이 때 사진을 Docker Image 라고 생각하면 된다. 그러면 사진(Docker Image) 를 들고 다니면서
      ”나 예전에 이랬다~~” 이러면서 아무한테나 보여줄 수 있다.
    • “나 예전에는 말이지~~ 내가 그때는 이랬고~~” 라고 얘기를 시작하는 것을
      Docker Container 라고 생각하면 된다. 사진(Image)을 보면서 옛 이야기(Container)를 하는 것!!
    • 근데 얘기는 듣는 사람이 있어야 하는 거잖아. 나만 떠든다고 얘기가 하니잖아.
      이 때 듣는 사람이 Port 이다. 컨테이너만 띄우고 Port 를 열지 않는다면, 혼자 떠드는 것!!
# 아래처럼 -p 옵션을 통해 Port 를 열어줘야한다.
docker run -d -p 443:443

✅ 해결한 소감


“너가 이기나 내가 이기나 한번해보자” 는 심정으로 계속 수정 → 실행 → 수정 계속…하아..힘들었다
매일 속으로만 생각하고, 너무 바빠서 다음에 다음에 하면서 미뤘는데 오늘 끝장을 봤다.
너무 힘들었고, NCP registry 를 쓰면서 복잡해지기도 하고, Docker-compose 랑 Docker 만 쓰는
두가지를 모두 해결해서 그런지, 시간이 너무 오래 걸렸지만 매우 좋았다. 다 끝내니까 내가 이긴 기분이다!!!

😂 시행착오 흔적…ㅜㅜ

하루만에 40커밋, 전에 시도까지 합쳐서 총 73번 도전한 workflow


✅ 전체 코드


Dockerfile

# 기본 이미지 설정 (Node.js)
FROM node:20

# 앱 디렉토리 생성 및 설정
WORKDIR /usr/src/app

# 패키지 파일 복사 및 설치
COPY package*.json ./
RUN npm install

# 소스 코드 복사
COPY . .

# 앱 빌드
RUN npm install --production=false
RUN npx prisma generate --schema=src/db/prisma/schema.prisma
RUN npm run build \
    rm -rf node_modules \
    npm install --production=true

# 컨테이너 실행 시 실행될 명령어
CMD ["npm", "run", "start:prod"]

docker-compose.yml

version: '3'

services:
  app:
    image: registry URL/Repo Name:Image Tag
    container_name: 원하는 컨테이너 이름 지정
    ports:
      - "443:443"
    env_file:
      - .env
    volumes:
      - /root/app/.env:/usr/src/app/.env
      - /etc/letsencrypt:/etc/letsencrypt:ro

Github actions 의 useDocker.yml 파일 → Docker 만 사용하는 경우

name: Build and Push Docker Image

on:
  push:
    branches:
      - deploy

jobs:
  build_and_push:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

      - name: Cache node modules
        uses: actions/cache@v2
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - 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: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: Build and Push Docker Image
        run: |
          docker build -t ${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }}:${{ secrets.DOCKER_IMAGE_TAG }} .
          docker push ${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }}:${{ secrets.DOCKER_IMAGE_TAG }}

  pull_from_registry:
    runs-on: ubuntu-latest

    needs: build_and_push

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

      - 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 : | 
            rm -rf app
            if [ ! -d "app" ]; then
              mkdir app
            fi

            cd app || exit

            if [ ! -f .env ]; then
              echo "DOCKER_IMAGE_TAG=${{ secrets.DOCKER_IMAGE_TAG }}" > .env
              echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env
              echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
            fi

            cd 

            echo ${{ secrets.NCP_SECRET_KEY }} | docker login -u ${{ secrets.NCP_ACCESS_KEY }} ${{ secrets.NCP_CONTAINER_REGISTRY }} --password-stdin
            
            docker stop $(docker ps -q --filter ancestor=${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }})

            docker rm $(docker ps -aq --filter ancestor=${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }})
            docker rmi ${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }}:latest
            
            docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }}:latest
            docker run -d -p 443:443 -v /root/app/.env:/usr/src/app/.env:ro -v /etc/letsencrypt:/etc/letsencrypt:ro ${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }}:latest
            docker image prune -f

Github actions 의 useDockerCompose.yml 파일 → Docker Compose 사용

name: Build and Push Docker Image

on:
  push:
    branches:
      - deploy

jobs:
  build_and_push:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@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: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: Build and Push Docker Image
        run: |
          docker build -t ${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }}:${{ secrets.DOCKER_IMAGE_TAG }} .
          docker push ${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }}:${{ secrets.DOCKER_IMAGE_TAG }}

  pull_from_registry:
    runs-on: ubuntu-latest

    needs: build_and_push

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
      
      - 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 : | 
            if [ ! -d "app" ]; then
              mkdir app
            fi
            cd app || exit

            if [ ! -f .env ]; then
              echo "DOCKER_IMAGE_TAG=${{ secrets.DOCKER_IMAGE_TAG }}" > .env
              echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env
              echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
            fi

            if [ ! -f docker-compose.yml ]; then
              curl -o docker-compose.yml https://raw.githubusercontent.com//Github_Repository_Name/Branch_Name/docker-compose.yml
            fi

            echo ${{ secrets.NCP_SECRET_KEY }} | docker login -u ${{ secrets.NCP_ACCESS_KEY }} ${{ secrets.NCP_CONTAINER_REGISTRY }} --password-stdin
            
            sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
            sudo chmod +x /usr/local/bin/docker-compose
            sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

            docker stop $(docker ps -q --filter ancestor=${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }})

            docker rm $(docker ps -aq --filter ancestor=${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }})
            docker rmi ${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }}:latest

            docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/${{ secrets.NCP_CONTAINER_REPO }}:latest
            docker-compose up -d
            docker image prune -f
profile
Backend-Developer

0개의 댓글