들어가며
먼저, 이전에 개인 프로젝트를 통한 AWS Serverless 배포 파이프라인을 구축하였다. 많은 요소들을 신경 써서 여러모로 구축하였고, 서버 비용도 예상할 수 있는 수준 이내로 끊으려고 했었다. 그런데 한 가지 문제가 발생하는데...
그것은 바로 배포가 무려 30분이나 걸린다는 사실이었다. 그 이유는 다음과 같았다. cpu 2 core, memory 4GB씩 물려 있던 ECS의 리소스를 비용 문제로 각각 절반식 줄여버린 것이다. 사실 생각해 보면 이미 예견된 문제였었다. AWS freetier에서 지원하는 ec2 t2.micro에서 스프링을 빌드해 본 적이 있다면 알 수 있을 것이다. 1 core와 1GB memory를 지원하는 t2.micro에서 gradlew build를 실행하면, 빌드 도중에 메모리가 부족해서 뻗어버린다!
이러한 문제를 해결하기 위해서는 swap memory 설정을 직접 해줘야 한다. 스토리지 공간까지 사용하여 빌드를 진행해야 t2.micro에서 그레이들을 빌드할 수 있다. 그러한 관점에서 생각해 보면 ECS의 리소스를 각각 절반까지 줄인 것은 내 실수였다. 배포까지 30분이 걸리다니...
급하게 비용 문제와 이것저것 다양한 문제를 해결하기 위해서 개인적으로 큰 서버를 학교 내에서 관리하는 친구에게 연락하여 적당한 스펙의 서버를 얻어냈다. CPU 2 core에 Memory 4GB로, 이전에 비하면 훨씬 쾌적한 스펙이다. 이렇게 클라우드와 온프레미즈를 함께 쓰게 전환되면서, 이번 포스팅에서는 Jenkins를 통한 CI/CD를 다루도록 한다.
본론
Jenkins?
Jenkins는 오픈 소스 CI/CD 툴으로써 다양한 소프트웨어 프로세스를 자동화하는 데 쓰이는 도구이다. 배포뿐만 아니라, 빌드, 테스트, 배포 작업을 자동화해 주는 서버이다. 오픈 소스로 굉장히 확장되어 있기 때문에 젠킨스 내에서 다양한 플러그인을 설치할 수 있으며, github 및 gitlab과의 통합도 잘 되어있는 편이다. 젠킨스 내부적인 DSL을 사용하기 때문에 초반에는 적응하기 어려울 수 있으나, 제대로 다룰 줄 안다면 CI/CD 뿐만 아니라 다양한 것들을 수행할 수 있다.
젠킨스에서 가장 중요한 것은 파이프라인이다. 젠킨스는 파이프라인을 통해서 일련의 CI/CD 작업을 정의할 수 있다. 이는 jenkinsfile이라는 파일에 코드로 작성할 수 있다.
서버 아키텍처
이제 본격적으로 Jenkins를 통한 CI/CD 구축을 구현해 보자. 일반적으로 젠킨스와 서버를 분리할 수 있다면 가장 좋겠지만, 이 포스팅은 하나의 서버에, Jenkins와 애플리케이션 서버를 같이 띄우는 상황을 구축하는 것이다.
이 과정에 앞서, nginx proxy manager라는 도구를 통해서 서버로 들어오는 트래픽을 다양하게 분산시키는 구성으로 만들면 아래와 같다.
위와 같은 상황이다. 먼저 포트 바인딩에 대해서 설명하자면, 외부에서 접근하지 않는 Mariadb를 제외하고는 모두 포트 바인딩을 통해서 외부로 노출시킨다. 젠킨스와 스프링이 같은 포트를 사용하기 때문에 적당히 포트를 바꿔서 배포한다. 일단 위와 같은 구조로 진행하였다.
그리고 NPM(nginx proxy manager)를 통해서 example.com의 서브 도메인으로 들어오는 모든 트래픽을 분산시켜서 뿌려준다. npm., jenkins., api. 등의 서브 도메인을 통해 80번 포트를 들어오는 트래픽을 https를 통해 분산시켜 주는 구성을 가지고 있다. npm의 설정 및 route 53의 설정에 대해서는 아래에서 다루도록 한다.
docker-compose
먼저, docker-compose.yml 파일을 작성해 보자.
version: '3.8'
services:
jenkins:
image: jenkins/jenkins:lts
restart: always
container_name: jenkins
user: root
ports:
- '8081:8080'
- '50000:50000'
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- jenkins_data:/var/jenkins_home
npm:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80'
- '443:443'
- '81:81'
environment:
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_NAME: "npm"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
- db
maria-db:
image: 'jc21/mariadb-aria:latest'
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: 'npm'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npm'
MARIADB_AUTO_UPGRADE: '1'
volumes:
- ./mysql:/var/lib/mysql
volumes:
jenkins_data:
이와 같이 docker-compose.yml을 작성하고 실행하게 되면 세 가지 컨테이너가 동시에 실행되게 된다. 여기서 한 가지 콘솔에 직접 붙어서 설정해두어야 하는 게 있는데, 그것은 바로 jenkins 컨테이너에 직접 연결해서 docker를 설치해야 한다는 점이다.
docker exec -it jenkins /bin/bash
위 명령어는 docker container에 접근하는 명령어이다. 접근 후에 docker를 설치한다.
apt-get update && apt-get install -y docker.io
위와 같이 설치하면 기본적인 구조는 끝난다.
NPM 세팅
다음으로는 npm의 세팅이 있다. 이에 대한 세팅은 아래의 링크를 참고하여, 각각의 서브 도메인을 통해서 흘려보내준다.
- [tistory] nginx proxy manager란? : https://marsboy.tistory.com/51
docker-compose를 사용해서 띄웠다면 81번 포트로 접근할 수 있다. 초기 username과 password는 아래와 같다.
- email : admin@example.com
- password : changeme
NPM 세팅을 통하여 jenkins. 및 api. 과 같이 서브도메인으로 접속하는 경우 각각의 포트에 분산해 주는 방식으로 사용하자.
Jenkins 세팅
이제 본격적으로 jenkins에 접속하자. 초기 비밀번호에 대해서는 특정 디렉터리를 참조하라고 맨 처음에 나와있다. docker 기반으로 실행하게 되면 docker container에 직접 접속하여 특정 디렉터리를 참조해야 하기 때문에 아래의 방식대로 진행하자.
먼저 컨테이너에 접속한 후, 해당 패스워드를 읽어온다.
docker exec -it jenkins /bin/bash
cat /var/jenkins_home/secrets/initialAdminPassword
이로써 얻은 password를 집어넣으면 진행할 수 있다.
그 후 위와 같은 페이지에서 Install suggested plugins를 클릭하여 적절한 플러그인을 세팅된 jenkins를 실행한다.
세팅이 완료됐다면, 계정을 생성하고, URL을 지정한 후에 다음과 같은 페이지를 볼 수 있다.
여기서 왼측의 새로운 item을 통해서 파이프라인을 생성할 수 있다.
item name을 지정하고 Pipeline을 선택한다.
그 후 위와 같은 페이지를 볼 수 있을 텐데, 설명에 맞게 적당히 필요한 옵션을 클릭한다. CI/CD 단계에서 가장 중요한 옵션은 이 빌드는 매개변수가 있습니다 섹션이다. 다양한 타입의 매개 변수를 입력할 수 있는 데, 아래와 같은 파라미터들이 있다. 이렇게 직접 젠킨스에서 집어넣은 매개변수(파라미터)를 사용할 수 있다.
각각의 파라미터들을 넣어줄 수 있는 방법들은 아래와 같다.
pipeline {
agent any
parameters {
booleanParam(name: 'DEPLOY_TO_PRODUCTION', defaultValue: false, description: 'Production에 배포할지 여부')
choice(name: 'ENVIRONMENT', choices: ['development', 'staging', 'production'], description: '배포할 환경 선택')
credentials(name: 'AWS_CREDENTIALS', description: 'AWS 자격증명')
file(name: 'CONFIG_FILE', description: '빌드에 필요한 설정 파일')
text(name: 'LONG_TEXT', description: '추가 설명을 입력하세요')
password(name: 'DB_PASSWORD', description: '데이터베이스 비밀번호')
run(name: 'PREVIOUS_BUILD', job: 'my-other-job', description: '참조할 이전 빌드 선택')
string(name: 'USERNAME', defaultValue: 'default_user', description: '사용자 이름')
text(name: 'PEM_KEY', description: 'master.pem 파일의 내용')
}
stages {
stage('Example') {
steps {
script {
echo "DEPLOY_TO_PRODUCTION: ${params.DEPLOY_TO_PRODUCTION}"
echo "ENVIRONMENT: ${params.ENVIRONMENT}"
echo "AWS_CREDENTIALS: ${params.AWS_CREDENTIALS}"
echo "CONFIG_FILE path: ${params.CONFIG_FILE}" // File parameter will be available as a file path
echo "LONG_TEXT: ${params.LONG_TEXT}"
echo "DB_PASSWORD: ${params.DB_PASSWORD}"
echo "PREVIOUS_BUILD: ${params.PREVIOUS_BUILD}"
echo "USERNAME: ${params.USERNAME}"
echo "PEM_KEY: ${params.PEM_KEY}"
}
}
}
}
}
위와 같이 parameters에 정의를 해둔 후 params.USERANME과 같이 꺼내서 셸 스크립트로 사용할 수 있다. 현재 프로젝트에서 진행하고 있는 jenkins 파이프라인 코드를 살펴보면 아래와 같다.
pipeline {
agent any
environment {
DOCKER_IMAGE = 'your-project:latest'
CONTAINER_NAME = 'your-project'
}
parameters {
text(name: 'ENV_FILE', defaultValue: '', description: '.env 파일의 내용')
}
stages {
stage('Pull Code') {
steps {
git url: 'https://github.com/your-project.git', branch: 'main'
}
}
stage('Create .env File') {
steps {
script {
writeFile file: 'src/main/resources/.env', text: params.ENV_FILE
}
}
}
stage('Gradle Build') {
steps {
script {
sh './gradlew clean build
}
}
}
stage('Build Docker Image') {
steps {
script {
sh "docker build -t ${DOCKER_IMAGE} ."
}
}
}
stage('Stop Running Container') {
steps {
script {
sh "docker stop ${CONTAINER_NAME} || true"
sh "docker rm ${CONTAINER_NAME} || true"
}
}
}
stage('Run New Container') {
steps {
script {
sh "docker run -d --name ${CONTAINER_NAME} -p 8080:8080 ${DOCKER_IMAGE}"
}
}
}
}
}
위와 같은 코드를 통해 간단하게 git 프로젝트를 불러와서, 해당 코드에 .env 파일을 삽입하여 도커 기반으로 실행하는 CI/CD를 구축할 수 있다. 근데 한 가지 빠진 게 있다. webhook을 통해서 jenkins를 트리거하는 코드를 github actions에 삽입해야 한다는 것이다.
파이프라인 설정 중 위와 같이 GitHub hook trigger for GITScm polling을 클릭한다. 그리고 반대인 github에서 반대로 jenkins의 url을 등록해줘야 한다.
중요한 포인트는 젠킨스 URL에 /github-webhook을 붙여서 webhook을 세팅해야 한다. 이를 통해 push event가 발생했을 때, 자동으로 젠킨스의 파이프라인이 실행되도록 할 수 있다!
파이프라인이 성공적으로 진행되면 위와 같은 결과를 확인할 수 있다.
마치며
multi-line string parameters에 쓰인 default 값이 초기화되는 이슈가 있는 것 같다. 오픈 소스를 직접 까보니까, multi-line string parameter가 string parameters로 쪼개지는 데, 해당 과정에서 문제가 발생하는 것 같다.
'DevOps > On-premise' 카테고리의 다른 글
Nginx Proxy Manager란? (1) | 2024.11.01 |
---|