
처음으로! CI, CD 파이프라인을 구축해 보았다! 나의 기억력을 믿지 못하기 때문에... 기록해보았다!
CI/CD의 기본 개념은 지속적인 통합, 지속적인 서비스 제공, 지속적인 배포이다. CI/CD는 새로운 코드 통합으로 인해 개발 및 운영팀에 발생하는 문제(일명 "통합 지옥(integration hell)")를 해결하기 위한 솔루션이다!
나는 EC2에 Docker container를 띄우고, main branch에 push 했을 때 배포 자동화 되도록 만들었다.
테스트 코드 통과 시 배포 되도록 하고 싶었지만... 지금 회사 상황상 테스트 코드까지는 무리..이다
도구를 선택하기 전에 했던 고민들은 다음과 같다.
커뮤니티 관련 웹 서비스라 규모가 아주 크지 않았고, 초기 서비스라 사용자도 많지 않을 것으로 예상했다.
지금 재직 중인 회사에는 배포 자동화가 구축되어 있지 않다.
일단 내가 혼자 구축해낼 수 있는, 빨리 배워서 성공시킬 수 있는 도구를 선택 해야겠다고 생각했다.
현재 사용 중인 코드 호스팅 플랫폼(Github), 클라우드 서비스(AWS RDS, AWS S3)와의 통합을 고려했다.
스타트업이다보니 아무리 기술적으로 확장성이 높고 사용하기 편하더라도 오버스팩은 ... 비용적으로 부담이다.
Docker Container도 하나(혹은 두 개? 정도만)만 띄울 예정이고 애플리케이션 규모도 그리 크지 않아, 쿠버네티스와 같은 기술은 지금 당장은 필요하지 않았다.
장점 : 유연성 및 확장성
다양한 빌드 및 배포 시나리오에 맞게 구성할 수 있다.
단점 : 설정 복잡성 및 초기 학습 곡선
EC2 가상 서버에 컨테이너를 띄우고 관리한다.
장점 : 관리 수준 및 자동 확장, 리소스 관리
- 서버리스 컨테이너 실행 환경을 제공하며 EC2 인스턴스 관리 없이 컨테이너를 실행한다.
- 컨테이너의 CPU 및 메모리 요구 사항을 기반으로 자동으로 필요한 컴퓨팅 리소스를 할당한다. 편리하지만, 리소스 할당에 따라 비용이 발생할 수 있다..
단점 : 비용 및 초기 학습 곡선
CPU 및 메모리 요구 사항, 실행 시간 및 사용된 컨테이너 수에 따라 결정된다. 사용량에 따라 비용이 크게 다를 수 있다. 아직.. Fargate를 잘 몰라서 사용량에 따라 비용이 다르다고 하면 무섭다...
Fargate를 사용하지 않으니 ECS를 사용할 필요가 없었고, 복잡한 배포나 확장성까지는 아직 필요가 없어 EKS도 필요하지 않았다.
node.js 버전을 내가 로컬에서 사용하고 있던 18버전으로 해주었다.
가벼운 알파인 버전으로 이미지를 빌드해주었다!
# Base image
FROM node:18-alpine
# Create app directory
WORKDIR /usr/src/app
# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
# Install app dependencies
RUN npm install
# Bundle app source
COPY . .
# Creates a "dist" folder with the production build
RUN npm run build
# --------------------------------------------------------------
ENV NODE_ENV=production
EXPOSE 8000
# Start the server using the production build
CMD [ "node", "dist/main.js" ]
Dockerfile
.dockerignore
.git
.gitignore
.cache
*.md
EC2 인스턴스는 기본적인 설정만 해주었다. 이 글에서는 생략하겠다
# root 계정으로 전환 + 현재 환경변수 사용
$ sudo su
# 패키지 버전 최신화
$ sudo apt update
$ sudo apt-get upgrade -y
CI, CD를 적용하려고 하는 Github repository의
Settings > Actions > runner > New self-hosted runner를 클릭하면 가이드라인이 나온다!
나는 EC2 인스턴스를 ubuntu 환경으로 만들었기 떄문에 Linux 환경으로 설정해주었다.
# Create a folder
$ mkdir actions-runner && cd actions-runner
# Download the latest runner package
$ curl -o actions-runner-linux-x64-2.311.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
# Optional: Validate the hash
$ echo "29fc8cf2dab4c195bb147384e7e2c94cfd4d4022c793b346a6175435265aa278 actions-runner-linux-x64-2.311.0.tar.gz" | shasum -a 256 -c
# Extract the installer
$ tar xzf ./actions-runner-linux-x64-2.311.0.tar.gz
# Create the runner and start the configuration experience
$ ./config.sh --url https://github.com/<repository주소> --token <토큰>
# Last step, run it!
$ ./run.sh
$ ./run.sh : 세션이 종료되어도 runner 유지하는 명령어

테스트 통과 시, 배포가 되는 과정이... 정석이지만
현재 나의 회사는... 테스트 코드를 작성하기 어려운 상황이다,, 일단 주석으로 처리 해놓았다... 추후에 꼭 테코 작성할 수 있길 바라며
# 해당 워크플로우의 타이틀
name: CI Pipeline
#이벤트
# main 브랜치에 한하여 push가 일어날 경우
on:
push:
branches: [ "main" ]
# jobs
# 이벤트가 발생하면 해야할 작업을 명시
# 테스트, 빌드, 배포 단위로 명시 (테스트는 생략 했음)
# 각각의 job은 Runner라는 컨테이너에서 개별적으로 실행
jobs:
# # 프로젝트 테스트 코드 With Jest
# test:
# # ubuntu-latest 환경의 Runner에서 실행
# runs-on: ubuntu-latest
build:
# # build가 시작되려면 test가 완료되어야 함
# needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v3
- name: Set up node.js 18.16.1
uses: actions/setup-node@v3
with:
node-version: 18.16.1
cache: 'npm'
- name: Install dependencies
run: npm install
# - runs: npm run test:e2e
# 도커 Hub 로그인
- name: Login Dockerhub
env:
DOCKER_USERNAME: ${{secrets.DOCKERHUB_USERNAME}}
DOCKER_PASSWORD: ${{secrets.DOCKERHUB_PASSWORD}}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
# Docker 이미지 빌드
- name: Build the Docker image
run: docker build -t <이미지이름> .
# 이미지 태깅
- name: taging
run: docker tag <이미지이름>:latest <dockerhub계정이름>/<이미지이름>:latest
# Dockerhub에 푸시
- name: Push to Dockerhub
run: docker push <dockerhub계정이름>/<이미지이름>:latest
Github Runner(self-hosted) 생성 가이드 라인에 다음과 같이 나와있어
runs-on: self-hosted 로 설정해주었다!

이미지를 Docker Hub에서 Pull하고,
이전 컨테이너를 삭제한 뒤,
새로운 컨테이너를 실행한다.
name: CD Pipeline
on:
workflow_run:
workflows: ["CI Pipeline"]
types:
- completed
jobs:
build:
runs-on: self-hosted
steps:
- name: Login Dockerhub
env:
DOCKER_USERNAME: ${{secrets.DOCKERHUB_USERNAME}}
DOCKER_PASSWORD: ${{secrets.DOCKERHUB_PASSWORD}}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
# Docker 이미지는 변경되지 않아야 하기 때문에, env 파일 설정은 CD 단계에서 처리
- name: .env setting
run: |
touch .env
echo "DB_HOST=${{secrets.DB_HOST}}" >> .env
echo "DB_PORT=${{secrets.DB_PORT}}" >> .env
echo "DB_NAME=${{secrets.DB_NAME}}" >> .env
echo "DB_PASSWORD=${{secrets.DB_PASSWORD}}" >> .env
echo "DB_USERNAME=${{secrets.DB_USERNAME}}" >> .env
echo "AWS_ACCESS_KEY=${{secrets.AWS_ACCESS_KEY}}" >> .env
echo "AWS_SECRET_KEY=${{secrets.AWS_SECRET_KEY}}" >> .env
echo "AWS_S3_BUCKET_NAME=${{secrets.AWS_S3_BUCKET_NAME}}" >> .env
echo "THUMBNAIL_OUTPUT_PATH=${{secrets.THUMBNAIL_OUTPUT_PATH}}" >> .env
echo "FFMPEG_PATH=${{secrets.FFMPEG_PATH}}" >> .env
echo "FFPROBE_PATH=${{secrets.FFPROBE_PATH}}" >> .env
echo "JWT_SECRET=${{secrets.JWT_SECRET}}" >> .env
echo "JWT_EXPIRES_IN=${{secrets.JWT_EXPIRES_IN}}" >> .env
echo "KAKAO_REST_API_KEY=${{secrets.KAKAO_REST_API_KEY}}" >> .env
echo "KAKAO_REDIRECT_URI=${{secrets.KAKAO_REDIRECT_URI}}" >> .env
echo "CORS_ORIGIN_EC2=${{secrets.CORS_ORIGIN_EC2}}" >> .env
- name: Pull Docker image
run: sudo docker pull <dockerhub계정이름>/<이미지이름>:latest
- name: Delete Old docker container
run: sudo docker rm -f web-api-container || true
- name: Run Docker Container
run: sudo docker run -d -p 8080:8080 --env-file .env --name web-api-container <dockerhub계정이름>/<이미지이름>:latest
Install Docker Engine on Ubuntu
# Add Docker's official GPG key:
$ 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
# Add the repository to Apt sources:
$ 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
모두 완료 되었으면,
main branch에 push를 하고! CI, CD가 잘 완료 되었는지 확인해보자!

$ sudo dockr ps -a


끄읏!!!!!! 😊👍
실제로는... 한 번에 CI/CD 환경이 쨘! 하고 구축되지 않았다...
docker 이미지와 컨테이너를 몇 번이고.. 만들었는데 사용하지 않는 이미지와 컨테이너가 디스크를 차지하고 있어, 새로운 docker 이미지와 컨테이너가 생성되지 않았었다.
이럴 때는
$ docker system prune
명령어를 입력해주면 사용하지 않는 이미지, 컨테이너들을 한 번에 삭제시킬 수 있다!

삭제 이후에 바로 docker 이미지 및 컨테이너를 생성하니 문제 없이 생성되었다!
2-5번에는 바로 .env 설정을 해준 코드를 올려놓았지만, 처음에는 .env 설정을 따로 해주어야 하는지 몰랐다.
Github Actions로 CI/CD가 완성되었다고 떴지만, docker log를 찍어보니, 아래와 같이 env 파일을 찾지 못해 AWS S3 Bucket 연결이 되지 않고 있었다.

구글링을 해보니 Github Actions로 도커라이징을 해줄 때 .env 파일은 github repository에 코드가 올라가 있지않아, 따로 github secret 설정을 해주어야 한다는 것을 알게 되었다.
2-5번에 나와있는 코드대로 필요한 env 설정을 해주고,
secret을 등록하고 싶은 GitHub Repository로 들어가 Settings > Secrets and analysis > Actions > New repository secret 에서 하나씩 등록해주면 된다!


Name에는 env변수명을 Secret에는 값을 넣어주면 된다!