Skip to content

Commit 60927d2

Browse files
authored
Update deploy.yml (#53)
1 parent 5022f2e commit 60927d2

File tree

1 file changed

+125
-14
lines changed

1 file changed

+125
-14
lines changed

.github/workflows/deploy.yml

Lines changed: 125 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ name: deploy
66
env:
77
IMAGE_REPOSITORY: nutree # GHCR 이미지 리포지토리명(소유자 포함 X)
88
CONTAINER_1_NAME: nutree_1 # 슬롯1(고정 이름)
9+
CONTAINER_2_NAME: nutree_2 # 슬롯2(고정 이름)
10+
CONTAINER_PORT: 8080 # 컨테이너 내부 포트(스프링부트)
911
EC2_INSTANCE_TAG_NAME: team08-ec2-1 # 배포 대상 EC2 Name 태그
1012
DOCKER_NETWORK: common # 도커 네트워크
1113
BACKEND_DIR: backend # Dockerfile 위치
1214

15+
concurrency:
16+
group: ${{ github.workflow }}-${{ github.ref }} # 커밋이 짧은 시간안에 몰려도 최신 커밋에 대해서만 액션이 수행되도록
17+
cancel-in-progress: false # 기존에 시작된 작업은 새 커밋이 들어왔다고 해서 바로 끄지 않고 끝날때까지 대기
18+
1319
on:
1420
push:
1521
paths:
@@ -20,7 +26,7 @@ on:
2026
- "backend/settings.gradle.kts"
2127
- "backend/Dockerfile"
2228
branches:
23-
- dev
29+
- dev # main 대신 dev 브랜치에서 배포
2430

2531
# 권한 최소화/명시화
2632
permissions:
@@ -113,10 +119,10 @@ jobs:
113119
ghcr.io/${{ env.OWNER_LC }}/${{ env.IMAGE_REPOSITORY }}:latest
114120
115121
# ---------------------------------------------------------
116-
# 3) 배포
122+
# 3) Blue/Green 무중단 배포 (EC2 + NPM 스위치)
117123
# ---------------------------------------------------------
118124
deploy:
119-
name: 배포
125+
name: Blue/Green 무중단 배포
120126
runs-on: ubuntu-latest
121127
needs: [makeTagAndRelease, buildImageAndPush]
122128
steps:
@@ -137,7 +143,7 @@ jobs:
137143
[[ -n "${INSTANCE_ID}" && "${INSTANCE_ID}" != "None" ]] || { echo "No running instance found"; exit 1; }
138144
echo "INSTANCE_ID=${INSTANCE_ID}" >> "${GITHUB_ENV}"
139145
140-
# 원격(SSM)으로 배포 수행
146+
# 원격(SSM)으로 Blue/Green 스위치 수행
141147
- name: AWS SSM Send-Command
142148
uses: peterkimzz/aws-ssm-send-command@master
143149
with:
@@ -150,28 +156,133 @@ jobs:
150156
command: |
151157
set -Eeuo pipefail
152158
159+
# ---------------------------------------------------------
160+
# 0) 실행 로그(라인 타임스탬프 부착)
161+
# ---------------------------------------------------------
153162
LOG="/tmp/ssm-$(date +%Y%m%d_%H%M%S).log"
154163
exec > >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG")
155164
exec 2> >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG" >&2)
156165
157-
source /etc/environment || true
158-
166+
# ---------------------------------------------------------
167+
# 1) 변수 정의
168+
# - 슬롯 이름은 고정(두 개의 컨테이너 이름)
169+
# - blue/green은 "역할"이며 매 배포마다 바뀜
170+
# ---------------------------------------------------------
171+
source /etc/environment || true # 시스템 전역 변수(+ 비밀) 주입 시도
159172
OWNER_LC="${{ github.repository_owner }}"
160-
OWNER_LC="${OWNER_LC,,}"
161-
IMAGE_TAG='${{ needs.makeTagAndRelease.outputs.tag_name }}'
162-
IMAGE_REPOSITORY='${{ env.IMAGE_REPOSITORY }}'
173+
OWNER_LC="${OWNER_LC,,}" # 소문자 표준화
174+
IMAGE_TAG="${{ needs.makeTagAndRelease.outputs.tag_name }}"
175+
IMAGE_REPOSITORY="${{ env.IMAGE_REPOSITORY }}"
163176
IMAGE="ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:${IMAGE_TAG}"
164-
CONTAINER_1_NAME="${{ env.CONTAINER_1_NAME }}"
177+
SLOT1="${{ env.CONTAINER_1_NAME }}"
178+
SLOT2="${{ env.CONTAINER_2_NAME }}"
179+
PORT_IN="${{ env.CONTAINER_PORT }}"
165180
NET="${{ env.DOCKER_NETWORK }}"
166181
167-
docker pull $IMAGE
168-
docker stop $CONTAINER_1_NAME || true
169-
docker rm $CONTAINER_1_NAME || true
170-
docker run -d --restart unless-stopped --name $CONTAINER_1_NAME --network $NET $IMAGE
182+
echo "🔹 Use image: ${IMAGE}"
183+
docker pull "${IMAGE}"
184+
185+
# ---------------------------------------------------------
186+
# 2) Nginx Proxy Manager(NPM) API 토큰 발급
187+
# - /etc/environment 등에 설정된 PASSWORD_1, APP_1_DOMAIN 사용 가정
188+
# ---------------------------------------------------------
189+
TOKEN=$(curl -s -X POST http://127.0.0.1:81/api/tokens \
190+
-H "Content-Type: application/json" \
191+
-d "{\"identity\": \"[email protected]\", \"secret\": \"${PASSWORD_1:-}\"}" | jq -r '.token')
192+
193+
# 토큰/도메인 검증(없으면 실패)
194+
[[ -n "${TOKEN}" && "${TOKEN}" != "null" ]] || { echo "NPM token issue failed"; exit 1; }
195+
[[ -n "${APP_1_DOMAIN:-}" ]] || { echo "APP_1_DOMAIN is empty"; exit 1; }
196+
197+
# 대상 프록시 호스트 ID 조회(도메인 매칭)
198+
PROXY_ID=$(curl -s -X GET "http://127.0.0.1:81/api/nginx/proxy-hosts" \
199+
-H "Authorization: Bearer ${TOKEN}" \
200+
| jq ".[] | select(.domain_names[]==\"${APP_1_DOMAIN}\") | .id")
201+
202+
[[ -n "${PROXY_ID}" && "${PROXY_ID}" != "null" ]] || { echo "Proxy host not found for ${APP_1_DOMAIN}"; exit 1; }
203+
204+
# 현재 프록시가 바라보는 업스트림(컨테이너명) 조회
205+
CURRENT_HOST=$(curl -s -X GET "http://127.0.0.1:81/api/nginx/proxy-hosts/${PROXY_ID}" \
206+
-H "Authorization: Bearer ${TOKEN}" \
207+
| jq -r '.forward_host')
208+
209+
echo "🔎 CURRENT_HOST: ${CURRENT_HOST:-none}"
210+
211+
# ---------------------------------------------------------
212+
# 3) 역할(blue/green) 판정
213+
# - blue : 현재 운영 중(CURRENT_HOST)
214+
# - green: 다음 운영(교체 대상)
215+
# ---------------------------------------------------------
216+
if [[ "${CURRENT_HOST:-}" == "${SLOT1}" ]]; then
217+
BLUE="${SLOT1}"
218+
GREEN="${SLOT2}"
219+
elif [[ "${CURRENT_HOST:-}" == "${SLOT2}" ]]; then
220+
BLUE="${SLOT2}"
221+
GREEN="${SLOT1}"
222+
else
223+
BLUE="none" # 초기 배포
224+
GREEN="${SLOT1}"
225+
fi
226+
echo "🎨 role -> blue(now): ${BLUE}, green(next): ${GREEN}"
227+
228+
# ---------------------------------------------------------
229+
# 4) green 역할 컨테이너 재기동
230+
# - 같은 네트워크 상에서 NPM이 컨테이너명:PORT로 프록시하므로 -p 불필요
231+
# ---------------------------------------------------------
232+
docker rm -f "${GREEN}" >/dev/null 2>&1 || true
233+
echo "🚀 run new container → ${GREEN}"
234+
docker run -d --name "${GREEN}" \
235+
--restart unless-stopped \
236+
--network "${NET}" \
237+
-e TZ=Asia/Seoul \
238+
"${IMAGE}"
239+
240+
# ---------------------------------------------------------
241+
# 5) 헬스체크 (/actuator/health 200 OK까지 대기)
242+
# ---------------------------------------------------------
243+
echo "⏱ health-check: ${GREEN}"
244+
TIMEOUT=120
245+
INTERVAL=3
246+
ELAPSED=0
247+
sleep 8 # 초기 부팅 여유
248+
249+
while (( ELAPSED < TIMEOUT )); do
250+
CODE=$(docker exec "${GREEN}" curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:${PORT_IN}/actuator/health" || echo 000)
251+
[[ "${CODE}" == "200" ]] && { echo "✅ ${GREEN} healthy"; break; }
252+
sleep "${INTERVAL}"
253+
ELAPSED=$((ELAPSED + INTERVAL))
254+
done
255+
[[ "${CODE:-000}" == "200" ]] || { echo "❌ ${GREEN} health failed"; docker logs --tail=200 "${GREEN}" || true; docker rm -f "${GREEN}" || true; exit 1; }
256+
257+
# ---------------------------------------------------------
258+
# 6) 업스트림 전환 (forward_host/forward_port만 업데이트)
259+
# ---------------------------------------------------------
260+
NEW_CFG=$(jq -n --arg host "${GREEN}" --argjson port ${PORT_IN} '{forward_host:$host, forward_port:$port}')
261+
curl -s -X PUT "http://127.0.0.1:81/api/nginx/proxy-hosts/${PROXY_ID}" \
262+
-H "Authorization: Bearer ${TOKEN}" \
263+
-H "Content-Type: application/json" \
264+
-d "${NEW_CFG}" >/dev/null
265+
echo "🔁 switch upstream → ${GREEN}:${PORT_IN}"
266+
267+
# ---------------------------------------------------------
268+
# 7) 이전 blue 종료(최초 배포면 생략)
269+
# ---------------------------------------------------------
270+
if [[ "${BLUE}" != "none" ]]; then
271+
docker stop "${BLUE}" >/dev/null 2>&1 || true
272+
docker rm "${BLUE}" >/dev/null 2>&1 || true
273+
echo "🧹 removed old blue: ${BLUE}"
274+
fi
275+
276+
# ---------------------------------------------------------
277+
# 8) 이미지 정리(현재 태그/ latest 제외) - 결과 없음도 오류 아님
278+
# - pipefail과 grep의 종료코드(1)를 고려해 블록 전체에 || true 적용
279+
# ---------------------------------------------------------
171280
{
172281
docker images --format '{{.Repository}}:{{.Tag}}' \
173282
| grep -F "ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:" \
174283
| grep -v -F ":${IMAGE_TAG}" \
175284
| grep -v -F ":latest" \
176285
| xargs -r docker rmi
177286
} || true
287+
288+
echo "🏁 Blue/Green switch complete. now blue = ${GREEN}"

0 commit comments

Comments
 (0)