[CI/CD] 인스턴스부터 DooD 배포까지의 흐름 (Feat. Nginx, Jenkins)

TraceofLight·2023년 5월 27일
1

CI-CD

목록 보기
2/2
post-thumbnail

23-06-16 볼륨 마운팅 명료성을 위해 경로 수정
23-07-05 개발 편의를 위해 세팅 조정

서론

[CI/CD] Docker + Jenkins + Git Webhook을 활용한 FastAPI 서버 자동 배포 정리

이번 글을 작성하게 된 이유는 위에 있는 지난 글에 1차적으로 공부하려고 했던 내용을 담았지만 수정할 부분을 수정하고, 가다듬을 부분은 가다듬어서 깔끔한 하나의 글로 완성하고자 했기 때문인다.

지난 번 이후의 배포 관련 Task와 주변 사람들의 배포를 도와주는 과정에서 어느 정도의 노하우를 터득했고 정형화했기 때문에 해당 기록을 남겨 다음 번에도 배포하는 과정에서 하나의 일관된 프로세스로 사용할 수 있으면 좋겠다! 해당 글을 순서대로만 따라한다면 아마 배포하는 것이 어렵지 않을 것이라고 생각한다.

1편 이후로 공부한 추가적인 내용

  • Docker in Docker로 구현하는 것보다 Docker out of Docker로 구현하는 이유?
  • 기본적으로 Docker는 Docker 컨테이너 위에서 명령을 수행할 수 있도록 하는 것을 권장하지 않음
  • Docker in Docker는 Docker가 설치된 컨테이너에 막강한 권한을 부여하는 것으로 보안 위험을 초래할 수 있음
  • 그나마 Docker out of Docker는 컨테이너 외부 Docker와의 소켓 공유를 통해 도커 명령을 실행하는 방식
  • Docker API를 사용하는 방식은 어때?

사실 1편에서 문제 해결을 위해 사용해봤던 방식이고 실제로 효과를 봤다.
하지만 Docker out of Docker가 성공적으로 빌드된 상황이라면 CI/CD 절차에서 크게 아직 필요성을 느끼진 못할 듯..


따로 글을 나누기에는 애매한 거 같아서 간단하게 기록한 API 관련 내용

Docker는 REST API를 통해 엔진과 클라이언트가 통신을 하는데 대부분 사용할 일이 없지만 사용할 수 있다.

  docker --tlsverify --tlscacert=/.docker/ca.pem \
  --tlscert=/.docker/cert.pem --tlskey=/.docker/key.pem \
  -H={server_ip}:{port_number} exec nginx \
  sed -i '4s/.*$/ server {container}:8000;/' /etc/nginx/sites-available/default.conf

해당 코드는 TLS 인증서를 가진 Docker Client를 통해 임의의 서버에 위치한 Nginx 설정 파일의 내용을 수정하는 코드이다.
-H 명령어를 통해서 해당 서버의 포트를 통해 Daemon(Docker Engine) 과 통신하며 나머지 코드는 인증서를 통한 접속 보안 절차이다.

만약 인증서를 사용하지 않은 채로 포트를 열어두거나 한다면 누군가 열어둔 포트를 통해 컨테이너 탁란을 당할 수 있다!

컨테이너 탁란 실제 경험담. Docker에서 사용하는 일반적인 포트를 인증 보안 없이 풀어둔 결과...

인증서를 통한 보안 처리는 반드시 진행해야 하며 피치 못한 이유로 그렇지 못한다면 해당 포트를 통해 행할 수 있는 명령을 제한하는 것이 필수불가결하다.

Docker API Documents 의 내용을 보면 더욱 자세하게 확인 가능!


All in One 배포 기반 구축의 절차

1. 인스턴스 접속 및 인스턴스 업데이트

ssh -i {secure shell key 경로} {사용자 이름}@{IP 주소}

sudo apt-get update
sudo apt-get upgrade

ubuntu 세팅을 진행하는 경우 사용자 이름이 ubuntu인 경우가 많다.

2. 방화벽 설정

sudo ufw allow ssh    # ssh를 22로 대체할 수 있다.
sudo ufw allow http   # http를 80으로 대체할 수 있다.
sudo ufw allow https  # https를 443으로 대체할 수 있다.

sudo ufw enable 	  # 방화벽 가동
sudo ufw status 	  # 방화벽 상태 확인

아마 클라우드를 사용하는 경우 기본적으로 클라우드 자체적인 포트 보안 설정이 있기 때문에 해당 설정에서 개방을 따로 해줘야 한다.
ufw는 우분투의 방화벽인데 처음에 설정할 때 꺼져 있는 경우 포트 허가를 해준 후 켜주는 절차가 필요하다.
위 설정을 통해 기본적인 http(80), https(443), ssh(22) 포트를 개방한 상태로 방화벽을 켤 수 있다.

반드시 포트 allow 설정 먼저 해두고 켜는 것을 추천한다. 22번, ssh 포트를 닫아둔 채로 방화벽을 켜면 접속할 수 없다..

3. Docker 및 Docker Compose 설치

# Docker 설치

sudo apt-get update
sudo apt-get install ca-certificates curl gnupg

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

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


sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Docker Compose 설치

sudo apt install docker-compose

우분투 Docker 설치 Documents

2023년 5월 기준 Documents에서 필요한 내용만 몰아서 기재했다.

sudo usermod -aG docker $USER

해당 명령어를 통해 sudo 없이 docker 명령어를 사용할 수 있다.
해당 명령어를 넣지 않는다면 /var/run/docker.sock: connect: permission denied 가 발생할 것이다.

4. docker-compose.yml 작성

version: '3.8'

networks:
  {원하는 네트워크 이름}:
    driver: bridge

services:

  # Jenkins Container 설정
  jenkins:
    container_name: jenkins
    image: jenkins/jenkins:lts
    restart: unless-stopped
    ports:
      - "{원하는 포트 번호}:8080"
    volumes:
      - ./mount/jenkins:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
    user: root
    environment:
      - TZ=Asia/Seoul
    networks:
      - {원하는 네트워크 이름}

배포 파이프라인 툴은 Jenkins를 활용한다고 생각하고 작성했다.

추가할 네트워크가 없다면 네트워크 부분은 아예 배제할 수 있고 Docker out of Docker를 사용하기 위해 소켓에 대한 볼륨 바인딩을 진행한다. 필요에 따라서 Time Zone이나 추가 볼륨 바인딩을 설정할 수 있다.

5. 사용할 도메인을 DNS 설정

도메인 사업자마다 다른 방식을 사용한다. 해당 도메인 사업자의 등록 방법을 보고 내가 사용할 서버의 IP를 등록해서 도메인과 이어주는 과정을 처리해야 한다.

What's My DNS

등록하고 바로 사용할 수 있는 게 아니라 DNS 서버에 반영되는 시간이 존재한다. 간단하게 얼마나 퍼졌나 확인할 수 있는 사이트를 위에 첨부했다.

6. Let’s Encrypt를 활용한 인증서 발급

1) docker-compose.yml
# Nginx 설정
  nginx:
    container_name: nginx
    image: nginx:latest
    restart: unless-stopped
    volumes:
      - ./mount/nginx/nginx.conf:/etc/nginx/nginx.conf 			# Nginx 설정을 위한 바인딩
      - ./mount/nginx/sites-available:/etc/nginx/sites-available
      - ./data/certbot/conf:/etc/letsencrypt				# 인증서 공유를 위한 바인딩
      - ./data/certbot/www:/var/www/certbot
    ports:
      - 80:80
      - 443:443
    environment:
      - TZ=Asia/Seoul
    networks:
      - {원하는 네트워크 이름}

  # Certbot 컨테이너 설정
  certbot:
    container_name: certbot
    image: certbot/certbot
    restart: unless-stopped
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    environment:
      - TZ=Asia/Seoul

해당 내용을 docker-compose.yml에 추가하고 따로 처리하진 않는다.

2) mount/sites-available/default.conf
server {
     listen 80;
     listen [::]:80;

     server_name my_domain.org; 

     location /.well-known/acme-challenge/ {
             allow all;
             root /var/www/certbot;
     }
}

파일을 생성하고 server_name 뒤에 내 도메인을 기재한 내용을 담는다.

3) mount/nginx.conf
user nginx;
worker_processes auto;
worker_priority 0;

pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 1024;
        multi_accept off;
}

http {

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;
        server_tokens off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;
        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for"';

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        access_log /var/log/nginx/access.log main;
        error_log /var/log/nginx/error.log debug;

        gzip on;

        include /etc/nginx/sites-available/*;

}

nginx를 처음 설치할 때 내용을 기반으로 적당한 기초 설정을 담았다.

4) Let's Encrypt를 통해 절차를 거친 인증서 발급
curl -L https://raw.githubusercontent.com/wmnnd/nginx-certbot/master/init-letsencrypt.sh -o init-letsencrypt.sh
chmod +x init-letsencrypt.sh
vi init-letsencrypt.sh			# 여기서 도메인과 메일 주소를 입력해야 함
sudo ./init-letsencrypt.sh

인증서 발급 절차를 도와줄 파일을 외부에서 받아오고 권한을 설정한다. 이후 내 도메인과 메일 주소를 파일에 입력하고 해당 파일을 실행하는 것으로 인증서 발급을 진행할 수 있다. 발급이 실패할 경우 로그를 보고 실패 원인을 찾아 고쳐서 재시도하면 될 것이다.

7. 인증서 갱신까지 고려한 Nginx 세팅

# Nginx 설정
  nginx:
    container_name: nginx
    image: nginx:latest
    restart: unless-stopped
    volumes:
      - ./mount/nginx.conf:/etc/nginx/nginx.conf
      - ./mount/sites-available:/etc/nginx/sites-available
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    ports:
      - 80:80
      - 443:443
    environment:
      - TZ=Asia/Seoul
    networks:
      - {연결을 원하는 네트워크 이름}
    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"

  # Certbot 컨테이너 설정
  certbot:
    container_name: certbot
    image: certbot/certbot
    restart: unless-stopped
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    environment:
      - TZ=Asia/Seoul
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

Let's Encrypt를 통해 발급한 인증서는 사용 기한이 제한되어 있으며 대신 유효 기간이 얼마 남지 않았다면 재발급을 진행할 수 있다. certbot 컨테이너와 nginx 컨테이너에 재발급 관련 명령어를 추가하여 인증서 갱신이 자동으로 진행되도록 처리할 수 있다. 인증서 발급이 완료된 상황으로 nginx가 에러로 인한 컨테이너 빌드 실패가 뜨지 않을 것이다.

8. Jenkins 컨테이너 내부에 Docker 설치


추가된 내용) 원래는 명령어를 직접 나열했는데 직관적이지 못하다는 생각이 들어서 수정!

# /jenkins-docker-install.sh

apt-get update
apt-get install ca-certificates curl gnupg

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null

apt-get update
apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

기존 docker 설치 방법에서 sudo 명령어를 배제한 코드를 sh 파일로 제작했다. 이 파일을 마운트를 통해 내부에 넣고 실행시키면 깔끔하게 한 방에 설치할 수 있다!


9. Docker Compose 처리

version: '3.8'

networks:
  {네트워크 이름}:
    driver: bridge

services:

  # Jenkins Container 설정
  jenkins:
    container_name: jenkins
    image: jenkins/jenkins:lts
    restart: unless-stopped
    ports:
      - "{포트 번호}:8080"
    volumes:
      - ./mount/jenkins:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
      - ./mount/jenkins-html:/var/lib/jenkins
      - ./jenkins-docker-install.sh:/jenkins-docker-install.sh
    user: root
    environment:
      - TZ=Asia/Seoul
    networks:
      - {네트워크 이름}

# Nginx 설정
  nginx:
    container_name: nginx
    image: nginx:latest
    restart: unless-stopped
    volumes:
      - ./mount/nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./mount/nginx/sites-available:/etc/nginx/sites-available
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    ports:
      - 80:80
      - 443:443
    environment:
      - TZ=Asia/Seoul
    networks:
      - {네트워크 이름}
    depends_on:
      - {선행 컨테이너}
    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"

  # Certbot 컨테이너 설정
  certbot:
    container_name: certbot
    image: certbot/certbot
    restart: unless-stopped
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    environment:
      - TZ=Asia/Seoul
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

이렇게 Jenkins 설정까지 추가한 뒤

docker-compose -f {yml 파일 위치 및 파일명} up -d

명령어를 통해 컨테이너를 전부 띄울 수 있다.

docker exec -it jenkins bin/bash
sh /jenkins-docker-install.sh

이렇게 Jenkins 컨테이너 내부에 Docker를 설치하는 것으로 Jenkins 내부 Shell 명령어를 통해 Docker Engine을 제어할 수 있게 된다.
기존 Docker Documents를 참고했으나 차이점은 sudo를 사용하지 않는다는 점과 Debian용 Docker를 설치한다는 점이 있겠다.

선택 사항

1. Swap Memory 설정

# swap memory 설정

sudo fallocate -l {설정 용량} /swapfile   # 설정 용량은 대체로 G 단위로 설정한다.
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# 재부팅 시에도 지속 사용을 원할 경우

sudo vi /etc/fstab 			# '/swapfile swap swap defaults 0 0' 내용을 해당 파일에 삽입

# swap 비활성화

sudo vi /etc/fstab 			# '/swapfile swap swap defaults 0 0' 내용을 제거
sudo swapoff -v /swapfile
sudo rm /swapfile

인스턴스 성능 및 필요에 따라 인스턴스가 터지는 것을 방지하기 위한 스왑 메모리 설정을 진행할 수 있다.

2. Jenkins 플러그인 설치 오류 시 시도 방법

<해결책 1>
Jenkins 관리 > Plugin Manager > Advanced settings > 업데이트 사이트 파트에 접속
https://updates.jenkins.io/update-center.jsonhttp://updates.jenkins.io/update-center.json 로 변경

<해결책 2>
skip-certificate-check 라는 이름의 플러그인 설치

완벽한 해결책은 아니지만 해당 해결책을 전부 적용하는 것으로 어느 정도의 효과를 봤다.

마치며

Docker와 관련된 내용은 이후 일부 프레임워크 배포에 대한 것을 제외하면 여기서 일단락하려고 한다. 그래도 내가 만든 결과물을 실제 네트워크 상에서 볼 수 있다는 성취감이 있어서 배포에 대한 내용은 이후에도 기회가 된다면 더 적어볼 생각이 있다.

profile
24시간은 부족한 게 맞다

0개의 댓글