최종 프로젝트 인프라 구축⑪ - multibranch CI 환경 구축
클라우드 서비스 구축
테라포머로 클라우드 인프라를 구축했지만, 테스트가 전부 로컬 온프레미스 위주로 돌아가고 있어서 막상 서비스를 구축하지 못했는데, 드디어 서비스를 올릴 수 있게 되었다.
그래서 이번에는 지난번 구축한 EFK에 서비스 pod를 올리고, 이를 외부에서 접속하기 위한 로드밸런서도 생성해 보겠다.
그 전에, AWS 환경으로 이미지를 빌드하고 올릴 작업이 필요해 이번 시간에는 해당 내용이다.
ECR 생성
ECR은 AWS에서 제공하는 컨테이너 이미지 레지스트리이다.
‘레포지토리 생성’버튼을 눌러 spring boot 서비스 이름별로 레포지토리를 생성해 주자.
당직(当職)은 서비스가 10개라 총 10개를 생성했다.
Jenkins 수정
젠킨스는 그냥 로컬 온프레미스에 있는 것으로 클라우드까지 배포하기로 했다.
개발환경(dev)에 이어 운영환경(prod)가 생겼고, 운영환경의 컨테이너 레지스트리는 AWS 기반이므로 AWS 접근을 위해 젠킨스 컨테이너파일을 수정해 줘야 했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
FROM jenkins/jenkins:lts-jdk17
USER root
# 기본 패키지 설치
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
ca-certificates \
gnupg \
lsb-release \
git \
software-properties-common \
sudo \
uidmap \
fuse-overlayfs \
slirp4netns && \
rm -rf /var/lib/apt/lists/*
# jenkins 계정에 sudo 권한 부여 (비밀번호 없이 가능하게)
RUN echo "jenkins ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/jenkins && \
chmod 440 /etc/sudoers.d/jenkins
# Podman 설치
RUN . /etc/os-release && \
echo "deb http://deb.debian.org/debian $VERSION_CODENAME-backports main" >> /etc/apt/sources.list && \
apt-get update && \
apt-get -t $VERSION_CODENAME-backports install -y podman podman-docker && \
rm -rf /var/lib/apt/lists/*
# Podman 기본 설정 (docker.io 검색 허용)
RUN mkdir -p /etc/containers && \
echo 'unqualified-search-registries = ["docker.io"]\n\n[[registry]]\nlocation = "192.168.56.200:5000"\ninsecure = true' > /etc/containers/registries.conf
# AWS CLI v2 설치
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" \
&& unzip awscliv2.zip \
&& ./aws/install \
&& rm -rf awscliv2.zip aws
# yq 설치
RUN curl -L https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -o /usr/local/bin/yq && \
chmod +x /usr/local/bin/yq
# Podman rootless 설정용 디렉터리 및 환경 변수 준비
ENV XDG_RUNTIME_DIR=/tmp/run
RUN mkdir -p /tmp/run && chmod 700 /tmp/run && chown jenkins:jenkins /tmp/run
# Jenkins 홈 내부 Podman 설정 디렉터리 준비
RUN mkdir -p /home/jenkins/.config/containers && chown -R jenkins:jenkins /home/jenkins/.config
USER jenkins
기존 파일에 AWS 로그인을 위한 aws cli를 설치하는 명령어가 추가되었다.
그럼 이 aws cli를 어디에 쓰냐구…?
젠킨스에서 이렇게
- AWS Credentials
- Amazon ECR
- Multibranch Scan Webhook Trigger 플러그인을 다운로드 받아준 후
내 계정의 AWS 액세스 키와 씨크릿 키를 넣은 크레덴셜 하나를 만들어 준다.
그리고 Pipeline 대신 Multibranch Pipeline이라는 아이템을 하나 생성해서
아이템 설정에서 Branch Sources 항목에 Git 소스를 추가하고
- Project Repository는 각 서비스의 깃허브 주소
- Credentials는 기존에 만들었던 깃허브 크리덴셜
- Behaviours에 Add-Filter by name(with wildcards)를 선택해 메인브랜취와 dev 브랜취 입력
잠시 깃허브로 돌아가, 웹훅을 새로 발급받아 준다.
이번에는 젠킨스 주소(당직은 ngrok으로 포워딩한) 뒤에 붙는 주소가
/multibranch-webhook-trigger/invoke?token=(임의의 문자) 이다.
이 (임의의 문자)를 기억해두었다,
아래에 Scan Multibranch Pipeline Triggers 항목에 Scan by webhook을 선택하고, 나오는 Trigger token에 (임의의 문자)를 입력하고 설정을 완료한다.
젠킨스파일에 쓸 개발/운영환경 레포지토리 주소를 담는 크리덴셜 변수도 만들어 준다.
개발(온프레미스)이면 독커허브 주소나 사설 레포지토리를(당직은 192.168.56.200:5000),
운영(AWS)이면 ECR 엔드포인트 주소(~~~.ecr.(지역코드).amazonzws.com)를 넣어 준다.
이렇게 서비스별로(…) 만들어 주면 각 아이템마다 dev, master(또는 main) 이렇게 파이프라인이 2개 생길 것이다.
(당직은 multibranch 폴더에서 10개 만들었습니다)
젠킨스파일과 배포
젠킨스 파일을 다음과 같이 수정해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#!/usr/bin/env groovy
def APP_NAME
def APP_VERSION
def DOCKER_IMAGE_NAME
def PROD_IMAGE_NAME
def HELM_VALUES
pipeline {
agent any
environment {
USER_EMAIL = '(내 깃허브 이메일)'
USER_ID = '(내 깃허브 아이디)'
REGISTRY_HOST = credentials('DEV_REGISTRY')
PROD_REGISTRY = credentials('PROD_REGISTRY')
SERVICE_NAME = '(서비스 이름: product, market 등...)'
}
tools {
gradle 'Gradle 8.14.2' // 젠킨스 Tools의 Gradle 이름
jdk 'OpenJDK 17' // 젠킨스 Tools의 JDK 이름
}
stages {
// master/main 브랜취시 aws ecr 연결
stage('Login into AWS When Master Branch') {
when {
expression { env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'main' }
}
steps {
script {
REGISTRY_HOST = PROD_REGISTRY
withCredentials([[
$class: 'AmazonWebServicesCredentialsBinding',
credentialsId: 'aws-credential',
accessKeyVariable: 'AWS_ACCESS_KEY_ID',
secretKeyVariable: 'AWS_SECRET_ACCESS_KEY'
]]) {
sh """
aws ecr get-login-password --region ap-northeast-2 \
| podman login --username AWS --password-stdin ${REGISTRY_HOST}
"""
}
}
}
}
stage('Set Version') {
steps {
script {
APP_NAME = sh (
script: "gradle -q getAppName",
returnStdout: true
).trim()
APP_VERSION = sh (
script: "gradle -q getAppVersion",
returnStdout: true
).trim()
DOCKER_IMAGE_NAME = "${REGISTRY_HOST}/${APP_NAME}:${APP_VERSION}"
sh "echo IMAGE_NAME is ${APP_NAME}"
sh "echo IMAGE_VERSION is ${APP_VERSION}"
sh "echo DOCKER_IMAGE_NAME is ${DOCKER_IMAGE_NAME}"
}
}
}
stage('Checkout Branch') {
steps {
// Git에서 해당 브랜취의 코드를 가져옵니다.
checkout scm
}
}
stage('Build Spring Boot App') {
steps {
// gradlew 권한부여
sh 'chmod +x gradlew'
// Gradlew로 빌드
sh './gradlew clean build'
}
}
stage('Image Build and Push to Registry') {
steps {
script {
// 이미지 빌드
sh "echo Image building..."
sh "podman build -t ${DOCKER_IMAGE_NAME} ."
// 레지스트리 푸쉬 - dev와 master 분리
if (env.BRANCH_NAME == 'dev'){
sh "echo Image pushing to local registry..."
sh "podman push ${DOCKER_IMAGE_NAME}"
}
// master/main 브랜취일시 ecr로 푸쉬
else if (env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'main'){
PROD_IMAGE_NAME = "${REGISTRY_HOST}/${APP_NAME}:${APP_VERSION}"
sh "echo Image pushing to prod registry..."
sh "podman tag ${DOCKER_IMAGE_NAME} ${PROD_IMAGE_NAME}"
sh "podman push ${PROD_IMAGE_NAME}"
}
// 로컬 이미지 제거
sh "podman rmi -f ${DOCKER_IMAGE_NAME} || true"
}
}
}
stage('Update Helm Values') {
steps{
script{
withCredentials([usernamePassword(
credentialsId:'github-credential',
usernameVariable: 'GIT_USERNAME',
passwordVariable: 'GIT_PASSWORD'
)]) {
def imageRepo = "${REGISTRY_HOST}/${APP_NAME}"
def imageTag = "${APP_VERSION}"
def MANIFEST_REPO = "https://${GIT_USERNAME}:${GIT_PASSWORD}@github.com/LG-CNS-2-FINAL-PROJECT-FINANCE/Backend_Manifests.git"
// master/main 브랜취용 매니페스트 분리
if (env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'main'){
HELM_VALUES = 'values-prod'
} else if (env.BRANCH_NAME == 'dev') {
HELM_VALUES = 'values-dev'
}
sh """
# Git 사용자 정보 설정(커밋 사용자 명시땜에)
git config --global user.email "${USER_EMAIL}"
git config --global user.name "${USER_ID}"
# 매니페스트 레포 클론
git clone ${MANIFEST_REPO}
cd Backend_Manifests
# yq를 사용하여 개발 환경의 values 파일 업데이트
yq -i '.image.repository = "${imageRepo}"' helm_chart/${SERVICE_NAME}/${HELM_VALUES}.yaml
yq -i '.image.tag = "${imageTag}"' helm_chart/${SERVICE_NAME}/${HELM_VALUES}.yaml
# 변경 사항 커밋 및 푸시
if ! git diff --quiet; then
git add helm_chart/${SERVICE_NAME}/${HELM_VALUES}.yaml
git commit -m "Update image tag for dev to ${DOCKER_IMAGE_NAME} [skip ci]"
git push origin master
else
echo "No changes to commit."
fi
"""
}
}
}
}
stage('Clean Workspace') {
steps {
deleteDir() // workspace 전체 정리
}
}
}
// 빌드 완료 후
post {
// 성공이든, 실패든 항상 수행
always {
echo "Cleaning up workspace..."
deleteDir() // workspace 전체 정리
echo "Cleaning up podman..."
sh "podman image prune -af || true" // podman 찌꺼기가 쌓여, 정리
sh "podman container prune -f || true" // podman 찌꺼기가 쌓여, 정리
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// master/main 브랜취시 aws ecr 연결
stage('Login into AWS When Master Branch') {
when {
expression { env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'main' }
}
steps {
script {
REGISTRY_HOST = PROD_REGISTRY
withCredentials([[
$class: 'AmazonWebServicesCredentialsBinding',
credentialsId: 'aws-credential',
accessKeyVariable: 'AWS_ACCESS_KEY_ID',
secretKeyVariable: 'AWS_SECRET_ACCESS_KEY'
]]) {
sh """
aws ecr get-login-password --region ap-northeast-2 \
| podman login --username AWS --password-stdin ${REGISTRY_HOST}
"""
}
}
}
}
아까 설치한 aws cli와 AWS 크리덴셜이 여기서 쓰이는 것이다.
젠킨스파일에서 마스터 브랜취가 대상일 때, aws cli와 크리덴셜을 사용해 ecr 레포에 로그인한다.
추가로 이전에 만든 헬름차트(라 읽고 배포용 yaml 레포지토리)의 values-dev.yaml에 더해 values-prod.yaml을 만들어 주고, AWS에 맞게 엔드포인트를 수정한다.
(db 주소를 rds 주소로 변경, 기타 mongo나 kafka 주소도 ec2에 설치한 서버 IP로 바꾸는 등… 이건 나중에 다룰게요)
왜냐면
1
2
3
4
5
6
// master/main 브랜취용 매니페스트 분리
if (env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'main'){
HELM_VALUES = 'values-prod'
} else if (env.BRANCH_NAME == 'dev') {
HELM_VALUES = 'values-dev'
}
이렇게 젠킨스 파일 내에서 개발/운영환경별로 분기가 생겼기 때문이다.
아무튼 젠킨스파일을 이렇게 바꾸고 메인 브랜취에 푸쉬(머지)하면…
이렇게 웹훅이 작동해 prod 기반 젠킨스 파이프라인이 돌고, ECR에 이미지가 추가된다!









