저번 프로젝트 때 Jenkins pipeline을 쉘 접속을 활용했던 게 마음에 안 들어서
이번 프로젝트에서는 꼭 DooD를 구현하겠다 마음먹고 다시 구현해봤다.
다른 방식으로 구현하면서 깨달은 점이 있다면
1. .env파일은 프로덕션 환경에서 만들거나 관리할 필요가 없다.
전부 jenkins credential에 만들어두고 compose up 할 때 환경 변수를 withcredentials 이용하여 주입해주기만 하면 되는 것이었다...!
이 사실을 알고 나서 빌드 프로세스의 이해가 쉬워지고, 구현이 간편해졌다.
- 그럼 jenkins credential이 무엇이냐?: 환경 변수나 계정 정보와 같은 민감한 정보를 관리해주는 역할을 한다.
2. scm checkout하면 jenkins 아래에 jenkins_workspace가 생기고, 그곳에 소스코드가 clone된다.
호스트 ec2에 있는 소스코드와는 전혀 관계가 없다... 헐
그리고 파이프라인 끝에 post action으로 clean해주면 소스코드와 환경변수가 정리되어 ec2에는 코드가 남아있지 않게 된다. 보안상 정말 유용한 방식인듯
폴더 구조
/home/ubuntu 안에
~/opt (호스트의 소스코드)
~/jenkins
~/jenkins_workspace
~/Dockerfile
~/jenkins-docker-compose.yml
구조로 구성했다.
1. jenkins-docker-compose.yml
services:
jenkins:
build:
context: .
dockerfile: Dockerfile
container_name: jenkins
environment:
JENKINS_JAVA_OPTIONS: >-
-Dhudson.model.DirectoryBrowserSupport.CSP="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;" -Dhttp.nonProxyHosts="lab.ssafy.com|updates.jenkins.io" -Dhttps.nonProxyHosts="lab.ssafy.com|updates.jenkins.io"
NO_PROXY: "lab.ssafy.com,updates.jenkins.io"
JENKINS_OPTS: "--prefix=/jenkins"
ports:
- "11027:8080"
volumes:
- ./jenkins:/var/jenkins_home
- /var/jenkins_home/workspace:/var/jenkins_home/workspace
- /home/ubuntu/opt/build:/app/build
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/bin/docker
- /usr/local/bin/docker-compose:/usr/local/bin/docker-compose:ro
networks:
- see_through
networks:
see_through:
external: true
1) 볼륨 마운트
- ./jenkins:/var/jenkins_home
- /var/jenkins_home/workspace:/var/jenkins_home/workspace
jenkins 관련 정적 파일들을 보관할 jenkins_home과
scm checkout할 시 생성하는 워크스페이스를 호스트와 공유하여 사라지지 않도록 한다.
- /home/ubuntu/opt/build:/app/build
jenkins workspace에서 빌드한 파일들을 호스트 os와 공유 (그래야 nginx에서 받아 쓸 수 있음)
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/bin/docker
- /usr/local/bin/docker-compose:/usr/local/bin/docker-compose:ro
docker와 docker-compose를 jenkins와 마운트한다.
마운트하지 않으면 ec2 호스트에 설치된 docker에 접근할 수 없어 DooD 구현이 불가능하다.
2) 네트워크 설정
networks:
- mynetwork
jenkins 컨테이너의 네트워크를 설정한다 (이름은 자유)
networks:
mynetwork:
external: true
전역 네트워크 설정으로, external을 허용해주어야 호스트에서의 mynetwork 네트워크가 jenkins 컨테이너에서도 접근이 가능하다.
3) environment 설정
- HTTPS 연결을 적용하면서 필요한 환경변수를 몇 개 설정했다.
일단 Git 연결은 프록시를 타면 안 돼서 no_proxy에 추가하고,
CSP 헤더 설정의 경우 일부 CSS/JS 리소스가 https 사용 시 보안 정책으로 제대로 로드되지 않아
해당 부분을 허용해주었다.
2. Dockerfile
FROM jenkins/jenkins:lts
USER root
RUN groupadd -g 999 docker || true && \
usermod -aG docker jenkins
USER jenkins
유저 jenkins를 docker group에 추가하는 과정이다.
이 과정이 없으면 호스트의 docker에 접근하기 불가능해져서 추가로 설정했다.
3. Jenkinsfile
Jenkins는 파이프라인 방식으로 만들었다.
credential은 global에 전부 추가했고...
git 연결 후 (만약 이 연결 부분이 뜨지 않으면 plugins에서 Git관련 플러그인을 설치 후 재시작하면 된다) 작성한다.
pipeline {
agent any
stages {
stage('Checkout') {
// steps {
// checkout scm
// }
steps {
git branch: 'dev', credentialsId: 'gitlab-access-token', url: 'https://lab.ssafy.com/s12-s-project/S12P21S002.git'
}
}
stage('Deploy') {
steps {
withCredentials([
string(credentialsId: 'DB_URL', variable: 'DB_URL'),
string(credentialsId: 'DB_USERNAME', variable: 'DB_USERNAME'),
string(credentialsId: 'DB_PASSWORD', variable: 'DB_PASSWORD'),
string(credentialsId: 'LLM_URL', variable: 'LLM_URL'),
string(credentialsId: 'NICKNAME_URL', variable: 'NICKNAME_URL'),
string(credentialsId: 'SWAGGER_USERNAME', variable: 'SWAGGER_USERNAME'),
string(credentialsId: 'SWAGGER_PASSWORD', variable: 'SWAGGER_PASSWORD'),
string(credentialsId: 'ALARM_UUID', variable: 'ALARM_UUID'),
string(credentialsId: 'VITE_API_SERVER_URL', variable: 'VITE_API_SERVER_URL'),
string(credentialsId: 'VITE_LOCAL_SERVER_URL', variable: 'VITE_LOCAL_SERVER_URL'),
string(credentialsId: 'VITE_WS_LOCAL_SERVER_URL', variable: 'VITE_WS_LOCAL_SERVER_URL'),
string(credentialsId: 'VITE_TYPECAST_API_KEY', variable: 'VITE_TYPECAST_API_KEY'),
string(credentialsId: 'OPENAI_API_KEY', variable: 'OPENAI_API_KEY'),
string(credentialsId: 'LLM_DB_URL', variable: 'LLM_DB_URL'),
string(credentialsId: 'DB_NAME', variable: 'DB_NAME'),
string(credentialsId: 'TYPECAST_API_KEY', variable: 'TYPECAST_API_KEY'),
string(credentialsId: 'TYPECAST_BASE_URL', variable: 'TYPECAST_BASE_URL'),
file(credentialsId: 'FCM_TOKEN', variable: 'FCM_TOKEN')
]) {
sh '''
mkdir -p api/src/main/resources/firebase
cp $FCM_TOKEN api/src/main/resources/firebase/seethrough-cf145-firebase-adminsdk-fbsvc-05904b7ab9.json
'''
sh "docker-compose down api client llm --volumes"
sh "docker-compose up -d api client"
sh "docker-compose up -d --build llm"
sleep time: 90, unit: 'SECONDS'
}
}
}
}
post {
always {
cleanWs()
}
}
}
checkout
- scm 해도 되는데 나는... 그냥 직접 연결했다.
deploy
- withcredentials로 환경변수를 전부 불러온다. 그리고 이걸 docker-compose를 이용하여 주입시킨다.
- compose는 아래에..
- 이후 쉘에서 필요한 컨테이너를 compose를 이용하여 재실행한다.
- 참고로 ec2에서 실행한 컨테이너와 jenkins가 실행한 컨테이너는 같은 이름을 가져도 다른 컨테이너 취급을 하므로 만약 중복 오류가 발생하면 호스트에서 해당 컨테이너를 docker remove를 이용하여 삭제해주면 된다(예: docker remove client)
- sleep울 90초 설정해뒀는데 그 이유가.. 컨테이너를 -d 옵션 써서 백그라운드로 실행하니까 얘가 소스 다 읽어서 빌드하기도 전에 post action을 실행하며 워크스페이스를 정리해버려서 설정해뒀다. 다른 좋은 방법 없나...
post
- workspace를 정리하는 cleanWs()를 항상(always) 실행한다.
완성
이 정도 하면 이제 jenkins가 컨테이너를 새로 올려준다. 만약 호스트에서 올린 컨테이너가 있다면 내려줘야 중복을 피할 수 있다. 호스트와 젠킨스는 다른 유저로 분리돼서 서로 다른 놈이 올린 컨테이너는 내려주지 못하는듯.