CI/CD 파이프라인 구축에 앞서 일단 지금까지 개발한 코드가 정상적으로 빌드되는지 테스트를 하기로 하였다.
이전에 진행했던 백엔드 코드를 도커라이징한 과정을 그대로 실행하였다.
📒 인프라 / Devops 엔지니어는 고객이 개발자인 만큼 개발자와의 소통도 중요한 요소 중 하나인 것 같다.
최신버전의 디렉터리를 빌드하였고 jar파일을 실행시켜보니 아래와 같이 인스턴스의 퍼블릭 ip로 접속이 되는 것을 확인하였다.
문제는 빌드과정에서 데이터베이스 연결이 안되는 경고가 나타나였고, 실제로 jar파일을 실행하는 과정에서 쿼리가 안되는 경고가 발생했다.
💡 백엔드 코드에서는 데이터베이스와 연결 설정을 하면되는 것이고, 프론트엔드 코드에서 백엔드와 연결 설정을 하면 되는 것이다. 이는 파이프라인을 작성한 이후에 3-tier 연동과정에서 진행할 예정이다.
Jenkins를 이용해 파이프라인을 구축할 것이다.
1 백엔드 코드 수정 및 변경
개발자분의 백엔드코드를 내 git으로 fork해서 가져온 뒤 workDIR에 clone을 통해 remote로 연결하였다. 이후 정상적으로 PUSH가 되는지 테스트하였다.
개발 코드를 디렉터리별로 관리하고 git을 등록할때도 그냥 전체 git주소를 등록하면 된다. 다만 아래와 같이 Pipeline코드를 짤때 빌드하길 원하는 git의 디렉터리를 지정해주면 된다.
dir('backend/spring'){ # 특정 디렉터리 지정 sh 'gradle wrapper --gradle-version 8.0.2' }
2 Jenkinsfile 작성
Jenkinsfile 작성에 앞서 플러그인관리에 들어가 Gradle Plugin을 설치해준다. 이후 글로벌 도구관리에 들어가서 다음과 같이 설정해준다. 버전은 본인이 사용하고 있는 gradle 버전으로 진행해주면 된다.
pipeline{
agent{
kubernetes{
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: gradle
image: gradle:8.0.2-jdk11
command: ['sleep']
args: ['infinity']
- name: kaniko
image: gcr.io/kaniko-project/executor:debug
command: ['sleep']
args: ['infinity']
volumeMounts:
- name: registry-credentials
mountPath: /kaniko/.docker
volumes:
- name: registry-credentials
secret:
secretName: regcred
items:
- key: .dockerconfigjson
path: config.json
'''
}
}
stages{
stage('checkout'){
steps{
container('gradle'){
git branch: 'main', url:'https://github.com/suhwan12/GoormUniversity-course-registration.git'
} # 루트 디렉터리에 다 모아놓고 git주소를 등록
# 루트 디렉터리에 모아놓지 않아도 dir로 빌드하길 원하는 디렉터리 지정 가능
}
}
stage('install gradlew'){ # gradlew 설치
steps{
container('gradle'){
sh 'gradle wrapper --gradle-version 8.0.2'
}
}
}
stage('gradle build project'){ # build와 libs 디렉터리 생성 및 jar파일 생성
steps{
container('gradle'){
sh './gradlew build'
}
}
}
stage('Build & Tag dokcer image'){ # Dockerfile 빌드
steps{
container('kaniko'){
sh "executor --dockerfile=Dockerfile \
--context=dir://${env.WORKSPACE} \
--destination=suhwan11/backend:latest \
--destination=suhwan11/backend:${env.BUILD_NUMBER}"
}
}
}
stage('Update K8s to New Deployment'){ # 새로운 이미지로 교체
steps{
container('gradle'){
git branch: 'main' , url:'https://github.com/suhwan12/finalproject-argocd.git'
sh 'sed -i "s/image:.*/image: suhwan11\\/backend:${BUILD_NUMBER}/g" back-deployment.yaml'
sh 'git config --global user.name suhwan12'
sh 'git config --global user.email xman0120@naver.com'
sh 'git config --global --add safe.directory /home/jenkins/agent/workspace/suhwan-test-pipe'
sh 'git add back-deployment.yaml'
sh 'git commit -m "Jenkins Build Number - ${BUILD_NUMBER}"'
withCredentials([gitUsernamePassword(credentialsId: 'github-credentials', gitToolName: 'Default')]) {
sh 'git push origin main'
}
}
}
}
}
}
Dockerfile을 빌드하기 위하여 Kaniko를 사용하였는데, Kaniko를 사용하여 빌드하고 태깅하여 Docker Hub에 Push하려면 Docker Hub에 대한 권한이 있어야 하기 때문에 Docker Hub에서 토큰을 생성해서 토큰을 이용하여 secret을 만들어준다.
생성한 secret을 볼륨으로 마운트한 컨테이너를 파드에 추가한다. Agent는 container로 kaniko가 지정되면 파드에서 임시로 kaniko컨테이너를 생성하고 해당 컨테이너 환경에서 작업을 수행한다.
-> 컨트롤러가 직접 작업을 하는 것이 아니라 파드에서 해당 작업에 필요한 환경을 갖춘 컨테이너를 임시적으로 띄워서 수행하기 때문에 항상 인스턴스를 띄울 필요가 없는 Jenkins에서의 노드 개념이다.
= 컴퓨팅 리소스 자원과 비용을 아낄 수 있다.
-> 또한 필요한 환경을 갖추었기 때문에 작업을 문제없이 수행한다.
마지막 stage에서 sed명령어를 통해 Docker Hub에 Push된 이미지를 ArgoCD와 연동중인 git에서 배포하고 있는 back-deployment의 image와 교체한다.
-> 이미지를 교체할 yaml파일을 정확하게 입력해야한다.
-> git에서 발급받은 토큰으로 Jenkins에서 credential을 생성하고 이것을 이용해 Jenkins가 Git에 접근할 권한을 가지게 한다. 이를 통해 push가 가능해진다.
빌드하는 과정에서 계속 오류가 발생하였는데 안전하지 않은 디렉터리라는 오류가 발생하였다. 따라서 아래와 같이 프로젝트 경로의 디렉터리에 안전한 디렉터리인것을 명시했다.
sh 'git config --global --add safe.directory /home/jenkins/agent/workspace/suhwan-test-pipe'
3 Dockerfile 작성
FROM openjdk:11-jre
WORKDIR /usr/src/
COPY . /usr/src/
CMD ["java","-jar","/usr/src/build/libs/api-0.0.1-SNAPSHOT.jar"]
처음에는 FROM으로 java 이미지 가져오고 바로 CMD로 jar파일을 실행시키도록 작성하였다.
4 결과 테스트
Jenkins에서 새로운 item을 생성해서 Pipeline script from SCM을 선택하고 git주소를 등록하고 main으로 변경하였다. 위에서 생성한 Jenkinsfile을 등록하였다.
-> 수동으로 빌드하지않아도 Jenkins에서 주기적으로 git에 변경사항이 없는지 모니터링하는 poll SCM은 생략하였다.
-> 본격적으로 개발 완료된 코드에 대해서는 파이프라인을 구축할 때 poll SCM을 적용시켜 개발자 코드의 변경사항에 대해서도 자동화를 구성할 것이지만, 현재는 테스트기 때문에 수동으로 빌드하는 것이 더 편하다.
DockerHub에서도 빌드 순서가 tag되어 생성된 이미지가 Push되어 있는 것을 확인할 수 있고, ArgoCD와 연동중인 git에서도 comment와 함께 새로 Push가 된 것을 볼 수 있다. 새로 Push된 yaml파일을 들어가보면 image가 자동으로 DockerHub에 새로 Push된 이미지로 교체되어 있는 것을 확인
ArgoCD가 자신과 연동중인 git에 변경사항이 생긴것을 감지하고 새로 교체된 이미지가 적용된 컨테이너를 가진 파드를 deployment가 새로운 replicaset을 만들어 Rolling Update로 배포되는 것을 확인
백엔드 Deployment를 외부에 노출시키고 있는 백엔드 Ingress( = ALB)의 Dns Name을 브라우저에 입력하면 정상적으로 웹페이지가 나타나는 것을 확인
-> 정상적으로 백엔드 서버가 배포 되었다는 것.
Jenkins에서 NodeJS 플러그인을 설치해준다.
Frontend인 경우는 User페이지와 Admin페이지가 구분되어있다. 개발자분이 페이지를 나누어서 개발중이다.
따라서 파이프라인을 구축해 빌드하는 것도 나누어서 빌드할 것이다.
따라서 Admin Frontend 와 User Frontend에 대해 각각 Deployment와 service를 추가할것이다. 이때 User Deployment에는 User 웹페이지를 나타내는 이미지, Admin Deployment에는 Admin 웹페이지를 나타내는 이미지를 구분하여 적용시킬 것이다.
이후 각각의 service를 동일한 ingress에 연결시킬것이고, 다만 ingress의 prefix 설정을 통해 도메인/manager을 입력하면 Admin 페이지를 띄우는 파드에 연결되어있는 서비스로 연결할 것이고, 사용자가 도메인 만을 입력하면 User 페이지를 띄우는 파드에 연결되어있는 서비스로 연결되게 할 것이다.
-> admin은 이미 내부에서 사용중인 api 주소이기 때문에 다른 이름을 사용해서 페이지를 구분할 것이다.
-> 도메인을 통해서 User와 Admin을 구분하는 것은 사용자가 쉽게 Admin페이지에 접속할 수 있기 때문에 User가 Admin페이지에 접속할 수 없게 만드는 보안장치도 구현해야 할 것이다.
이 방법은 실패하여 결국 도메인을 2개 사서 각각 연결하는 방식을 택하였다. 추후에 위 방법을 사용하여 개발하는 방법도 진행해볼 예정이다.
1 프론트엔드용 인스턴스 생성 및 git clone
하는 방식의 기존에 진행했던 방식과 같다.
2 기본적인 빌드를 위한 툴 세팅
프론트엔드는 vue와 엔진엑스를 이용해서 실행시킬 것이다.
sudo apt update
# nginx 설치
sudo apt install -y nginx
# nodjs 설치, npm도 같이 설치되기 떄문에 추가로 설치할 필요X
sudo apt-get install -y nodejs
cd 프로젝트_repo_주소
npm install
3 Dockerfile 작성
프론트엔드 코드를 도커파일을 통해서 빌드하여 이미지화 시킬 것이다.
FROM node:lts-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:stable-alpine
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
vi nginx.conf
server {
listen 80;
client_max_body_size 5M;
server_name _;
location /api {
proxy_pass http://<백엔드 주소>;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
location / {
root /usr/share/nginx/html/;
index index.html;
error_page 405 =200 $uri;
try_files $uri $uri/ /index.html;
}
}
4 Docker 이미지 생성 및 컨테이너 실행
docker build -t frontend:test1 .
docker run -it --rm -p 8080:80 frontend:test1
이후 인스턴스의 퍼블릭IP:8080 주소로 접속하면 프론트에 구성한 화면을 볼 수 있다.
5 ArgoCD로 배포
정상적으로 페이지가 나타나는 것을 확인하였으니, Docker Hub에 Push한다. ArgoCD가 배포중인 Frontend-Deployment의 image를 위에서 Push한 image로 교체한다.
브라우저에 [ https://도메인 ] 을 입력했을 때 정상적으로 로그인 페이지가 나타난다.
의문인점은 위에서 컨테이너를 실행시켜서 브라우저에 인스턴스의 퍼블릭IP:8080를 입력했을 때는 정상적으로 로그인페이지가 뜨지만, 8080포트가 아닌 80으로 접속했을 때는 그냥 nginx페이지가 뜬다.
하지만 ArgoCD에 배포하기 위해 해당 image로 교체하고 Container Port로 80포트를 뚫어놓은 상태에서 브라우저에 도메인을 입력하면 로그인페이지가 뜬다. 즉 8080으로 Frontend파드에 접근하지 않았음에도 로그인페이지가 뜬 것이다.
내가 생각했을때는 컨테이너에 현재 nginx가 띄어져 있고 안에 vue 실행파일이 존재하는 형태이기 때문에 nginx 컨테이너에 접속했을때 포트포워딩을 굳이 하지 않아도 안에 있는 dist 파일을 실행시켜주는 것이라고 생각한다.