GitHub Actions
Amazon EC2
NGINX
docker
Spring Boot, Maven
Ubuntu 선택
키 페어 생성(RSA, pem)
- 아래 사진과 위치로 이동.
- 테스트 목적이라면 HTTPS, HTTP 트래픽 허용.
- 스토리지는 프리티어라면 15GIB 까지 사용 가능.
생성된 EC2 인스턴스에 탄력적 IP 주소 할당
아래 사진과 같은 위치에 sh 형식의 파일 생성

코드 작성
#!/bin/bash
ssh -i ~/.ssh/_pem_키_이름 ubuntu@_탄력젹_IP
//접속하고자 하는 IP에 ssh 접속을 하겠다.
// '~/' 이 경로는 로컬C _ 사용자 _ USER 에 해당하는 경로

sudo su //관리자 권한 주기
apt-get update //최신 버전 가져오기
apt-get upgrade
apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
//필요한 패키지 설치
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
//Docker의 공식 GPG키를 추가
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
//Docker의 공식 apt 저장소를 추가
//여기까지 설치하면 docker를 설치할 수 있는 환경이 만들어졌다.
apt-get update //시스템 패키지 업데이트
apt-get install docker-ce docker-ce-cli containerd.io
//Docker 설치
curl \
-L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" \
-o /usr/local/bin/docker-compose
//Docker-Compose 설치
chmod +x /usr/local/bin/docker-compose
//Docker-Compose 실행 권한 주기
docker -v
docker-compose -v


만들어진 토큰을 해당 위치에 저장합니다.
데스크탑에 도커를 다운로드 합니다.

docker login
Username: //본인 username 입력
Password: //아까 저장했던 docker 토큰
Login Succeeded

docker pull nginx
//docker를 통해 설치를 하면 자동으로 필요한 nginx가 설치가 됩니다.
docker container run --name nginxserver -d -p 80:80 nginx
//nginx 실행
//-d : detach _ 계속해서 실행되고 있다.
//-p 80:80 nginx _ 외부에서 80포트로 들어오면 해당 컨테이너 안에 80포트에 nginx로 들어가라
docker ps
현재까지 EC2 Ububtu 안에 Dcoker를 설치한 상태입니다.
또, 도커를 활용하여 nginx까지 설치를 완료했습니다.
여기서 nginx라는 image를 가져온 것입니다.
docker 안에 가상의 컨테이너가 생성되고, 그 안에 nginx가 80포트로 실행됐습니다.
만약 컨테이너의 포트번호가 90이라고 하면 nginx에 80 포트가 실행됩니다.
ec2 관리자 권한을 준 상태에서
docker exec -it nginxserver bash
//해당 컨테이너 루트 계정으로 접속
//root@~~ < 이 부분이 변합니다. _ 컨테이너의 고유 ID가 출력됩니다.
cd etc/nginx/
//tab을 눌러지면 경로들이 나옵니다.
cd conf.d
apt-get update
apt-get upgrade
apt-get install vim
//파일 에디터 사용을 위한 vim 설치 과정
vim default.conf
어떤 파일이 열리면 가장 맨 위에 아래 코드를 작성한다.
upstream blue {
server 탄력적_IP:8080;
}
upstream green {
server 탄력적_IP:8081;
}
그리고 이제 server 안에 코드를 작성합니다.
server {
....
include /etc/nginx/conf.d/service-env.inc;
#해당 파일은 새로 만들어야 합니다.
location / {
proxy_pass http://$server_url;
#우리가 만든 파일에 blue, green에 따라서 다른 포트번호를 가져옵니다.
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;
...
}
저장하고 나온뒤
vim service-env.inc
//파일을 다시 만들어 줍니다.
아래 코드를 작성해주고 저장하고 나옵니다.
set $service_url green;
클라이언트에서 nginx를 통해 서버로 요청을 보냅니다.
작성한 파일들로 인해 nginx에서 $server_url 이 blue일때 8080포트로,
반대로 green일때 8081포트로 요청을 보냅니다.
자신이 배포할려는 프로젝트에 .mvn 파일을 확인해줍니다.

https://start.spring.io/ 없다면 해당 사이트에 접속해서 만들어줍니다.
또, gitignore 파일에 이러한 문구가 있다면 지워주세요.
!.mvn/wrapper/maven-wrapper.jar
@Value("${server.env}")
private String env;
@Value("${server.port}")
private String serverPort;
@Value("${server.serverAddress}")
private String serverAddress;
@Value("${serverName}")
private String serverName;
@GetMapping("/hc") //테스트 용도.
public ResponseEntity<?> check(){
Map<String,String> rd = new TreeMap<>();
rd.put("serverName",serverName);
rd.put("serverAddress",serverAddress);
rd.put("serverPort",serverPort);
rd.put("env",env);
return ResponseEntity.ok(rd);
}
@GetMapping("/env") //현재 어떤 서버가 가동하는지 확인하는 필요한 컨트롤러
public ResponseEntity<?> getEnv(){
return ResponseEntity.ok(env);
}
spring:
profiles:
active: local
group:
local: local, common, secret
blue: blue, common, secret
green: green common, secret
# gruop에서 active가 local로 잡히면 local, common, secret 이 3개를 로드해라.
# on-profile: local, on-profile: common 등등
server:
env: blue #우선 기본적으로 blue로 잡아 줍니다.
--- # 이 뜻은 yml 파일에서 영역을 나눌때 사용 합니다.
spring:
config:
activate:
on-profile: local
#만약 리다이렉트 uri 가 필요하다면
security:
oauth2:
client:
registration:
kakao:
redirectUri: http://탄력적_IP/login~
server:
port: 8080
serverAddress: localhost
serverName: local_server
---
spring:
config:
activate:
on-profile: blue
security:
oauth2:
client:
registration:
kakao:
redirectUri: http://탄력적_IP/login~
server:
port: 8080
serverAddress: 탄력적_IP
serverName: blue_server
---
spring:
config:
activate:
on-profile: green
security:
oauth2:
client:
registration:
kakao:
redirectUri: http://탄력적_IP/login~
server:
port: 8081
serverAddress: 탄력적_IP
serverName: green_server
#포트가 달라도 공통적으로 쓸 부분을 common
#나 같은 경우는 비밀번호 그런거?
---
spring:
config:
activate:
on-profile: common
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:-
username: -
password: -
jpa:
show-sql: true #true or false
properties:
hibernate:
format_sql: true
ec2 에서 관리자 권한 준 상태에서 명령어를 입력합니다.
vim docker-compose-blue.yml
//새로운 yml파일을 생성. 편집기 입력
------ //docker-compose-blue.yml
version: '3.8'
services:
blue:
image: docker_username/pem_key_이름:latest
container_name: blue
ports:
- "8080:8080"
environment:
- PROFILES=blue
- ENV=blue
//저장하고 편집기 종료합니다.
------
cp docker-compose-blue.yml ./docker-compose-green.yml
//기존 blue파일을 복사하여 green파일을 만들겠다는 명령어 입니다.
------ //docker-compose-green.yml
version: '3.8'
services:
blue:
image: docker_username/pem_key_이름:latest
container_name: green
ports:
- "8081:8081"
environment:
- PROFILES=green
- ENV=green
//저장하고 편집기를 종료합니다.
그리고 프로젝트 최상단에 Dockerfile 이라는 이름에 파일을 만들어주세요.

아래 코드에 있는 변수는 방금 만든 yml 파일에 변수입니다. (PROFILES, ENV)
docker-compose에서 green을 주면 green 서버가 열리는거고
blue를 주면 blue가 열리게 됩니다.
FROM amazoncorretto:17-alpine-jdk //버전에 맞게 찾아서 변경해주세요.
ARG JAR_FILE=target/*.jar //jar파일에 경로를 찾아준다.
ARG PROFILES //
ARG ENV
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Dspring.profiles.active=${PROFILES}","-Dserver.env=${ENV}","-jar","app.jar"]
//.jar 파일 실행
필요한 도커 이미지를 만들었습니다.

레포지토리에 접속하여 위의 사진과 같은 Actions을 클릭합니다.


필요한 시크릿 변수 이름
1. DOCKERHUB_USERNAME //도커 username
2. DOCKERHUB_TOKEN //도커 토큰
3. EC2_SSH_KEY //pem키
3. LIVE_SERVER_IP //탄력적 IP
변수 생성이 끝났으면 상단에 Actions 클릭합니다.

사진처럼 클릭하고 이제 yml 파일을 작성합니다.
//트리거 - 이벤트가 발생하면 자동으로 실행됩니다.
//트리거 파일 작성.
name: CI
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ] //각 해당 브랜치에 push 및 pull_request가 발생하면 발동해라.
permissions:
contents: read //읽기 권한
jobs: //일하는 단위
build:
runs-on: ubuntu-latest //우분투 최신 버전을 사용한다. 우분투는 스크립트를 사용해서 작성하기 무난하다.
steps:
- uses: actions/checkout@v3 //push 및 pull_request 된 레포지토리를 pull 받아준다.
- name: Install JDK 17
uses: actions/setup-java@v3
with:
java-version: '17' //여기서 원하는 버전 입력하시면 됩니다. 11, 17 버전만 사용해봤습니다 ^^
distribution: 'temurin'
- name: Build with Maven
run: |
chmod 777 ./mvnw
./mvnw clean package -Dtestskip //target 파일을 지우고 다시 생성 _ test 없이 실행한다.
//만약 테스트 파일이 있다면 -Dtestskip 을 지워주세요.
//우리가 많이 본 .jar 파일이 만들어진다.
//아래 과정은 도커에 우리가 원하는 설치를 진행하고 도커 허브로 올린 후
//우리가 사용하려는 EC2에서 해당 이미지를 가져오는 과정입니다.
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build Docker
run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/live_server . //이미지를 만든다.
//live_server -> 도커 이미지 이름
- name: Push Docker
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/live_server:latest //만든 이미지를 도커허브에 올린다.
deploy:
needs: build //정상적으로 build가 완료되면 실행하라. 만약 위에 build 과정이 실패하면 발동 안함.
runs-on: ubuntu-latest
steps:
- name: Set target IP
run: | //nginx가 서버가 현재 블루가 실행 중이면 배포를 그린으로 하고, 그린이 실행 중이면 블루를 배포한다.
//그러기 위해선 현재 어떤 서버가 실행 중에 있는지 확인해야한다.
STATUS=$(curl -o /dev/null -w "%{http_code}" "http://${{ secrets.LIVE_SERVER_IP }}/env")
echo $STATUS
if [ $STATUS = 200 ]; then
CURRENT_UPSTREAM=$(curl -s "http://${{ secrets.LIVE_SERVER_IP }}/env")
else
CURRENT_UPSTREAM=green
fi //조건문 닫는다.
echo CURRENT_UPSTREAM=$CURRENT_UPSTREAM >> $GITHUB_ENV
if [ $CURRENT_UPSTREAM = blue ]; then
echo "CURRENT_PORT=8080" >> $GITHUB_ENV
echo "STOPPED_PORT=8081" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=green" >> $GITHUB_ENV
elif [ $CURRENT_UPSTREAM = green ]; then
echo "CURRENT_PORT=8081" >> $GITHUB_ENV
echo "STOPPED_PORT=8080" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=blue" >> $GITHUB_ENV
else
echo "error"
exit 1
fi
- name: Docker compose
uses: appleboy/ssh-action@master
with:
username: ubuntu //ec2 실행
host: ${{ secrets.LIVE_SERVER_IP }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: | //도커 실행
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/live_server:latest
sudo docker-compose -f docker-compose-${{env.TARGET_UPSTREAM}}.yml up -d
- name: Check deploy server URL
uses: jtalk/url-health-check-action@v3
with: //health-check
url: http://${{ secrets.LIVE_SERVER_IP }}:${{env.STOPPED_PORT}}/env
max-attempts: 5 //프리티어 사용하시는 분들이라면 health-check 횟수를 5번 이상으로도 설정
retry-delay: 10s
- name: Change nginx upstream
uses: appleboy/ssh-action@master
with:
username: ubuntu //다시 ec2 접속
host: ${{ secrets.LIVE_SERVER_IP }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: | //nginx서버 접속 _ 커맨드만 실행하기 위해 bash 사용.
sudo docker exec -i nginxserver bash -c 'echo "set \$service_url ${{ env.TARGET_UPSTREAM }};" > /etc/nginx/conf.d/service-env.inc && nginx -s reload'
//이 명령어에서는 파일 변수를 블루라면 그린으로 바꾼다.
- name: Stop current server
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.LIVE_SERVER_IP }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: | //만약 블루라면 블루 컨테이너를 중지와 제거.
sudo docker stop ${{env.CURRENT_UPSTREAM}}
sudo docker rm ${{env.CURRENT_UPSTREAM}}
인바운드 규칙 편집 접속해서
본인이 사용하는 포트번호를 넣어주세요.
ex) 8080, 8081

CICD.yml 파일을 작성하고 커밋하면 처음 실행하면
배포가 작동하지 않습니다.
변수 설정을 해둬서 처음에 green으로 시작하기 때문입니다.
2번째 실행부터 제대로 가동됩니다.
application-secret.yml 을 사용하시는 분들은 아래를 봐주세요

이렇게 사진처럼 application-secret.yml 파일에 들어갈 내용을 base64 인코딩을 하여 넣어주세요.
그리고 아래 코드를 사용해서 스크립트를 수정해주세요.
base64 인코딩 된 파일을 디코딩해서 사용하겠다는 뜻입니다.
- name: Build with Maven
run: |
echo ${{secret.APPLICATION_SECRET}} | base64 --decode > ./src/main/resources/application-secret.yml
chmod 777 ./mvnw
./mvnw clean package -Dtestskip