Skip to content

๐Ÿ”ง config(gha): ์‹œ๋งจํ‹ฑ ๋ฆด๋ฆฌ์ฆˆ ์›Œํฌํ”Œ๋กœ ์ถ”๊ฐ€ #12

๐Ÿ”ง config(gha): ์‹œ๋งจํ‹ฑ ๋ฆด๋ฆฌ์ฆˆ ์›Œํฌํ”Œ๋กœ ์ถ”๊ฐ€

๐Ÿ”ง config(gha): ์‹œ๋งจํ‹ฑ ๋ฆด๋ฆฌ์ฆˆ ์›Œํฌํ”Œ๋กœ ์ถ”๊ฐ€ #12

Workflow file for this run

name: EC2 - Deploy (dispatch)

Check failure on line 1 in .github/workflows/ec2.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/ec2.yml

Invalid workflow file

(Line: 34, Col: 22): Unrecognized named-value: 'env'. Located at position 10 within expression: fromJson(env.INSTANCE_IDS_JSON), (Line: 34, Col: 22): Unexpected value '${{ fromJson(env.INSTANCE_IDS_JSON) }}'
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