애플리케이션 개발 단계부터 배포까지의 모든 단계를 자동화하여 더 효율적이고 빠르게 사용자에게 빈번히 배포할 수 있게 하는 것.
개발자를 위한 자동화 프로세스라고 볼 수 있으며, Code - Build - Test 단계에서 꾀할 수 있다.
지속적인 서비스 제공(Continuous Delivery) 및 지속적인 배포(Continuous Deployment)를 의미하며 이 두 용어는 상호 교환적으로 사용된다.
이 부분은 Release - Deploy - Operate 단계에서 꾀할 수 있다.
결론: CI/CD 파이프라인을 구축함으로써 코드 병합 중 생길 수 있는 오류를 방지하고 배포 자동화를 통해 효율적인 프로세스를 완성.

도커 컨테이너를 하나만 사용했을 때는 서버에 요청이 들어왔을때 스프링 컨테이너가 있는 8080 포트로 연결시켜 주었다. 하지만 Blue green 배포 에서는 blue, green 각 컨테이너 실행시 8081, 8082 포트에서 실행되게 된다.
서버를 재배포하는 상황을 가정하여 과정을 정리해보자.
$sudo apt update
$sudo apt install nginx
✅기존 Docker가 설치되어 있다면 제거
sudo apt-get remove docker docker-engine docker.io containerd runc
✅ 필수 패키지 설치
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release
✅ Docker GPG Key 추가
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
✅ Docker 저장소 추가
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
✅ 패키지 인덱스 업데이트 및 Docker 설치
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
✅ Docker 설치 확인
sudo docker --version
✅ sudo 없이 docker 명령어 사용 설정
sudo usermod -aG docker $USER
newgrp docker
⁉ 참고
apt-key 사용 중단 경고 해결 (Ubuntu 22.04 이상):
apt-key 명령어가 더 이상 사용되지 않는다는 경고 메시지가 표시될 경우, 다음 방법을 사용하여 키를 추가합니다.
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/docker-archive-keyring.gpg
✅ 컨테이터 생성 및 실행
$docker ps # 실행중인 컨테이너 확인 명령어
$sudo docker run --name <container_name> -d -p <host_port>:<image_port> nginx
✅ 컨테이너 실행 확인
$docker ps
✅ 컨테이너 상세 정보 확인
$docker inspect <container_id>
✅ 컨테이너 로그 확인
$docker logs -f <container_id>
✅ 컨테이너 내부로 명령어 실행
$docker exec -it <container_id> <cmd>
$ sudo curl -SL https://github.com/docker/compose/releases/download/v2.29.6/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
$ docker -compose --version
# 실행 권한 부여
$ sudo ls -l /usr/local/bin
$ sudo chmod +x /usr/local/bin/docker-compose
$ sudo ls -l /usr/local/bin
$ docker -compose --version
⚡주의해야할 점!!!

Github Actions 가상 환경 세팅 → build하여 jar 생성 → Docker hub에 jar 이미지 push(여기까지 CI) → EC2에서 deploy.sh 파일 실행 → docker-compose up으로 blue or green 컨테이너 생성 및 실행 (CD 끝)
name: CI CD
on:
push:
branches:
- develop
- test
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.0
- name: Setup Java JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: application.yml 설정
run: |
cd ./src/main
mkdir -p resources
cd resources
touch ./application.yml
echo "$APPLICATION_MAIN" > ./application.yml
env:
APPLICATION_MAIN: ${{ secrets.APPLICATION_MAIN }}
- name: Cache Gradle
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build -x test
- name: Build and push Docker image
env:
DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}}
DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}}
run: |
docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
docker build -t $DOCKER_USERNAME/${{secrets.DOCKER_REPOSITORY}} .
docker push $DOCKER_USERNAME/${{secrets.DOCKER_REPOSITORY}}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.0
- name: Deploy to EC2 via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{secrets.EC2_HOST}}
username: ubuntu
key: ${{secrets.EC2_PRIVATE_KEY}}
port: 22
script: |
cd /home/ubuntu
sudo chmod +x deploy.sh
sudo ./deploy.sh
sudo docker image prune -af

name: CI CD
on:
push:
branches:
- develop
- test
jobs:
build:
runs-on: ubuntu-latest
steps:
ubuntu-latestGitHub이 제공하는 Ubuntu 리눅스 가상머신에서 모든 작업이 실행됨.
./gradlew)- name: Checkout
uses: actions/checkout@v4.1.0
GitHub Actions가 현재 브랜치의 코드를 가져옴 (git clone과 유사)
- name: Setup Java JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
Temurin(OpenJDK 기반) Java 17을 설치해서 이후 Gradle 빌드에서 사용 가능하게 함
설치 경로는 자동으로 JAVA_HOME 환경 변수로 설정됨
application.yml 주입- name: application.yml 설정
run: |
cd ./src/main
mkdir -p resources
cd resources
touch ./application.yml
echo "$APPLICATION_MAIN" > ./application.yml
env:
APPLICATION_MAIN: ${{ secrets.APPLICATION_MAIN }}
프로젝트 내부에 실제 파일로 application.yml 생성
APPLICATION_MAINapplication.yml 경로: src/main/resources/application.yml이걸 통해 .gitignore된 설정 파일을 런타임에 주입 가능
- name: Cache Gradle
uses: actions/cache@v3
빌드 속도 향상을 위한 Gradle 캐시 사용
~/.gradle/caches, ~/.gradle/wrapper를 캐싱함
- name: Grant execute permission for gradlew
run: chmod +x gradlew
./gradlew 스크립트에 실행 권한 부여 (리눅스 환경에서는 필수)
- name: Build with Gradle
run: ./gradlew build -x test
build task 실행 (테스트는 생략)
.jar 생성 → 위치: build/libs/*.jar- name: Build and push Docker image
env:
DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}}
DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}}
run: |
docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
docker build -t $DOCKER_USERNAME/yong2hae .
docker push $DOCKER_USERNAME/yong2hae
build/libs/*.jar을 Dockerfile에 기반해 이미지로 빌드한 뒤, Docker Hub로 푸시
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.0
- name: Deploy to EC2 via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{secrets.EC2_HOST}}
username: ubuntu
key: ${{secrets.EC2_PRIVATE_KEY}}
port: 22
script: |
cd /home/ubuntu
sudo chmod +x deploy.sh
sudo ./deploy.sh
sudo docker image prune -af
./gradlew build -x test
FROM openjdk:17
COPY build/libs/yong2hae-0.0.1-SNAPSHOT.jar app.jar
CMD ["java", "-jar", "app.jar"]
#!/bin/bash
# 현재 실행 중인 컨테이너 확인
IS_GREEN_EXIST=$(docker ps | grep green)
NGINX_CONF_PATH="/etc/nginx/nginx.conf"
# Blue가 실행 중이면 Green을 업그레이드
if [ -z "$IS_GREEN_EXIST" ]; then
echo "### BLUE => GREEN ###"
echo ">>> Green 버전의 컨테이너를 배포합니다."
# Green 컨테이너 이미지 Pull
echo ">>> Green 이미지 Pull"
docker-compose pull green
# Green 컨테이너 실행
echo ">>> Green 컨테이너 UP"
docker-compose up -d green
# Health Check (정상 동작 확인)
while true; do
echo ">>> Green 서버 Health Check 중..."
sleep 3
REQUEST=$(curl -s http://127.0.0.1:8082)
if [ -n "$REQUEST" ]; then
echo ">>> ✅ Green 서버 정상 가동 완료!"
break
fi
done
sleep 3
# Nginx 설정 변경 (Green 활성화)
echo ">>> Nginx 설정을 Green으로 변경합니다."
sudo cp /etc/nginx/nginx.green.conf $NGINX_CONF_PATH
sudo nginx -s reload
# 기존 Blue 컨테이너 종료
echo ">>> 기존 Blue 컨테이너 종료"
docker-compose stop blue
# Green이 실행 중이면 Blue를 업그레이드
else
echo "### GREEN => BLUE ###"
echo ">>> Blue 버전의 컨테이너를 배포합니다."
# Blue 컨테이너 이미지 Pull
echo ">>> Blue 이미지 Pull"
docker-compose pull blue
# Blue 컨테이너 실행
echo ">>> Blue 컨테이너 UP"
docker-compose up -d blue
# Health Check (정상 동작 확인)
while true; do
echo ">>> Blue 서버 Health Check 중..."
sleep 3
REQUEST=$(curl -s http://127.0.0.1:8081)
if [ -n "$REQUEST" ]; then
echo ">>> ✅ Blue 서버 정상 가동 완료!"
break
fi
done
sleep 3
# Nginx 설정 변경 (Blue 활성화)
echo ">>> Nginx 설정을 Blue로 변경합니다."
sudo cp /etc/nginx/nginx.blue.conf $NGINX_CONF_PATH
sudo nginx -s reload
# 기존 Green 컨테이너 종료
echo ">>> 기존 Green 컨테이너 종료"
docker-compose stop green
fi
echo "✅ 배포 완료!"
version: '3'
services:
blue:
image: [your_docker_username]/[docker_repo_name]
container_name: blue
restart: always
ports:
- 8081:8080
green:
image: [your_docker_username]/[docker_repo_name]
container_name: green
restart: always
ports:
- 8082:8080


nginx가 리버스 프록시 역할을 하여 blue, green의 포트를 관리해준다.
도메인 → 백엔드(Spring) 컨테이너 포트로 요청 전달
예:
http://my-service.com 으로 접속localhost:8080 (Spring 앱)으로 전달🔁 Spring 부트 앱이 직접 외부에서 요청 받는 게 아니라, Nginx가 대신 받고 전달함
하나의 컨테이너를 새로 띄우고, Nginx의 설정만 바꿔서 트래픽 전환
#nginx code
upstream backend {
server app_v1:8080;
# 나중에
# server app_v2:8080;
}
결론: nginx를 사용하여 트래픽 전환을 해주고 이를 통해 무중단 배포가 가능하게 한다.
server {
listen 80 default_server;
listen [::]:80 default_server;
# SSL configuration
#
# listen 443 ssl default_server;
# listen [::]:443 ssl default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
# include snippets/snakeoil.conf;
root /var/www/html;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
server_name localhost;
location / {
proxy_pass http://localhost:8081;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
}
# pass PHP scripts to FastCGI server
#
location ~ \.php$ {
include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
# # With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
server {
listen 80 default_server;
listen [::]:80 default_server;
# SSL configuration
#
# listen 443 ssl default_server;
# listen [::]:443 ssl default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
# include snippets/snakeoil.conf;
root /var/www/html;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
server_name localhost;
location / {
proxy_pass http://localhost:8082;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
}
# pass PHP scripts to FastCGI server
#
location ~ \.php$ {
include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
# # With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
이제 여기까지 환경 세팅이 끝났으면 CI/CD를 실행시킬 수 있는 브렌치에 push를 하여 CI/CD를 실행시킨다.
