🔧 config(actions): 시맨틱 릴리즈 안정화를 위해 checkout에 ref=${{ github.sha }} 및 … #14
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: EC2 - Deploy (dispatch) | ||
|
Check failure on line 1 in .github/workflows/ec2.yml
|
||
| on: | ||
| repository_dispatch: | ||
| types: [deploy-ec2] | ||
| workflow_dispatch: | ||
| concurrency: | ||
| group: ec2-deploy | ||
| cancel-in-progress: false | ||
| permissions: | ||
| contents: read | ||
| env: | ||
| AWS_REGION: ap-northeast-2 | ||
| # ✅ 배포 대상 EC2 리스트(JSON 배열). 필요에 맞게 교체하세요. | ||
| INSTANCE_IDS_JSON: '["i-08f17d18d83292c07","i-08316767f2bd4a5ed"]' | ||
| # ✅ 앱/컨테이너 설정 | ||
| APP_NAME: kickytime # 기존 컨테이너 기본 이름 (예: kickytime) | ||
| CONTAINER_PORT: "8080" # 컨테이너 내부 포트 | ||
| HOST_PORT_BASE: "8080" # 기존 컨테이너가 바인드하는 호스트 포트 | ||
| HEALTH_PATH: "/actuator/health" # 헬스체크 경로 | ||
| HEALTH_TIMEOUT_SEC: "180" # 헬스체크 최대 대기시간 | ||
| ENV_FILE_PATH: "" # 예: /opt/kickytime/.env (없으면 빈 값) | ||
| jobs: | ||
| deploy: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| max-parallel: 1 | ||
| matrix: | ||
| instance_id: ${{ fromJson(env.INSTANCE_IDS_JSON) }} | ||
| env: | ||
| PAYLOAD_IMAGE_URI: ${{ github.event.client_payload.image_uri }} | ||
| PAYLOAD_TAG: ${{ github.event.client_payload.tag }} | ||
| PAYLOAD_BRANCH: ${{ github.event.client_payload.branch }} | ||
| PAYLOAD_SHA: ${{ github.event.client_payload.sha }} | ||
| steps: | ||
| - name: Gate - only deploy for main | ||
| id: gate | ||
| run: | | ||
| if [ "${PAYLOAD_BRANCH}" = "main" ]; then | ||
| echo "GO=true" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "GO=false" >> $GITHUB_OUTPUT | ||
| echo "Non-deploy branch: ${PAYLOAD_BRANCH}" | ||
| fi | ||
| - name: Checkout (same commit as build) | ||
| if: steps.gate.outputs.GO == 'true' | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ env.PAYLOAD_SHA }} | ||
| - name: Configure AWS credentials | ||
| if: steps.gate.outputs.GO == 'true' | ||
| uses: aws-actions/configure-aws-credentials@v4 | ||
| with: | ||
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | ||
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | ||
| aws-region: ${{ env.AWS_REGION }} | ||
| - name: Send rolling deploy command via SSM | ||
| if: steps.gate.outputs.GO == 'true' | ||
| id: ssm | ||
| run: | | ||
| set -e | ||
| TARGET_ID="${{ matrix.instance_id }}" | ||
| echo "Deploying to instance: $TARGET_ID" | ||
| # 새 컨테이너가 바인드할 임시 포트(현재 HOST_PORT_BASE+1) | ||
| NEXT_PORT=$(( ${HOST_PORT_BASE} + 1 )) | ||
| # SSM에 보낼 스크립트를 JSON 안전하게 변환 | ||
| read -r -d '' SCRIPT <<'EOSCRIPT' | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
| APP_NAME="${APP_NAME}" | ||
| IMAGE_URI="${PAYLOAD_IMAGE_URI}" | ||
| CONTAINER_PORT="${CONTAINER_PORT}" | ||
| HOST_PORT_BASE="${HOST_PORT_BASE}" | ||
| NEXT_PORT=$(( HOST_PORT_BASE + 1 )) | ||
| HEALTH_PATH="${HEALTH_PATH}" | ||
| HEALTH_TIMEOUT="${HEALTH_TIMEOUT_SEC}" | ||
| ENV_FILE_PATH="${ENV_FILE_PATH}" | ||
| OLD_NAME="${APP_NAME}" | ||
| NEW_NAME="${APP_NAME}_new" | ||
| echo "[1/7] ECR 로그인" | ||
| REGISTRY_HOST="$(echo "$IMAGE_URI" | awk -F'/' '{print $1}')" | ||
| aws ecr get-login-password --region "${AWS_REGION}" | docker login --username AWS --password-stdin "${REGISTRY_HOST}" | ||
| echo "[2/7] 새 이미지 Pull: ${IMAGE_URI}" | ||
| docker pull "${IMAGE_URI}" | ||
| echo "[3/7] 이전 NEW 컨테이너 정리(있으면)" | ||
| if docker ps -a --format '{{.Names}}' | grep -q "^${NEW_NAME}$"; then | ||
| docker rm -f "${NEW_NAME}" || true | ||
| fi | ||
| echo "[4/7] 새 컨테이너 기동: ${NEW_NAME} (host ${NEXT_PORT} -> container ${CONTAINER_PORT})" | ||
| RUN_ENV_OPTS=() | ||
| if [ -n "${ENV_FILE_PATH}" ]; then | ||
| RUN_ENV_OPTS+=( --env-file "${ENV_FILE_PATH}" ) | ||
| fi | ||
| docker run -d \ | ||
| --name "${NEW_NAME}" \ | ||
| -p "${NEXT_PORT}:${CONTAINER_PORT}" \ | ||
| "${RUN_ENV_OPTS[@]}" \ | ||
| "${IMAGE_URI}" | ||
| echo "[5/7] 헬스체크: http://127.0.0.1:${NEXT_PORT}${HEALTH_PATH} (timeout: ${HEALTH_TIMEOUT}s)" | ||
| SECS=0 | ||
| until curl -fsS "http://127.0.0.1:${NEXT_PORT}${HEALTH_PATH}" >/dev/null 2>&1; do | ||
| sleep 3 | ||
| SECS=$(( SECS + 3 )) | ||
| if [ "${SECS}" -ge "${HEALTH_TIMEOUT}" ]; then | ||
| echo "❌ 새 컨테이너 헬스체크 실패. 로그:" | ||
| docker logs --tail 200 "${NEW_NAME}" || true | ||
| exit 1 | ||
| fi | ||
| done | ||
| echo "✅ 새 컨테이너 Healthy" | ||
| echo "[6/7] 기존 컨테이너 ${OLD_NAME} 종료/삭제 및 포트 스위칭" | ||
| if docker ps -a --format '{{.Names}}' | grep -q "^${OLD_NAME}$"; then | ||
| docker rm -f "${OLD_NAME}" || true | ||
| fi | ||
| # 새 컨테이너를 기본 이름으로 승격 | ||
| docker rename "${NEW_NAME}" "${OLD_NAME}" | ||
| echo "[7/7] 청소 (Dangling Images/Containers)" | ||
| docker system prune -f || true | ||
| echo "🎉 롤링 완료: $(docker ps --filter "name=${OLD_NAME}" --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}')" | ||
| EOSCRIPT | ||
| # jq로 이스케이프하여 commands에 넣기 | ||
| JSON_SCRIPT=$(jq -Rn --arg s "$SCRIPT" '$s') | ||
| CMD_ID=$(aws ssm send-command \ | ||
| --document-name "AWS-RunShellScript" \ | ||
| --instance-ids "$TARGET_ID" \ | ||
| --parameters commands="[$JSON_SCRIPT]" \ | ||
| --comment "Rolling deploy ${APP_NAME} -> ${PAYLOAD_IMAGE_URI}" \ | ||
| --timeout-seconds 1800 \ | ||
| --query "Command.CommandId" \ | ||
| --output text) | ||
| echo "SSM CommandId: ${CMD_ID}" | ||
| aws ssm wait command-executed --command-id "$CMD_ID" --instance-id "$TARGET_ID" | ||
| # 실패 시 로그 출력 | ||
| STATUS=$(aws ssm list-command-invocations --command-id "$CMD_ID" --instance-id "$TARGET_ID" --details \ | ||
| --query "CommandInvocations[0].Status" --output text) | ||
| echo "SSM Status: $STATUS" | ||
| if [ "$STATUS" != "Success" ]; then | ||
| echo "---- SSM Output ----" | ||
| aws ssm list-command-invocations --command-id "$CMD_ID" --instance-id "$TARGET_ID" --details \ | ||
| --query "CommandInvocations[0].CommandPlugins[0].{StdOut:Output,StdErr:StandardErrorContent}" --output json | ||
| exit 1 | ||
| fi | ||
| - name: Summary | ||
| if: steps.gate.outputs.GO == 'true' | ||
| run: | | ||
| echo "### ✅ EC2 Rolling Deploy Completed" >> $GITHUB_STEP_SUMMARY | ||
| echo "- Instance: \`${{ matrix.instance_id }}\`" >> $GITHUB_STEP_SUMMARY | ||
| echo "- Image: \`${PAYLOAD_IMAGE_URI}\`" >> $GITHUB_STEP_SUMMARY | ||
| echo "- Branch: \`${PAYLOAD_BRANCH}\`" >> $GITHUB_STEP_SUMMARY | ||
| echo "- Commit: \`${PAYLOAD_SHA}\`" >> $GITHUB_STEP_SUMMARY | ||