๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Web Development/Back-end

Nestjs & Docker & Github Action ํ™œ์šฉ ์ž๋™ ๋ฐฐํฌ ๊ตฌ์ถ•ํ•˜๊ธฐ

๐Ÿ“Œ CICD ๋ž€ ?

  • CI (Continuous Integration) : ์ง€์†์  ํ†ตํ•ฉ. ์ƒ์„ฑ ๋ฐ ์ˆ˜์ • ์ฝ”๋“œ๋“ค์ด ์ž๋™์œผ๋กœ Test → Build ํ•˜๋Š” ๊ณผ์ •์„ ์ž๋™ํ™”
  • CD (Continuous Deploy) : ์ง€์†์  ๋ฐฐํฌ. CI ์ดํ›„ ๋ฐฐํฌ ํ™˜๊ฒฝ๊นŒ์ง€ ์ „๋‹ฌํ•ด ์ž๋™ ๋ฐฐํฌํ•˜๋Š” ๊ณผ์ •์„ ์ž๋™ํ™”
  • ๋ชฉ์  : Testํ•˜์—ฌ ์ •์ƒ์ ์ธ ์ฝ”๋“œ๊ฐ€ Deploy ๋˜๋Š” ๊ฒƒ์ธ์ง€ ํ™•์ธํ•˜๋Š” ์ ˆ์ฐจ๋ฅผ ๊ฑฐ์น˜๊ณ  → Buildํ•˜๊ณ  → ghcr ์— Deploy ๋œ ์ด๋ฏธ์ง€๋ฅผ Github Action Runner๋ฅผ ์ ์šฉํ•ด ์ž๋™ ๋ฐฐํฌ๋˜๋„๋ก ๊ตฌ์„ฑํ•˜์‹œ์˜ค.

๐Ÿ“Œ Dockerfile ๋งŒ๋“ค๊ณ  ํ…Œ์ŠคํŠธํ•ด๋ณด๊ธฐ

Dockerfile ์ž‘์„ฑ

# Match with my local Node Version
FROM node:16-alpine

# RUN mkdir -p /app
WORKDIR /app

# My . Dir -> /app Directory
ADD . /app/

# Dependency Install
RUN npm install

# Build
RUN npm run build

# PORT (8080) Expose
EXPOSE 8080

# START
ENTRYPOINT npm run start:prod

Dockerignore ๋งŒ๋“ค๊ธฐ

node_modules/
dist/

 

Build Docker Image๋ฅผ ๋งŒ๋“ค๊ธฐ

  • -t ์ด๋ฏธ์ง€ ํƒœ๊ทธ๋ช… ์˜ต์…˜
  • ์ผ๋ฐ˜์ ์œผ๋กœ -t ์ด๋ฏธ์ง€๋ช…์นญ:๋ฒ„์ „ ์œผ๋กœ ๋ช…์‹œ ๋ฒ„์ „ ๋ฏธ์ง€์ •์‹œ latest๋กœ ์ž๋™ ์ง€์ •ํ•จ.
docker build **-t my-nest** .

 

์ด๋ฏธ์ง€ ์‹คํ–‰

  • -d ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์‹คํ–‰
  • -p ํฌํŠธํฌ์›Œ๋”ฉ ํฌํŠธ ์ง€์ • (๋กœ์ปฌํฌํŠธ : ์ปจํ…Œ์ด๋„ˆ ํฌํŠธ)
  • -v ์‚ฌ์šฉ ๋ณผ๋ฅจ (์ €์žฅ์†Œ)์„ค์ •
  • --network ์‚ฌ์šฉ ๋„คํŠธ์›Œํฌ ์„ค์ • (host, bridge)
  • ๋งจ๋’ค์— ์ด๋ฏธ์ง€ ํƒœ๊ทธ๋ช… ์ž‘์„ฑํ•ด ์‹คํ–‰
docker run --name my-nest-test -d -p 5000:8080 --network host my-nest

 

 

๐Ÿ“Œ Github Action

Github Token ๋ฐœํ–‰

  • ๊ฐœ์ธ Settings์—์„œ Token ๋ฐœํ–‰ ์ง„ํ–‰
  • ๋‹จ, workflow write, delete๊ถŒํ•œ์„ ์ฃผ์–ด์•ผํ•œ๋‹ค.

Actions Secret ์„ค์ • (Organization)

  • Github Repository → Settings ์—์„œ ์‚ฌ์šฉํ•  ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋“ฑ๋ก ์ง„ํ–‰
  • Github Action Workflow์—์„œ ์‚ฌ์šฉํ•  ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋“ฑ๋ก

๋„์ปค์ด๋ฏธ์ง€๋ฅผ ghcr์— ์˜ฌ๋ฆฌ๊ธฐ

  • ์›ํ•˜๋Š” ์„œ๋ฒ„ ์ด๋ฏธ์ง€๋ฅผ ์ •์˜ํ•ด์„œ ghcr์— ์˜ฌ๋ฆฐ๋‹ค.
#ghcr Login & Password ์ž…๋ ฅํ•˜๋ผ๋Š” ํ‘œ์‹œ๊ฐ€ ๋‚˜์˜ค๋Š”๋ฐ, ํ† ํฐ ์ž…๋ ฅ
**docker login** ghcr.io -u <USERNAME>

# ์ด๋ฏธ์ง€ ๋นŒ๋“œ
**docker build** -t ghcr.io/<๊นƒํ—ˆ๋ธŒ์•„์ด๋””>/<์ด๋ฏธ์ง€๋ช…:๋ฒ„์ „> .

# ์ด๋ฏธ์ง€ ํ‘ธ์‹œ
**docker push** ghcr.io/blockmonkey1992/test:latest

 

Github Action ์ ์šฉ Workflow ์ž‘์„ฑ

  • ์•„๋ž˜ ํด๋”์— ์ž‘์„ฑํ•˜๋ฉด ๊นƒํ—ˆ๋ธŒ์—์„œ Action ์ˆ˜ํ–‰์„ ์ž๋™์ ์œผ๋กœ ์ฐพ์•„์„œ ์‹คํ–‰ํ•œ๋‹ค.
  • jobs ์— ๊ฐ ๋‹จ์œ„๋Š” ๋ณ‘๋ ฌ ์‹คํ–‰์„ ํ•˜๋‚˜, needs: build์™€ ๊ฐ™์ด ํ‘œ์‹œํ•ด์„œ ๋™๊ธฐ์  ์‹คํ–‰์ด ๊ฐ€๋Šฅ
# .github/workflows/cicd.yml
name: NEST-CICD

# Config : Event 
on:
  push:
    branches: ["main"]

# Env
env:
  DOCKER_IMAGE: ghcr.io/blockchain-lighthouse/lighthouse-server
  DOCKER_CONTAINER: nest-server

jobs:
  # Config Test Before Build
  test:
    runs-on: ubuntu-20.04
    steps:
    - name: Checkout Source Code
      uses: actions/checkout@v3

    - name: Nodejs 16
      uses: actions/setup-node@v3
      with:
        node-version: 16.19.0
        cache: 'npm'

    - name: Setting .env
      run: |
        echo "SERVER_PORT=${{ secrets.SERVER_PORT }}" >> .env
        echo "DATABASE_DB=${{ secrets.DATABASE_DB }}" >> .env
        echo "DATABASE_HOST=${{ secrets.DATABASE_HOST }}" >> .env
        echo "DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}" >> .env
        echo "DATABASE_PORT=${{ secrets.DATABASE_PORT }}" >> .env
        echo "DATABASE_USER=${{ secrets.DATABASE_USER }}" >> .env
        echo "REDIS_IP=${{ secrets.REDIS_IP }}" >> .env
        echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env
        echo "REDIS_PORT=${{ secrets.REDIS_PORT }}" >> .env
        echo "SWAGGER_ID=${{ secrets.SWAGGER_ID }}" >> .env
        echo "SWAGGER_PW=${{ secrets.SWAGGER_PW }}" >> .env
        cat .env

    - run: npm install
    - run: npm run test

  # Config : Build & Push to GHCR Docker Image 
  build:
    needs: test
    runs-on: ubuntu-20.04
    steps:
    - name: Checkout Source Code
      uses: actions/checkout@v3

    - name: Set up Docker build
      id: buildx
      uses: docker/setup-buildx-action@v2
   
    - name: Login to ghcr
      uses: docker/login-action@v2
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.BLOCKMONKEY_TOKEN }}

    - name: Build and Push
      id: docker_build
      uses: docker/build-push-action@v3
      with:
        push: true
        tags: ${{ env.DOCKER_IMAGE }}:latest

  # Config : Deploy To Cloud Server
  deploy:
    needs: build
    runs-on: [self-hosted, blk]
    steps:
    - name: Login to ghcr
      uses: docker/login-action@v2
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.BLOCKMONKEY_TOKEN }}
    # Docker Start! (Del Current Container & Image)
    - name: Running Docker
      run: |
        echo "SERVER_PORT=${{ secrets.SERVER_PORT }}" >> .env
        echo "DATABASE_DB=${{ secrets.DATABASE_DB }}" >> .env
        echo "DATABASE_HOST=${{ secrets.DATABASE_HOST }}" >> .env
        echo "DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}" >> .env
        echo "DATABASE_PORT=${{ secrets.DATABASE_PORT }}" >> .env
        echo "DATABASE_USER=${{ secrets.DATABASE_USER }}" >> .env
        echo "REDIS_IP=${{ secrets.REDIS_IP }}" >> .env
        echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env
        echo "REDIS_PORT=${{ secrets.REDIS_PORT }}" >> .env
        echo "SWAGGER_ID=${{ secrets.SWAGGER_ID }}" >> .env
        echo "SWAGGER_PW=${{ secrets.SWAGGER_PW }}" >> .env
        cat .env
        docker stop ${{ env.DOCKER_CONTAINER }} && docker rm ${{ env.DOCKER_CONTAINER }} && docker rmi ${{ env.DOCKER_IMAGE }}:latest
        docker run --env-file ./.env -d -p 80:8080 --name ${{ env.DOCKER_CONTAINER }} --restart always ${{ env.DOCKER_IMAGE }}:latest

 

ํ˜ธ์ŠคํŒ… ์„œ๋ฒ„ ์„ค์ • ์ง„ํ–‰

  • Docker ์„ค์น˜
$ sudo apt update

$ sudo apt-get install -y ca-certificates \\ 
    curl \\
    software-properties-common \\
    apt-transport-https \\
    gnupg \\
    lsb-release

$ sudo mkdir -p /etc/apt/keyrings

$ curl -fsSL <https://download.docker.com/linux/ubuntu/gpg> | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

$ echo \\
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] <https://download.docker.com/linux/ubuntu> \\
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

$ sudo apt install docker-ce docker-ce-cli containerd.io
$ docker version
  • Github Action Runner ์…‹ํŒ…
    • Repository์—์„œ → Settings → Actions → Runner์—์„œ ๋Ÿฌ๋„ˆ ์ถ”๊ฐ€ํ•˜๊ธฐ ํ•˜๋ฉด ๋œ๋‹ค.
    • ํ˜ธ์ŠคํŒ… ์„œ๋ฒ„ ํ„ฐ๋ฏธ๋„์— ์ ‘์†ํ•˜์—ฌ ์•„๋ž˜ ๋‚ด์šฉ์„ ์ง„ํ–‰ํ•˜๋ฉด ๋œ๋‹ค.
    • Network ๋ณด์•ˆ ๊ทธ๋ฃน ์„ค์ • ํ›„ ์ ‘์†ํ•ด๋ณด๋ฉด ์ •์ƒ์ ์œผ๋กœ ๋‚˜์˜ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
     
// Must not run with sudo ์—๋Ÿฌ ๋งŒ๋‚˜๋ฉด ? - ์•„๋ž˜ ๋ช…๋ น์–ด ์น˜๊ณ , ๋‹ค์‹œ ํ•ด๋ณด์‹œ์˜ค. 
export RUNNER_ALLOW_RUNASROOT="1"

Action Runner ์‹คํ–‰ ๋ช…๋ น์–ด
์„ฑ๊ณตํ™”๋ฉด

 

 

๐Ÿ“Œ Nginx ์„ค์ • ๋ฐ ์ ์šฉ

  • Nginx ๋ž€?
    • Nginx๋Š” ๊ฒฝ๋Ÿ‰ ์›น ์„œ๋ฒ„.ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ์š”์ฒญ์„ ๋ฐ›์•˜์„ ๋•Œ ์š”์ฒญ์— ๋งž๋Š” ์ •์  ํŒŒ์ผ์„ ์‘๋‹ตํ•ด์ฃผ๋Š” HTTP Web Server๋กœ ํ™œ์šฉ๋˜๊ธฐ๋„ ํ•˜๊ณ , Reverse Proxy Server๋ฅผ ํ™œ์šฉํ•˜์—ฌ WAS ์„œ๋ฒ„์˜ ๋ถ€ํ•˜๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ๋Š” ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ๋กœ ํ™œ์šฉ๋˜๊ธฐ๋„ ํ•จ.
  • Nginx Docker File ์ž‘์„ฑ
FROM nginx:latest

#RUN rm /etc/nginx/conf.d/default.conf
RUN rm /etc/nginx/conf.d/default.conf

#COPY ../../ngingx.conf to /etc/nginx/conf.d
COPY ../../nginx.conf /etc/nginx/conf.d

#EXPOSE port 80
EXPOSE 80

#nginx start
CMD ["nginx", "-g", "daemon off;"]
  • nginx.conf ์ž‘์„ฑ
upstream nest { # nest ๋ผ๋Š” upstream ์„œ๋ฒ„๋ฅผ ์ •์˜
		#์ผ๋ฐ˜์ ์ธ ํ”„๋ก์‹œ ๊ตฌ์กฐ์—์„œ ์š”์ฒญ์„ ๋ฐ›๋Š” ์ชฝ์„ upstream, ์‘๋‹ต์„ ๋ฐ›๋Š” ์ชฝ์„ downstream์ด๋ผ๊ณ  ํ•œ๋‹ค.
		server 172.17.0.1:8080; # 8080์— ์—ฐ๊ฒฐ (๋„์ปคํ•œ์ •)
}

server {
    listen 8080; # 8080๋ฒˆ ํฌํŠธ๋ฅผ ์—ด์–ด์ค€๋‹ค. ๋“ค์–ด์˜ค๋ฉด -> ์•„๋ž˜ ProxyPass๋กœ ์ „๋‹ฌ
    server_name  server.bclh.link; # ๋„๋ฉ”์ธ
    location / { # "/" ๋„๋ฉ”์ธ์— ๋„๋‹ฌํ•˜๋ฉด ์•„๋ž˜์˜ proxy๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค.
			return 301 https://$host$request_uri;
    }
}
#172.17.0.1 < ์ด ์ฃผ์†Œ๋Š” docker0 interface ์˜ ์ž๋™ํ• ๋‹น๋œ ip ์ฃผ์†Œ
#์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์™ธ๋ถ€๋กœ ํ†ต์‹ ํ•  ๋•Œ๋Š” ๋ฌด์กฐ๊ฑด docker0interface๋ฅผ ์ง€๋‚˜์•ผ ํ•œ๋‹ค.
#์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์—ฌ๋Ÿฌ๊ฐœ์ผ ๊ฒฝ์šฐ ๋ชจ๋“  container๋Š” 172.17.XX.YY ๋Œ€์—ญ์—์„œ IP๋ฅผ ํ•˜๋‚˜์”ฉ ํ• ๋‹น๋ฐ›๊ฒŒ ๋œ๋‹ค.
  • Github Workflow ํŒŒ์ผ ์—…๋ฐ์ดํŠธ
    • Nginx ์„ค์ • ๋‚ด์šฉ์€ ์ž์ฃผ ๋ณ€๋™์‚ฌํ•ญ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ test๋‚˜ build ๋‹จ๊ณ„์—์„œ ํ•„์š”์—†๋Š” ๋ถ€๋ถ„์ด๋ฏ€๋กœ deploy ๋ถ€๋ถ„์—๋งŒ ์‹คํ–‰ํ•  ๋•Œ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.
env:
  DOCKER_NEST_IMAGE: ghcr.io/blockchain-lighthouse/lighthouse-server
  DOCKER_NEST_CONTAINER: nest-server
  DOCKER_NGINX_IMAGE: ghcr.io/blockchain-lighthouse/lighthouse-nginx
  DOCKER_NGINX_CONTAINER: nginx-server

# Config : Deploy To My Cloud Server
deploy:
  needs: build
  runs-on: [self-hosted, blk]
  steps:
  - name: Login to ghcr
    uses: docker/login-action@v2
    with:
      registry: ghcr.io
      username: ${{ github.actor }}
      password: ${{ secrets.BLOCKMONKEY_TOKEN }}
  # Docker Start! (Del Current Container & Image)
  - name: Running Nginx
    run: |
			docker stop ${{ env.DOCKER_NGINX_CONTAINER }} && docker rm ${{ env.DOCKER_NGINX_CONTAINER }} && docker rmi ${{ env.DOCKER_NGINX_IMAGE }}:latest
      docker run -d -p 80:80 --name ${{ env.DOCKER_NGINX_CONTAINER }} --restart always ${{ env.DOCKER_NGINX_IMAGE }}:latest

๐Ÿ“Œ Https ์ ์šฉํ•˜๊ธฐ

ํ˜ธ์ŠคํŠธ ์„œ๋ฒ„์— ์ง„์ž… → Certbot ์„ค์น˜ → ์ธ์ฆ์„œ ๋ฐœ๊ธ‰

# Certbot Install (๋‹จ, 80๋ฒˆ ํฌํŠธ๊ฐ€ ๋™์ž‘์ค‘์ด๋ฉด ์‹คํ–‰๋˜์ง€ ์•Š์œผ๋‹ˆ, Nginx Container๋ฅผ ์ž ๊น ๊บผ์คฌ๋‹ค)
sudo snap install --classic certbot

# ์ธ์ฆ์„œ ๋ฐœ๊ธ‰
certbot certonly --standalone

# ์„ฑ๊ณต ๋ฉ”์„ธ์ง€๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ถœ๋ ฅ๋œ๋‹ค.
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/server.bclh.link/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/server.bclh.link/privkey.pem
This certificate expires on 2023-06-20.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

# ๋ฐœ๊ธ‰ ๋œ ์ธ์ฆ์„œ ํ™•์ธ
cd etc/letsencrypt/live/server.bclh.link

## ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŒŒ์ผ๋“ค์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์ž์„ธํžˆ ๋ณด๋ฉด ์—ฌ๊ธฐ ์›๋ณธํŒŒ์ผ์ด ์žˆ๊ณ , archive์— ์‚ฌ๋ณธ ํŒŒ์ผ์ด ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ.
lrwxrwxrwx 1 root root   40 Mar 22 08:04 cert.pem -> ../../archive/server.bclh.link/cert1.pem
lrwxrwxrwx 1 root root   41 Mar 22 08:04 chain.pem -> ../../archive/server.bclh.link/chain1.pem
lrwxrwxrwx 1 root root   45 Mar 22 08:04 fullchain.pem -> ../../archive/server.bclh.link/fullchain1.pem
lrwxrwxrwx 1 root root   43 Mar 22 08:04 privkey.pem -> ../../archive/server.bclh.link/privkey1.pem

๋ฐœ๊ธ‰๋œ ์ธ์ฆ์„œ ํŒŒ์ผ๋“ค์„ Nginx Docker Container์— ์—ฐ๊ฒฐํ•˜์ž

  • Docker Volumn๊ณผ ์—ฐ๊ฒฐํ•ด์ฃผ๊ธฐ ์œ„ํ•ด ์œ„์—์„œ ์ƒ์„ฑํ•œ ์ธ์ฆ์„œ ํŒŒ์ผ์„ /cert ํด๋”๋ฅผ ๋งŒ๋“ค์–ด ๋‹ด์•„์ค€๋‹ค.
cd /
mkdir cert

sudo cp /etc/letsencrypt/archive/backend.bclh.link/fullchain1.pem /cert/fullchain1.pem 
sudo cp /etc/letsencrypt/archive/backend.bclh.link/privkey1.pem /cert/privkey1.pem
  • Nginx Config ํŒŒ์ผ์„ ์ˆ˜์ •ํ•ด์ค€๋‹ค.
    • 443์„ ๋””ํดํŠธ๋กœ ๋ฆฌ์Šค๋‹ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์œผ๋ฉฐ, Redirect ์„ค์ •, ssl certificate & key์˜ ๊ฒฝ๋กœ๋ฅผ ์žก์•„์ค€๋‹ค.
    • ์ˆ˜์ •ํ•œ ํŒŒ์ผ์€ ๋‹ค์‹œ build → Push To Ghcr ๊ณ ๊ณ  !
upstream nest {
    server 172.17.0.1:8080;
}

**server {
    listen 80;
    server_name backend.bclh.link; # ๋„๋ฉ”์ธ์œผ๋กœ ๋ณ€๊ฒฝ

    # Redirect
    location / {
        return 301 https://$host$request_uri;
    }
}**

server {
    listen 443 ssl;
    server_name backend.bclh.link;

    location / {
        proxy_pass <http://nest>;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
    }

    **ssl_certificate /etc/nginx/cert/fullchain1.pem;
    ssl_certificate_key /etc/nginx/cert/privkey1.pem;**
}
  • 443 ํฌํŠธ ์„ค์ •์„ ๋ณด์•ˆ๊ทธ๋ฃน์— ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.
  • Github Action cicd.yml ํŒŒ์ผ์— Port 443 ์ถ”๊ฐ€ & Volumn ์„ค์ •
- name: Run Nginx
      run: |
        docker stop ${{ env.DOCKER_NGINX_CONTAINER }} && docker rm ${{ env.DOCKER_NGINX_CONTAINER }} && docker rmi ${{ env.DOCKER_NGINX_IMAGE }}:latest
        docker run -d -p 80:80 -p 443:443 **-v /cert:/etc/nginx/cert** --name ${{ env.DOCKER_NGINX_CONTAINER }} --restart always ${{ env.DOCKER_NGINX_IMAGE }}:latest

 

๐Ÿ“Œ Reference