7575
7676 deploy :
7777 runs-on : ubuntu-latest
78- needs : [ buildImageAndPush ]
78+ needs : [ makeTagAndRelease, buildImageAndPush ]
7979 env :
80- OWNER : ${{ github.repository_owner }}
80+ IMAGE_REPOSITORY : catfe_backend # 도커 이미지 명
81+ CONTAINER_1_NAME : catfe_1 # 슬롯 1
82+ CONTAINER_2_NAME : catfe_2 # 슬롯 2
83+ CONTAINER_PORT : 8080 # 컨테이너 내부 포트
84+ DOCKER_NETWORK : common # 도커 네트워크
8185 EC2_INSTANCE_TAG_NAME : team5-ec2-1
8286
8387 steps :
9599 [[ -n "${INSTANCE_ID}" && "${INSTANCE_ID}" != "None" ]] || { echo "No running instance found"; exit 1; }
96100 echo "INSTANCE_ID=${INSTANCE_ID}" >> "${GITHUB_ENV}"
97101
98- - name : set lower case owner name
99- run : |
100- echo "OWNER_LC=${OWNER,,}" >> ${GITHUB_ENV}
101-
102102 - name : AWS SSM Send-Command
103103 uses : peterkimzz/aws-ssm-send-command@master
104104 id : ssm
@@ -110,21 +110,128 @@ jobs:
110110 working-directory : /
111111 comment : Deploy
112112 command : |
113- # 1. env 변수 확인
114- echo "OWNER_LC = ${{ env.OWNER_LC }}"
113+ set -Eeuo pipefail
115114
116- # 2 . EC2 인스턴스 아이디 확인
115+ # 1 . EC2 인스턴스 아이디 확인
117116 echo "INSTANCE_ID=${INSTANCE_ID}"
118117
119- # 3. 최신 이미지 pull
120- docker pull ghcr.io/${{ env.OWNER_LC }}/catfe-backend:latest
118+ # 3. 실행 로그(라인 타임스탬프 부착)
119+ LOG="/tmp/ssm-$(date +%Y%m%d_%H%M%S).log"
120+ exec > >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG")
121+ exec 2> >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG" >&2)
122+
123+ # 4. 변수 정의
124+ source /etc/environment || true
125+ OWNER_LC="${{ github.repository_owner }}"
126+ OWNER_LC="${OWNER_LC,,}"
127+ IMAGE_TAG="${{ needs.makeTagAndRelease.outputs.tag_name }}"
128+ IMAGE_REPOSITORY="${{ env.IMAGE_REPOSITORY }}"
129+ IMAGE="ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:${IMAGE_TAG}"
130+ SLOT1="${{ env.CONTAINER_1_NAME }}"
131+ SLOT2="${{ env.CONTAINER_2_NAME }}"
132+ PORT_IN="${{ env.CONTAINER_PORT }}"
133+ NET="${{ env.DOCKER_NETWORK }}"
134+
135+ # 도커 이미지 pull 받기
136+ echo "🔹 Use image: ${IMAGE}"
137+ docker pull "${IMAGE}"
138+
139+ #5. NPM API 토큰 발급
140+ TOKEN=$(curl -s -X POST http://127.0.0.1:81/api/tokens \
141+ -H "Content-Type: application/json" \
142+ -d "{\"identity\": \"[email protected] \", \"secret\": \"${PASSWORD:-}\"}" | jq -r '.token') 143+
144+ # 조회한 토큰과 도메인 검증
145+ [[ -n "${TOKEN}" && "${TOKEN}" != "null" ]] || { echo "NPM token issue failed"; exit 1; }
146+ [[ -n "${DOMAIN:-}" ]] || { echo "DOMAIN is empty"; exit 1; }
147+
148+ # 6. 대상 프록시 호스트 ID 조회(도메인 매칭)
149+ PROXY_ID=$(curl -s -X GET "http://127.0.0.1:81/api/nginx/proxy-hosts" \
150+ -H "Authorization: Bearer ${TOKEN}" \
151+ | jq ".[] | select(.domain_names[]==\"${APP_1_DOMAIN}\") | .id")
152+
153+ # 조회한 프록시 호스트 ID 검증
154+ [[ -n "${PROXY_ID}" && "${PROXY_ID}" != "null" ]] || { echo "Proxy host not found for ${APP_1_DOMAIN}"; exit 1; }
155+
156+ # 현재 프록시가 바라보는 업스트림(컨테이너명) 조회
157+ CURRENT_HOST=$(curl -s -X GET "http://127.0.0.1:81/api/nginx/proxy-hosts/${PROXY_ID}" \
158+ -H "Authorization: Bearer ${TOKEN}" \
159+ | jq -r '.forward_host')
160+
161+ echo "🔎 CURRENT_HOST: ${CURRENT_HOST:-none}"
162+
163+ # 7. 역할(blue/green) 판정 (blue -> 현재 운영 중인 서버, green -> 교체할 서버)
164+ if [[ "${CURRENT_HOST:-}" == "${SLOT1}" ]]; then
165+ BLUE="${SLOT1}"
166+ GREEN="${SLOT2}"
167+
168+ elif [[ "${CURRENT_HOST:-}" == "${SLOT2}" ]]; then
169+ BLUE="${SLOT2}"
170+ GREEN="${SLOT1}"
171+
172+ # 초기 배포
173+ else
174+ BLUE="none"
175+ GREEN="${SLOT1}"
176+
177+ # 조건문 종료
178+ fi
179+ echo "🎨 role -> blue(now): ${BLUE}, green(next): ${GREEN}"
180+
181+ # 8. Green 역할 컨테이너
182+ docker rm -f "${Green}" > /dev/null 2>&1 || true
183+ echo "run new container -> ${Green}"
184+ docker run -d --name "${Green}" \
185+ --restart unless-stopped \
186+ --network "${NET}" \
187+ -e TZ=Asia/Seoul \
188+ "${IMAGE}"
189+
190+ # 9. 헬스체크
191+ echo "⏱ health-check: ${GREEN}"
192+ TIMEOUT=120
193+ INTERVAL=3
194+ ELAPSED=0
195+ sleep 8 # 초기부팅 여유
196+
197+ while (( ELAPSED < TIMEOUT )); do
198+ CODE=$(docker exec "${GREEN}" curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:${PORT_IN}/actuator/health" || echo 000)
199+ [[ "${CODE}" == "200" ]] && { echo "✅ ${GREEN} healthy"; break; }
200+ sleep "${INTERVAL}"
201+ ELAPSED=$((ELAPSED + INTERVAL))
202+ done
203+ [[ "${CODE:-000}" == "200" ]] || { echo "❌ ${GREEN} health failed"; docker logs --tail=200 "${GREEN}" || true; docker rm -f "${GREEN}" || true; exit 1; }
204+
205+ # 10. 업스트림 전환
206+ NEW_CFG=$(jq -n --arg host "${GREEN}" --argjson port ${PORT_IN} '{forward_host:$host, forward_port:$port}')
207+ curl -s -X PUT "http://127.0.0.1:81/api/nginx/proxy-hosts/${PROXY_ID}" \
208+ -H "Authorization: Bearer ${TOKEN}" \
209+ -H "Content-Type: application/json" \
210+ -d "${NEW_CFG}" >/dev/null
211+ echo "🔁 switch upstream → ${GREEN}:${PORT_IN}"
212+
213+ # 11. 이전 Blue 종료
214+ if [[ "${BLUE}" != "none" ]]; then
215+ docker stop "${BLUE}" >/dev/null 2>&1 || true
216+ docker rm "${BLUE}" >/dev/null 2>&1 || true
217+ echo "🧹 removed old blue: ${BLUE}"
218+ fi
219+
220+ # 12. 이미지 정리
221+ {
222+ docker images --format '{{.Repository}}:{{.Tag}}' \
223+ | grep -F "ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:" \
224+ | grep -v -F ":${IMAGE_TAG}" \
225+ | grep -v -F ":latest" \
226+ | xargs -r docker rmi
227+ } || true
228+
229+ echo "🏁 Blue/Green switch complete. now blue = ${GREEN}"
230+
231+
232+
233+
234+
121235
122- # 2. 기존 컨테이너 종료 및 제거
123- docker stop catfe-backend 2>/dev/null
124- docker rm catfe-backend 2>/dev/null
125236
126- # 3. 새로운 컨테이너 실행
127- docker run -d --name catfe-backend -p 8080:8080 ghcr.io/${{ env.OWNER_LC }}/catfe-backend:latest
128237
129- # 4. dangling 이미지 삭제
130- docker rmi $(docker images -f "dangling=true" -q)
0 commit comments