Blue / Green 전략으로 무중단 배포 하기
- Docker
- Nginx
- Jenkins
- Spring
- Intellij
Docker 설치
docker 와 docker compose 설치를 먼저 해야한다.
다음 링크를 확인하여 기준에 맞게 설치하자
Nginx 설치
아래 링크를 확인하여 설치 및 설정
Docker compose 작성
blue, green 배포 전략이기 때문에 yml 은 2개가 필요하다
docker-compose.blue.yml
sudo vi docker-compose.blue.yml
version: '3.1'
services:
api:
image: ${IMAGE_STORAGE}/${IMAGE_NAME}:${BUILD_NUMBER}
container_name: ${IMAGE_NAME}-blue
environment:
- LANG=ko_KR.UTF-8
- UWSGI_PORT=8080
- TZ=Asia/Seoul
ports:
- '8080:8080'
networks:
default:
external:
name: service-network
docker-compose.green.yml
sudo vi docker-compose.green.yml
version: '3.1'
services:
api:
image: ${IMAGE_STORAGE}/${IMAGE_NAME}:${BUILD_NUMBER}
container_name: ${IMAGE_NAME}-green
environment:
- LANG=ko_KR.UTF-8
- UWSGI_PORT=8081
- TZ=Asia/Seoul
ports:
- '8081:8080'
networks:
default:
external:
name: service-network
포워딩은 아래와 같다
- blue: 8080
- green: 8081
IMAGE_STORAGE
, IMAGE_NAME
, BUILD_NUMBER
3개의 변수는 Jenkins
에서 환경변수로 지정할 예정
두 컨테이너에서 사용할 docker network도 생성
docker network create service-network
배포 스크립트 작성
sudo vi deploy.sh
{user}
계정명 경로 사용
예) /home/ec2-user
/etc/nginx/sites-available/domain.com.conf
내용 수정 필요
# 1
EXIST_BLUE=$(docker-compose -p ${IMAGE_NAME}-blue -f docker-compose.blue.yml ps | grep Up)
if [ -z "$EXIST_BLUE" ]; then
docker-compose -p ${IMAGE_NAME}-blue -f /home/{user}/docker-compose.blue.yml up -d
BEFORE_COMPOSE_COLOR="green"
AFTER_COMPOSE_COLOR="blue"
BEFORE_PORT_NUMBER=8081
AFTER_PORT_NUMBER=8080
else
docker-compose -p ${IMAGE_NAME}-green -f /home/{user}/docker-compose.green.yml up -d
BEFORE_COMPOSE_COLOR="blue"
AFTER_COMPOSE_COLOR="green"
BEFORE_PORT_NUMBER=8080
AFTER_PORT_NUMBER=8081
fi
echo -e "\n${AFTER_COMPOSE_COLOR} server up(port:${AFTER_PORT_NUMBER})"
# 2
for cnt in {1..10}
do
echo "서버 응답 확인중..(${cnt}/10)";
UP=$(curl -s http://localhost:${AFTER_PORT_NUMBER}/actuator/health | grep 'UP')
if [ -z "${UP}" ]
then
sleep 10
continue
else
break
fi
done
if [ $cnt -eq 10 ]
then
echo "서버가 정상적으로 구동되지 않았습니다."
exit 1
fi
# 3
sudo sed -i "s/${BEFORE_PORT_NUMBER}/${AFTER_PORT_NUMBER}/" /etc/nginx/sites-available/domain.com.conf
sudo nginx -s reload
echo "Deploy Completed!!"
# 4
echo "$BEFORE_COMPOSE_COLOR server down(port:${BEFORE_PORT_NUMBER})"
docker-compose -p ${IMAGE_NAME}-${BEFORE_COMPOSE_COLOR} -f docker-compose.${BEFORE_COMPOSE_COLOR}.yml down
# 5
echo "y" | docker container prune
# 6
echo "y" | docker image prune
# 7
echo "y" | docker volume prune
# 8
echo "y" | docker system prune -a
- 2 번 과정에서
actuator/health
는Spring
에서health check
하기 위한 용도로 사용한actuator
라이브러리이며
타 언어나 프레임워크마다health check
하는 방법으로 대체하면 됨 - 2 번 과정에서
localhost
는ubuntu
인 경우127.0.0.1
로 해야함 - 3 번 과정에서
sed -i "" 파일명
을 하는 것은 포트를 변환하기 위한 작업 - 3 번 과정에서 포트변환이 일어났기 때문에 방화벽에서 8081 가 막혀서 실행이 안된다면 추가해주자
Naver Cloud Platform 에서는 ACG 설정 하면된다 - 4 번 과정에서 새로 실행한 컨테이너가 정상인 것을 확인 하였기 때문에 기존에 사용하던 컨테이너를 중단시킨다
- 5 ~ 8 번 과정에서 미사용하는 데이터를 삭제한다
만약 latest 로만 처리하려면 항상 새로 pull 받기 위해 아래 처럼 1번 과정을 수정한다
# 1
EXIST_BLUE=$(docker-compose -p ${IMAGE_NAME}-blue -f docker-compose.blue.yml ps | grep Up)
if [ -z "$EXIST_BLUE" ]; then
docker-compose -f /home/{user}/docker-compose.blue.yml pull && docker-compose -p ${IMAGE_NAME}-blue -f /home/{user}/docker-compose.blue.yml up -d
BEFORE_COMPOSE_COLOR="green"
AFTER_COMPOSE_COLOR="blue"
BEFORE_PORT_NUMBER=8081
AFTER_PORT_NUMBER=8080
else
docker-compose -f /home/{user}/docker-compose.blue.yml pull && docker-compose -p ${IMAGE_NAME}-green -f /home/{user}/docker-compose.green.yml up -d
BEFORE_COMPOSE_COLOR="blue"
AFTER_COMPOSE_COLOR="green"
BEFORE_PORT_NUMBER=8080
AFTER_PORT_NUMBER=8081
fi
echo -e "\n${AFTER_COMPOSE_COLOR} server up(port:${AFTER_PORT_NUMBER})"
docker-compose -f /home/{user}/docker-compose.blue.yml pull &&
앞의 명령어를 추가
Container Registry
에서 이미지를 pull
받으려면 docker
에 로그인 필요
docker login {Container Registry 주소}
Naver Cloud Platform 에서 사용하는 경우 로그인 ID, PW 는 root 계정의 API public key, secret key 가 순서대로 ID, PW 이다
배포 수동 테스트
수동 테스트를 위한 *.jar
또는 *.war
파일을 서버에 옮기고 (파일질라 등을 이용)Dockerfile
을 아래처럼 작성
FROM amazoncorretto:11-alpine-jdk
EXPOSE 8080
COPY *.war app.jar
CMD java -jar app.jar
추후 프로젝트 내에서 파일을 추가할 때 COPY
는 다음 처럼 수정
FROM amazoncorretto:11-alpine-jdk
EXPOSE 8080
COPY ./build/lib/*.war 프로젝트명.war
CMD java -jar 프로젝트명.war
war 말고 jar 면 jar 확장자로 수정하고 jdk 버전은 프로젝트에서 호환되는 것에 맞게 지정
만약 프로젝트에서 spring profile
도 구분하거나 spring cloud config
를 사용하는 경우 아래처럼 환경변수를 모두 보내주어야함
아래 내용은 프로젝트 내에서 Dockerfile
예시
FROM amazoncorretto:11-alpine-jdk
EXPOSE 8080
ARG SPRING_PROFILES_ACTIVE
ARG CONFIG_SERVER_ENCRYPT_KEY
ARG CONFIG_SERVER_USERNAME
ARG CONFIG_SERVER_PASSWORD
ENV SPRING_PROFILES_ACTIVE=$SPRING_PROFILES_ACTIVE
ENV CONFIG_SERVER_ENCRYPT_KEY=$CONFIG_SERVER_ENCRYPT_KEY
ENV CONFIG_SERVER_USERNAME=$CONFIG_SERVER_USERNAME
ENV CONFIG_SERVER_PASSWORD=$CONFIG_SERVER_PASSWORD
COPY ./build/libs/*.war 프로젝트명.war
CMD java -jar ./프로젝트명.war
다시 서버로 돌아가서 이미지 파일을 build 하고 Registry 로 push 한다.
build 단계에서 Dockerfile 과 COPY 하는 경로는 동일해야한다.
docker build -t {Container Registry 주소}/{Image Name} .
// docker build -t {Container Registry 주소}/{Image Name} --build-arg SPRING_PROFILES_ACTIVE=dev .
docker push {Container Registry 주소}/{Image Name}
2번째 주석은 build 할 때 이미지에서 환경변수를 사용해야하는 경우에는 직접 매개변수를 넘겨줘야 한다.
수동 테스트 시에 필요한 이미지는 외부에서 직접 보내주는게 아니기 때문에 (로컬 파일에서 부르는 것도 아님) build 할 때 변수를 직접 넘겨줘야한다.
# 환경변수 등록
export IMAGE_STORAGE={Container Registry 주소};
export IMAGE_NAME={Image Name};
export BUILD_NUMBER={Image Tag};
# 읽기, 실행 권한 부여
sudo chmod 755 deploy.sh
sudo chmod 755 docker-compose.blue.yml
sudo chmod 755 docker-compose.green.yml
# 스크립트 실행
./deploy.sh
deploy.sh
를 실행할 때 같은 경로에 Dockerfile
, *.war
2개가 같이 있어야한다.
테스트 이후에는 2개의 파일(Dockerfile
, *.war
)을 삭제해도 됨
이 글을 읽고 또 구축하는 단계에서 수동 테스트가 제대로 되지 않은 경우 꼭 여기에 업데이트 글을 넣자 !!! 까먹지말라 !!
나중에 Jenkins 에서 이용할 환경변수로 사용될 것이기 때문에 프로필 파일에 추가해둔다
sudo vi /home/{user}/.bashrc
만약 .bash_profile
이 있다면 거기에 해도 무방
파일을 수정권한을 주고 최하단에 아래와 같이 작성
export IMAGE_STORAGE={Container Registry 주소}
export IMAGE_NAME={Image Name}
export BUILD_NUMBER={Image Tag}
파일을 저장하고 나와서 다음을 실행하여 업데이트 한다
sudo source /home/{user}/.bashrc
Jenkins 파이프라인 구축
(업데이트 2023-07-14)
AWS ECR + Jenkins CI CD 구축은 여기로
- 소스 Push
- Jenkins Webhook
- Application Test, Build
- Docker image Build
- Docker image Push
- ssh 배포할 서버에 ssh로 쉘 명령어 실행(./deploy.sh)
Jenkinsfile 작성
아래 파이프라인에는 slack notification webhook
도 추가되어있다.
pipeline {
agent any
tools {
jdk("java-11-amazon-corretto")
}
environment {
SLACK_CHANNEL = "#채널명"
SLACK_SUCCESS_COLOR = "#2C953C";
SLACK_FAIL_COLOR = "#FF3232";
}
stages {
stage('Set Environment') {
post {
success {
slackSend(channel: SLACK_CHANNEL, color: SLACK_SUCCESS_COLOR, message: "==================================================================\n배포 파이프라인이 시작되었습니다.\n${JOB_NAME}(${BUILD_NUMBER})\n${GIT_COMMIT_AUTHOR} - ${GIT_COMMIT_MESSAGE}\n${BUILD_URL}")
}
}
steps {
script {
switch(BRANCH_NAME) {
case '브랜치명-dev' :
env.IMAGE_NAME = '프로젝트명'
env.IMAGE_STORAGE = '서버에 전달할 Container Registry 주소'
env.IMAGE_STORAGE_CREDENTIAL = 'Jenkins 에서 사용할 로그인 정보 파일명'
env.SSH_CONNECTION = 'SSH 접속정보 예) ncloud@123.112.121.221'
env.SSH_CONNECTION_CREDENTIAL = 'Jenkins 에서 사용할 SSH 로그인 정보 파일명'
break
//case '브랜치명-prod' :
// env.IMAGE_NAME = '프로젝트명'
// env.IMAGE_STORAGE = '서버에 전달할 Container Registry 주소'
// env.IMAGE_STORAGE_CREDENTIAL = 'Jenkins 에서 사용할 로그인 정보 파일명'
// env.SSH_CONNECTION = 'SSH 접속정보 예) ncloud@123.112.121.222'
// env.SSH_CONNECTION_CREDENTIAL = 'Jenkins 에서 사용할 SSH 로그인 정보 파일명'
// break
}
GIT_COMMIT_AUTHOR = sh(script: "git --no-pager show -s --format=%an ${GIT_COMMIT}", returnStdout: true).trim();
GIT_COMMIT_MESSAGE = sh(script: "git --no-pager show -s --format=%B ${GIT_COMMIT}", returnStdout: true).trim();
}
}
}
stage('Clean Build Test') {
post {
success {
slackSend(channel: SLACK_CHANNEL, color: SLACK_SUCCESS_COLOR, message: 'Build Test에 성공하였습니다.')
}
failure {
slackSend(channel: SLACK_CHANNEL, color: SLACK_FAIL_COLOR, message: 'Build Test에 실패하였습니다.\n==================================================================')
}
}
steps {
sh "SPRING_PROFILES_ACTIVE=${BRANCH_NAME} ./gradlew clean build test"
// 아래는 멀티모듈 환경인 경우
// sh "SPRING_PROFILES_ACTIVE=${BRANCH_NAME} ./gradlew :app-api:clean :app-api:build :app-api:test"
}
}
stage('Build Container Image') {
post {
success {
slackSend(channel: SLACK_CHANNEL, color: SLACK_SUCCESS_COLOR, message: 'Container Image Build에 성공하였습니다.')
}
failure {
slackSend(channel: SLACK_CHANNEL, color: SLACK_FAIL_COLOR, message: 'Container Image Build에 실패하였습니다.\n==================================================================')
}
}
steps {
script {
image = docker.build("${IMAGE_STORAGE}/${IMAGE_NAME}", ".")
// SPRING_PROFILES_ACTIVE - spring 프로필을 나누는 경우 사용
// CONFIG_SERVER_* - spring cloud config 사용하는 경우 사용
// image = docker.build("${IMAGE_STORAGE}/${IMAGE_NAME}", "--build-arg SPRING_PROFILES_ACTIVE=${BRANCH_NAME} --build-arg CONFIG_SERVER_ENCRYPT_KEY=${CONFIG_SERVER_ENCRYPT_KEY} --build-arg CONFIG_SERVER_USERNAME=${CONFIG_SERVER_USERNAME} --build-arg CONFIG_SERVER_PASSWORD=${CONFIG_SERVER_PASSWORD} .")
// 멀티 모듈인 경우 프로젝트명 기준으로 Jenkins 파일 위치를 지정
// image = docker.build("${IMAGE_STORAGE}/${IMAGE_NAME}", "./app-api")
}
}
}
stage('Push Container Image') {
post {
success {
slackSend(channel: SLACK_CHANNEL, color: SLACK_SUCCESS_COLOR, message: 'Container Image Push에 성공하였습니다.')
}
failure {
slackSend(channel: SLACK_CHANNEL, color: SLACK_FAIL_COLOR, message: 'Container Image Push에 실패하였습니다.\n==================================================================')
}
}
steps {
script {
docker.withRegistry("https://${IMAGE_STORAGE}", IMAGE_STORAGE_CREDENTIAL) {
image.push("${BUILD_NUMBER}")
image.push("latest")
}
}
}
}
stage('Remove Container Image') {
steps {
sh "docker image rm ${IMAGE_STORAGE}/${IMAGE_NAME}:latest"
sh "docker image rm ${IMAGE_STORAGE}/${IMAGE_NAME}:${BUILD_NUMBER}"
}
}
stage('Server Run') {
post {
success {
slackSend(channel: SLACK_CHANNEL, color: SLACK_SUCCESS_COLOR, message: '배포에 성공하였습니다.\n==================================================================')
}
failure {
slackSend(channel: SLACK_CHANNEL, color: SLACK_FAIL_COLOR, message: '배포에 실패하였습니다.\n==================================================================')
}
}
steps {
sshagent(credentials: [SSH_CONNECTION_CREDENTIAL]) {
sh "ssh -o StrictHostKeyChecking=no ${SSH_CONNECTION} 'IMAGE_NAME=${IMAGE_NAME} IMAGE_STORAGE=${IMAGE_STORAGE} BUILD_NUMBER=${BUILD_NUMBER} ./deploy.sh'"
}
}
}
}
}
에러 대응
+ scp -r folder user@ip:/path
Host key verification failed.
lost connection
script returned exit code 1
만약 위같은 에러가 뜬다면 scp 명령어에 다음을 추가한다.
-o StrictHostKeyChecking=no
완성된 부분은 아래와 같다.
scp -o StrictHostKeyChecking=no -r folder user@ip:/path
'DevOps > CI CD' 카테고리의 다른 글
GitHub Actions + AWS ECR + Docker CI CD 구축하기 (0) | 2023.09.11 |
---|---|
AWS ECR + Jenkins CI CD 구축하기 (0) | 2023.07.14 |
Bitbucket SSH key Change (0) | 2023.06.21 |