CI/CD를 구축하는 방법은 수백가지는 된다고 생각한다. 때문에 본인이 가지고있는 서버 환경이나 운영 되고있는 서비스에 따라 CI/CD 구축을 효율적으로 구축할 수 있어야 한다. 본 포스팅에서는 CI/CD에 대해, 그리고 Docker라는 것에 대해 막연하게 생각하고 있는 사람들에게 도움이 되고자 AWS EC2 인스턴스에 Docker를 활용하여 간단하게 CI/CD 구축하는 방법을 안내한다.
1. AWS EC2 인스턴스 생성 및 INOUND 포트 설정
AWS EC2 인스턴스는 요즘 클릭 몇번에 해결된다. 물론 생성 이후 보안규칙에서 INBOUND(EC2 인스턴스로 들어오는 데이터 혹은 요청에 대한 내용)에 대해 사전 지식이 필요하다. 나는 Ubuntu 22.04/t2.micro(프리티어)를 생성했다.
EC2인스턴스에는 docker만 설치했다. 파이프라인 구축에 필요한 Jenkins, spring application을 구동시킬 JDK, 향후 사용할 Redis 및 Nginx는 각각의 Docker Container로 구동시킬 것이고, spring application 및 Jenkins GUI에 접속, SSH 접속 및 SSL 보안설정을 하기 위해 Inbound 규칙을 80,8080,22,8089,443 포트를 추가하였다.
8080
Spring boot 기반의 Web Application에 접근하기 위한 포트. 추후 8080 포트를 Spring boot App에 접근하기 위한 8081 포트로 포트바인딩 해준다.
22
EC2 인스턴스로 SSH 접속을 하기위한 포트.
8089
Jenkins Dashboard (GUI)로 접근을 하기위한 포트. 추후 8089 포트를 Jenkins 도커 컨테이너의 8080포트로 포트바인딩 해준다.
80
추후 Nginx를 사용하여 Front Project에 접근하기 위한 포트. 추후 Nginx 도커 컨테이너의 80 포트로 포트바인딩 해준다.
443
SSL 보안설정을 통해 추후 Https로 접근하기 위한 포트.
2. 도커 설치 및 도커 컴포즈 파일 작성
AWS EC2 인스턴스에 SSH로 접속을 하건 Console로 접속을 하건, Termial로 접근하여 도커를 설치한다.
APT 도커 저장소 설정
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] 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
도커 컴포즈 파일 생성
나는 EC2의 /app/docker/ path에 모든 파일을 생성할 것이다. 우선 docker-compose.yml 파일을 아래와 같이 생성한다. weakwil-api-project는 글 작성 기준 현재 내가 참여하고 있는 의지박약 탈출 스터디의 프로젝트를 진행하기 위해 지은 이름으로 각자 원하는 이름을 작성하면 된다.
version: '3.9'
services:
nginx:
image: nginx:latest
ports:
- "80:80"
container_name: nginx
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/sites-enabled:/etc/nginx/sites-enabled #virtual host
- /app:/app
networks:
- weakwill-api-project
redis:
image: redis:latest
container_name: redis
ports:
- "6379:6379"
networks:
- weakwill-api-project
# Jenkins 서비스 추가
jenkins:
image: jenkins/jenkins:latest
container_name: jenkins
ports:
- "8089:8080" # Jenkins 웹 인터페이스 포트
- "50000:50000" # Jenkins 에이전트 포트
volumes:
- ./jenkins_home:/var/jenkins_home # Jenkins 데이터 저장 볼륨
networks:
- weakwill-api-project
networks:
weakwill-api-project:
기타 config 파일 생성
추후 사용할 nginx 설정 관련 폴더를 /app/docker/nginx/ 하위에 생성한다.
nginx.conf
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
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"';
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
#include /etc/nginx/sites-enabled/*;
}
conf.d/default.conf
server {
listen 80;
location / {
proxy_pass http://당신의 Aws Ec2 Endpoint:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
도커 컴포즈로 컨테이너 생성
컨테이너를 생성하기 전에, 나처럼 프리티어로 진행하는 분들의 경우 프리티어의 인스턴스 메모리는 우리가 넉넉히 사용하지 못하는 양이기에 스왑 메모리를 설정해주자. 구글에 Swap Memory Setting in Ubunut 등의 키워드로 검색하면 수많은 방법들이 나와있을 것이다. 나는 2GB의 스왑메모리를 확보하였다. 아래 도커 컴포즈 명령어를 사용하여 docker-compose.yml 파일에 명시되어있는 내용을 기준으로 컨테이너를 생성해준다.
/app/docker$ docker-compose up -d
/app/docker$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
295f4c4709b4 nginx:latest "/docker-entrypoint.…" 2 seconds Up 37 hours 0.0.0.0:80->80/tcp, :::80->80/tcp nginx
4bbf7aa15872 redis:latest "docker-entrypoint.s…" 2 seconds Up 37 hours 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp redis
afb6f3754528 jenkins/jenkins:latest "/usr/bin/tini -- /u…" 2 seconds Up 37 hours 0.0.0.0:50000->50000/tcp, :::50000->50000/tcp, 0.0.0.0:8089->8080/tcp, :::8089->8080/tcp jenkins
3. Spring Boot Project에 JenkinsFile 생성
서비스할 예정인 프로젝트면 Springboot던 Laravel이던 상관이 없다. 본인이 배포하고싶은 프로젝트의 최상위 패스에 JenkinsFile을 아래와 같이 설정한다.
pipeline {
agent any
//triggers {
// // This will trigger the pipeline on every push to the repository
// pollSCM('H * * * *')
//}
stages {
stage('Checkout') {
steps {
// Checking out the code from the repository
checkout scm
}
}
stage('Build') {
steps {
// Run the gradle build
sh './gradlew clean build'
}
}
stage('Test') {
steps {
echo 'Testing the application...'
// Run tests (if any)
sh './gradlew test'
}
}
stage('Deploy') {
steps {
// Deploy the application
sh '''
cp build/libs/*SNAPSHOT.jar ~/deploy/
echo $(date +"%Y-%m-%d %H:%M:%S") > ~/deploy/deploy.txt
'''
}
}
}
post {
always {
// Clean up the workspace to free up space
cleanWs()
}
}
}
Pipeline 구성
Agent
agent any는 이 파이프라인이 Jenkins의 임의의 사용 가능한 에이전트에서 실행될 수 있음을 의미한다.
Triggers
triggers 섹션에서는 pollSCM('H * * * *')를 사용하여 소스 코드 관리(SCM) 시스템을 매시 정각마다 폴링하도록 설정한다. 이는 새로운 커밋이 있는지 확인하고, 있으면 파이프라인을 트리거하지만 우리는 웹훅을 사용할테니 주석처리 한다. 웹훅을 사용하지 않고 주기적으로 폴딩할때 사용한다.
Checkout
'Checkout' 스테이지에서는 checkout scm 명령을 사용하여 소스 코드를 체크아웃한다. 이는 파이프라인이 실행되는 위치에서 Jenkinsfile을 포함한 저장소의 최신 코드를 가져온다.
Build
'Build' 스테이지에서는 sh './gradlew clean build' 명령을 통해 Gradle 빌드를 실행다. 이 단계에서는 프로젝트가 클린 빌드 되며, 컴파일 및 기타 빌드 관련 태스크가 수행된다.
Test
'Test' 스테이지에서는 sh './gradlew test' 명령으로 프로젝트의 테스트를 실행한다. 이는 단위 테스트나 다른 종류의 자동화된 테스트가 포함될 수 있다.
Deploy
'Deploy' 스테이지에서는 sh 스크립트를 사용하여 빌드된 JAR 파일을 ~/deploy/ 디렉토리로 복사하고, 현재 시간을 deploy.txt 파일에 기록하게 한다. 이는 배포의 일환으로 application을 컨테이너화 시킬때 사용할 것이다.
Post-processing
항상 실행되는 후처리 단계로, 위 스크립트는 젠킨스 작업 공간을 정리한다.
3. Application을 포함하는 도커파일 생성
나는 JDK 17버전을 사용하는 Spring boot 기반의 Application을 배포할 것이기 때문에 아래와 같이 작성하였다. /app/docker/springboot/ 밑에 Dockerfile을 작성했다.
# 기본 이미지로 OpenJDK가 포함된 공식 Java 이미지를 사용한다.
FROM openjdk:17
# 애플리케이션 JAR 파일을 컨테이너 내부로 복사한다.
COPY ../jenkins_home/deploy/weakwill-api-0.0.1-SNAPSHOT.jar /app/weakwill-api.jar
# 컨테이너가 시작될 때 애플리케이션을 실행한다.
ENTRYPOINT ["java", "-jar", "/app/weakwill-api.jar"]
나는 /app/docker/jenkins_home을 jenkins 컨테이너의 /var/jenkins_home과 볼륨마운트 시켜놨기 때문에 위처럼 작성했다. 본인의 젠킨스 설정에 맞게 바꿔 사용하면 된다. 꼭 젠킨스 홈이 아니더라도, 컨테이너 외부의 ec2에서 접근이 가능한 폴더로 이동시키면 되니 참고하여 설정하자.
4. deploy.sh 파일 생성 및 크론 등록
흔히들 Jenkins를 CI/CD 도구로 알고있는데, 내가 생각했을 때 Jenkins는 CI (지속적인 통합 및 테스트) 까지를 구성하는 파이프라인 도구로 활용하면 좋겠지만, 그 다음의 배포 프로세스는 Docker Image를 만들어 DockerHub에 Pull하고, 그 트리거를 서비스 운영 서버에서 캐치하여 pull 및 run을 하는 형식으로 가야한다고 생각한다.
하지만 여기서는 하나의 Ec2 서버에서 모든것 (CI 부터 CD 까지)을 하는 목적을 두기 때문에 나는 서비스 어플리케이션 배포를 deploy.sh라는 쉘스크립트 파일을 만들었다. 주석을 보면 설명을 달아두었으니 이해하기 쉬울 것이다.
#!/bin/bash
# deploy.txt 파일의 경로
DEPLOY_FILE="./jenkins_home/deploy/deploy.txt"
# JAR 파일의 경로
JAR_FILE="./jenkins_home/deploy/weakwill-api-0.0.1-SNAPSHOT.jar"
# deploy 폴더의 경로
DEPLOY_DIR="./jenkins_home/deploy"
# Docker 이미지 이름
IMAGE_NAME="weakwill-api"
# Docker 이미지 버전 태그
IMAGE_TAG="latest"
# Docker 컨테이너 이름
CONTAINER_NAME="weakwill-api-container"
# deploy.txt 파일이 존재하는지 확인
if [ -f "$DEPLOY_FILE" ]; then
# Docker 이미지 빌드
DOCKER_BUILDKIT=1 docker build -t $IMAGE_NAME:$IMAGE_TAG -f ./springboot/Dockerfile .
# 기존 컨테이너가 실행 중인지 확인
# 기존 컨테이너 이름 변경
if docker ps -q -f name=$CONTAINER_NAME; then
OLD_CONTAINER_NAME="${CONTAINER_NAME}-old-$(date +%Y%m%d%H%M%S)"
docker stop $CONTAINER_NAME
docker rename $CONTAINER_NAME $OLD_CONTAINER_NAME
#docker rm -f $OLD_CONTAINER_NAME
fi
# 새 Docker 컨테이너 실행
docker run -d -p 8080:8081 --name $CONTAINER_NAME $IMAGE_NAME:$IMAGE_TAG
# 기존 컨테이너 삭제 (선택 사항: 안정성 확인 후 수행)
docker rm -f $OLD_CONTAINER_NAME
# deploy 폴더 안의 내용 삭제
rm -rf "$DEPLOY_DIR"/*
fi
1~4번까지 모든 프로세스를 구축하였다면 본인이 사용하는 git 저장소와 jenkins 컨테이너와의 webhook을 구성을 잘 맞췄다는 가정 하에 push 이벤트를 트리거로 삼아서 자동으로 빌드부터 테스트, application container를 통한 실 서비스 배포까지 정상적으로 작동할 것이다.
하지만 Jenkins를 검색해서 본 포스팅을 읽는 독자분의 경우 대부분 GitLab보다는 GitHub를 사용할테니 (GitLab은 독자적인 CI/CD 및 데브옵스 워크플로우를 내장해서 Jenkins없이도 수 많은 방법이 있음) 웹훅 설정이 안되어 있다면 google에 "Git Hub Jenkins WebHook Setting" 등의 키워드로 검색하여 웹훅을 설정할 수 있으니 참고 바란다.
5. 결론
1. Source Code Update
2. Git Push
3. 젠킨스 Stage VIew 확인
젠킨스에서 application에 명시한 jenkinsfile의 스크립트를 실행한다.
4. 배포 성공 파일 생성여부 확인
배포 관련 파일이 생성되었는지 확인하고 기존 컨테이너 시작시간이 6시간 전인걸 확인한다.
5. 크론 동작 확인
1분 뒤 크론이 동작하면서 새로운 컨테이너로 바뀐 것을 확인한다.
6. 서비스 적용 확인
ec2의 엔드포인트 혹은 public IP로 접속하여 확인한다.
'CICD' 카테고리의 다른 글
[CI/CD 구축] AWS, Docker, GitLab을 사용하여 CI/CD 구축하기 3편(끝) (0) | 2022.12.23 |
---|---|
[CI/CD 구축] AWS, Docker, GitLab을 사용하여 CI/CD 구축하기 2편 (0) | 2022.12.16 |
[CI/CD 구축] AWS, Docker, GitLab을 사용하여 CI/CD 구축하기 1편 (2) | 2022.12.10 |