1- name : EC2 - Deploy (dispatch )
1+ name : EC2 Deploy (short )
22
33on :
44 repository_dispatch :
55 types : [deploy-ec2]
6- workflow_dispatch :
7-
8- concurrency :
9- group : ec2-deploy
10- cancel-in-progress : false
11-
12- permissions :
13- contents : read
6+ workflow_dispatch : {}
147
158env :
169 AWS_REGION : ap-northeast-2
17- # ✅ 배포 대상 EC2 리스트(JSON 배열). 필요에 맞게 교체하세요.
18- INSTANCE_IDS_JSON : ' ["i-08f17d18d83292c07","i-08316767f2bd4a5ed"]'
19-
20- # ✅ 앱/컨테이너 설정
21- APP_NAME : kickytime # 기존 컨테이너 기본 이름 (예: kickytime)
22- CONTAINER_PORT : " 8080" # 컨테이너 내부 포트
23- HOST_PORT_BASE : " 8080" # 기존 컨테이너가 바인드하는 호스트 포트
24- HEALTH_PATH : " /actuator/health" # 헬스체크 경로
25- HEALTH_TIMEOUT_SEC : " 180" # 헬스체크 최대 대기시간
26- ENV_FILE_PATH : " " # 예: /opt/kickytime/.env (없으면 빈 값)
10+ IMAGE : ${{ github.event.client_payload.image_uri || '077672914621.dkr.ecr.ap-northeast-2.amazonaws.com/kickytime-repo:latest' }}
11+ TARGET_TAG_KEY : Role
12+ TARGET_TAG_VALUE : app
13+ PORT : " 8080"
14+ HEALTH : " /actuator/health"
2715
2816jobs :
2917 deploy :
3018 runs-on : ubuntu-latest
31- strategy :
32- max-parallel : 1
33- matrix :
34- instance_id : ${{ fromJson(env.INSTANCE_IDS_JSON) }}
35-
36- env :
37- PAYLOAD_IMAGE_URI : ${{ github.event.client_payload.image_uri }}
38- PAYLOAD_TAG : ${{ github.event.client_payload.tag }}
39- PAYLOAD_BRANCH : ${{ github.event.client_payload.branch }}
40- PAYLOAD_SHA : ${{ github.event.client_payload.sha }}
41-
4219 steps :
43- - name : Gate - only deploy for main
44- id : gate
45- run : |
46- if [ "${PAYLOAD_BRANCH}" = "main" ]; then
47- echo "GO=true" >> $GITHUB_OUTPUT
48- else
49- echo "GO=false" >> $GITHUB_OUTPUT
50- echo "Non-deploy branch: ${PAYLOAD_BRANCH}"
51- fi
52-
53- - name : Checkout (same commit as build)
54- if : steps.gate.outputs.GO == 'true'
55- uses : actions/checkout@v4
56- with :
57- ref : ${{ env.PAYLOAD_SHA }}
58-
59- - name : Configure AWS credentials
60- if : steps.gate.outputs.GO == 'true'
61- uses : aws-actions/configure-aws-credentials@v4
20+ - uses : aws-actions/configure-aws-credentials@v4
6221 with :
63- aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }}
22+ aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }}
6423 aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }}
65- aws-region : ${{ env.AWS_REGION }}
24+ aws-region : ${{ env.AWS_REGION }}
6625
67- - name : Send rolling deploy command via SSM
68- if : steps.gate.outputs.GO == 'true'
69- id : ssm
26+ # ✅ 추가된 최소 스텝: 실행중 + 태그(Role=app) + SSM Online 교집합 → ids_list 출력
27+ - name : Resolve running + tagged + SSM-online
28+ id : resolve
29+ shell : bash
7030 run : |
71- set -e
72-
73- TARGET_ID="${{ matrix.instance_id }}"
74- echo "Deploying to instance: $TARGET_ID"
75-
76- # 새 컨테이너가 바인드할 임시 포트(현재 HOST_PORT_BASE+1)
77- NEXT_PORT=$(( ${HOST_PORT_BASE} + 1 ))
78-
79- # SSM에 보낼 스크립트를 JSON 안전하게 변환
80- read -r -d '' SCRIPT <<'EOSCRIPT'
81- #!/usr/bin/env bash
8231 set -euo pipefail
83-
84- APP_NAME="${APP_NAME}"
85- IMAGE_URI="${PAYLOAD_IMAGE_URI}"
86- CONTAINER_PORT="${CONTAINER_PORT}"
87- HOST_PORT_BASE="${HOST_PORT_BASE}"
88- NEXT_PORT=$(( HOST_PORT_BASE + 1 ))
89- HEALTH_PATH="${HEALTH_PATH}"
90- HEALTH_TIMEOUT="${HEALTH_TIMEOUT_SEC}"
91- ENV_FILE_PATH="${ENV_FILE_PATH}"
92-
93- OLD_NAME="${APP_NAME}"
94- NEW_NAME="${APP_NAME}_new"
95-
96- echo "[1/7] ECR 로그인"
97- REGISTRY_HOST="$(echo "$IMAGE_URI" | awk -F'/' '{print $1}')"
98- aws ecr get-login-password --region "${AWS_REGION}" | docker login --username AWS --password-stdin "${REGISTRY_HOST}"
99-
100- echo "[2/7] 새 이미지 Pull: ${IMAGE_URI}"
101- docker pull "${IMAGE_URI}"
102-
103- echo "[3/7] 이전 NEW 컨테이너 정리(있으면)"
104- if docker ps -a --format '{{.Names}}' | grep -q "^${NEW_NAME}$"; then
105- docker rm -f "${NEW_NAME}" || true
32+ RUNNING_IDS=$(aws ec2 describe-instances \
33+ --region "${AWS_REGION}" \
34+ --filters "Name=tag:${TARGET_TAG_KEY},Values=${TARGET_TAG_VALUE}" "Name=instance-state-name,Values=running" \
35+ --query 'Reservations[].Instances[].InstanceId' --output text | tr '\t' '\n' || true)
36+ SSM_ONLINE_IDS=$(aws ssm describe-instance-information \
37+ --region "${AWS_REGION}" \
38+ --filters "Key=PingStatus,Values=Online" \
39+ --query 'InstanceInformationList[].InstanceId' --output text | tr '\t' '\n' || true)
40+ TARGET_IDS=$(comm -12 <(printf '%s\n' $RUNNING_IDS | sort) <(printf '%s\n' $SSM_ONLINE_IDS | sort) || true)
41+ if [ -z "${TARGET_IDS:-}" ]; then
42+ echo "No targets (running+tagged+ssm-online)."; exit 1
10643 fi
44+ echo "ids_list=$(printf '%s ' $TARGET_IDS)" >> "$GITHUB_OUTPUT"
10745
108- echo "[4/7] 새 컨테이너 기동: ${NEW_NAME} (host ${NEXT_PORT} -> container ${CONTAINER_PORT})"
109- RUN_ENV_OPTS=()
110- if [ -n "${ENV_FILE_PATH}" ]; then
111- RUN_ENV_OPTS+=( --env-file "${ENV_FILE_PATH}" )
112- fi
113-
114- docker run -d \
115- --name "${NEW_NAME}" \
116- -p "${NEXT_PORT}:${CONTAINER_PORT}" \
117- "${RUN_ENV_OPTS[@]}" \
118- "${IMAGE_URI}"
119-
120- echo "[5/7] 헬스체크: http://127.0.0.1:${NEXT_PORT}${HEALTH_PATH} (timeout: ${HEALTH_TIMEOUT}s)"
121- SECS=0
122- until curl -fsS "http://127.0.0.1:${NEXT_PORT}${HEALTH_PATH}" >/dev/null 2>&1; do
123- sleep 3
124- SECS=$(( SECS + 3 ))
125- if [ "${SECS}" -ge "${HEALTH_TIMEOUT}" ]; then
126- echo "❌ 새 컨테이너 헬스체크 실패. 로그:"
127- docker logs --tail 200 "${NEW_NAME}" || true
128- exit 1
129- fi
130- done
131- echo "✅ 새 컨테이너 Healthy"
132-
133- echo "[6/7] 기존 컨테이너 ${OLD_NAME} 종료/삭제 및 포트 스위칭"
134- if docker ps -a --format '{{.Names}}' | grep -q "^${OLD_NAME}$"; then
135- docker rm -f "${OLD_NAME}" || true
136- fi
137-
138- # 새 컨테이너를 기본 이름으로 승격
139- docker rename "${NEW_NAME}" "${OLD_NAME}"
140-
141- echo "[7/7] 청소 (Dangling Images/Containers)"
142- docker system prune -f || true
143-
144- echo "🎉 롤링 완료: $(docker ps --filter "name=${OLD_NAME}" --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}')"
145- EOSCRIPT
146-
147- # jq로 이스케이프하여 commands에 넣기
148- JSON_SCRIPT=$(jq -Rn --arg s "$SCRIPT" '$s')
149-
150- CMD_ID=$(aws ssm send-command \
151- --document-name "AWS-RunShellScript" \
152- --instance-ids "$TARGET_ID" \
153- --parameters commands="[$JSON_SCRIPT]" \
154- --comment "Rolling deploy ${APP_NAME} -> ${PAYLOAD_IMAGE_URI}" \
155- --timeout-seconds 1800 \
156- --query "Command.CommandId" \
157- --output text)
158-
159- echo "SSM CommandId: ${CMD_ID}"
160- aws ssm wait command-executed --command-id "$CMD_ID" --instance-id "$TARGET_ID"
161-
162- # 실패 시 로그 출력
163- STATUS=$(aws ssm list-command-invocations --command-id "$CMD_ID" --instance-id "$TARGET_ID" --details \
164- --query "CommandInvocations[0].Status" --output text)
165- echo "SSM Status: $STATUS"
166- if [ "$STATUS" != "Success" ]; then
167- echo "---- SSM Output ----"
168- aws ssm list-command-invocations --command-id "$CMD_ID" --instance-id "$TARGET_ID" --details \
169- --query "CommandInvocations[0].CommandPlugins[0].{StdOut:Output,StdErr:StandardErrorContent}" --output json
170- exit 1
171- fi
172-
173- - name : Summary
174- if : steps.gate.outputs.GO == 'true'
46+ - name : Run on all tagged instances via SSM (by instance-ids)
47+ env :
48+ IDS_LIST : ${{ steps.resolve.outputs.ids_list }}
17549 run : |
176- echo "### ✅ EC2 Rolling Deploy Completed" >> $GITHUB_STEP_SUMMARY
177- echo "- Instance: \`${{ matrix.instance_id }}\`" >> $GITHUB_STEP_SUMMARY
178- echo "- Image: \`${PAYLOAD_IMAGE_URI}\`" >> $GITHUB_STEP_SUMMARY
179- echo "- Branch: \`${PAYLOAD_BRANCH}\`" >> $GITHUB_STEP_SUMMARY
180- echo "- Commit: \`${PAYLOAD_SHA}\`" >> $GITHUB_STEP_SUMMARY
50+ echo "Deploying image: $IMAGE"
51+
52+ aws ssm send-command \
53+ --region $AWS_REGION \
54+ --document-name "AWS-RunShellScript" \
55+ --instance-ids $IDS_LIST \
56+ --cloud-watch-output-config CloudWatchOutputEnabled=true,CloudWatchLogGroupName=/aws/ssm/kickytime \
57+ --parameters '{
58+ "commands": [
59+ "set -e",
60+ "command -v docker >/dev/null 2>&1 || (dnf -y install docker && systemctl enable --now docker)",
61+ "REG=$(echo \"'$IMAGE'\" | cut -d/ -f1)",
62+ "aws ecr get-login-password --region '$AWS_REGION' | docker login --username AWS --password-stdin $REG",
63+ "docker rm -f kickytime || true",
64+ "docker pull \"'$IMAGE'\"",
65+ "docker run -d --name kickytime -p '$PORT':'$PORT' \\",
66+ " -e SPRING_DATASOURCE_URL=\"'${{ secrets.DB_URL }}'\" \\",
67+ " -e SPRING_DATASOURCE_USERNAME=\"'${{ secrets.DB_USERNAME }}'\" \\",
68+ " -e SPRING_DATASOURCE_PASSWORD=\"'${{ secrets.DB_PASSWORD }}'\" \\",
69+ " \"'$IMAGE'\"",
70+ "for i in $(seq 1 60); do curl -fsS http://127.0.0.1:'$PORT$HEALTH' && break || sleep 2; done"
71+ ],
72+ "executionTimeout": ["1800"]
73+ }' \
74+ --comment "Deploy $IMAGE"
0 commit comments