Skip to content

fix(ci/cd): Update archive file path in Lightsail deployment workflow #9

fix(ci/cd): Update archive file path in Lightsail deployment workflow

fix(ci/cd): Update archive file path in Lightsail deployment workflow #9

name: Build and Deploy to PROD (Lightsail Blue/Green via SSH + Docker)
on:
push:
branches: [ "main-test" ]
env:
# --- 애플리케이션/컨테이너 공통 ---
PROJECT_NAME: "devdevdev"
IMAGE_NAME: "devdevdev/app" # 로컬 빌드 이미지 이름(태그 latest, SHA)
CONTAINER_BASE: "devdevdev-main-server" # 컨테이너 베이스명
BLUE_SUFFIX: "-blue"
GREEN_SUFFIX: "-green"
# --- 포트 구성 ---
BLUE_PORT: "18080" # 호스트 포트(Blue)
GREEN_PORT: "18081" # 호스트 포트(Green)
APP_PORT: "8080" # 컨테이너 내부 포트(Spring Boot)
# --- 헬스체크 ---
HEALTHCHECK_PATH: "/actuator/health" # Actuator 미사용이면 "/" 로 변경하세요
HEALTHCHECK_TIMEOUT: "3" # curl 타임아웃(초)
HEALTHCHECK_RETRY: "20" # 재시도 횟수 (약 60초)
# --- SSH/Lightsail ---
SSH_USER: "ec2-user" # SSH 접속 사용자
LIGHTSAIL_HOST: "${{ secrets.LIGHTSAIL_HOST }}" # 퍼블릭 IP 또는 도메인
jobs:
build:
name: Build and Deploy (Blue/Green)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# ====== 리소스/시크릿 주입 (현재 파이프라인과 동일) ======
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: 21
distribution: corretto
- name: make application-prod.yml
run: |
cd ./src/main/resources
echo "${{ secrets.application_prod }}" >> ./application-prod.yml
echo "${{ secrets.application_jwt_prod }}" >> ./application-jwt-prod.yml
echo "${{ secrets.application_oauth2_prod }}" >> ./application-oauth2-prod.yml
echo "${{ secrets.application_storage_s3_prod }}" >> ./application-storage-s3-prod.yml
echo "${{ secrets.application_open_ai }}" >> ./application-open-ai.yml
echo "${{ secrets.application_opensearch_prod }}" >> ./application-opensearch-prod.yml
- name: make application-test.yml
run: |
cd ./src/test/resources
echo "${{ secrets.application_storage_s3 }}" >> ./application-storage-s3.yml
echo "${{ secrets.application_open_ai }}" >> ./application-open-ai.yml
echo "${{ secrets.application_opensearch_test }}" >> ./application-opensearch-test.yml
# ====== Gradle 빌드 (Docker가 JAR을 COPY할 수 있도록 선행) ======
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Build with Gradle (bootJar)
run: ./gradlew bootJar -x test -x asciidoctor
# ====== Docker 빌드 ======
- name: Use Dockerfile-prod if present
run: |
if [ -f Dockerfile-prod ]; then
rm -f Dockerfile
cp Dockerfile-prod Dockerfile
fi
- name: Build Docker image
run: |
docker build \
-t ${IMAGE_NAME}:${GITHUB_SHA} \
-t ${IMAGE_NAME}:latest \
.
- name: Save image as archive
run: |
mkdir -p out
docker save ${IMAGE_NAME}:${GITHUB_SHA} | gzip > out/image.tar.gz
echo "ARCHIVE=out/image.tar.gz" >> $GITHUB_ENV
# ====== SSH 준비 ======
- name: Prepare SSH key
run: |
echo "${{ secrets.LIGHTSAIL_SSH_KEY }}" > key.pem
chmod 600 key.pem
mkdir -p ~/.ssh
ssh-keyscan -H ${LIGHTSAIL_HOST} >> ~/.ssh/known_hosts
# ====== 아카이브/스크립트 전송 ======
- name: Upload image archive
run: |
scp -i key.pem -o StrictHostKeyChecking=yes "$ARCHIVE" \
${SSH_USER}@${LIGHTSAIL_HOST}:/home/${SSH_USER}/image.tar.gz
- name: Upload blue/green deploy script
run: |
cat > deploy_blue_green.sh <<'EOS'
#!/usr/bin/env bash
set -euo pipefail
sudo systemctl enable --now docker >/dev/null 2>&1 || true
IMAGE_NAME=${IMAGE_NAME:-devdevdev/app}
CONTAINER_BASE=${CONTAINER_BASE:-devdevdev-main-server}
BLUE_SUFFIX=${BLUE_SUFFIX:--blue}
GREEN_SUFFIX=${GREEN_SUFFIX:--green}
BLUE_PORT=${BLUE_PORT:-18080}
GREEN_PORT=${GREEN_PORT:-18081}
APP_PORT=${APP_PORT:-8080}
HEALTHCHECK_PATH=${HEALTHCHECK_PATH:-/} # actuator 없으면 /
HEALTHCHECK_TIMEOUT=${HEALTHCHECK_TIMEOUT:-3}
HEALTHCHECK_RETRY=${HEALTHCHECK_RETRY:-20}
UPSTREAM_FILE="/etc/nginx/conf.d/backend-upstream.upstream"
BLUE_NAME="${CONTAINER_BASE}${BLUE_SUFFIX}"
GREEN_NAME="${CONTAINER_BASE}${GREEN_SUFFIX}"
# 아카이브 경로는 HOME 기준으로
ARCHIVE_FILE="$HOME/image.tar.gz"
echo "[1/9] Load image: ${ARCHIVE_FILE}"
ls -lh "${ARCHIVE_FILE}" || { echo "[!] archive missing"; exit 1; }
gzip -t "${ARCHIVE_FILE}"
gunzip -c "${ARCHIVE_FILE}" | sudo docker load
ACTIVE_PORT=""
if [ -f "${UPSTREAM_FILE}" ]; then
ACTIVE_PORT=$(grep -oE '127\.0\.0\.1:([0-9]+)' "${UPSTREAM_FILE}" | awk -F: '{print $2}' || true)
fi
if [ -z "${ACTIVE_PORT}" ]; then
echo "server 127.0.0.1:${BLUE_PORT};" | sudo tee "${UPSTREAM_FILE}" >/dev/null
ACTIVE_PORT="${BLUE_PORT}"
fi
echo "[2/9] Current active port: ${ACTIVE_PORT}"
if [ "${ACTIVE_PORT}" = "${BLUE_PORT}" ]; then
TARGET_NAME="${GREEN_NAME}"; TARGET_PORT="${GREEN_PORT}"
OLD_NAME="${BLUE_NAME}"; OLD_PORT="${BLUE_PORT}"
else
TARGET_NAME="${BLUE_NAME}"; TARGET_PORT="${BLUE_PORT}"
OLD_NAME="${GREEN_NAME}"; OLD_PORT="${GREEN_PORT}"
fi
echo "[3/9] Target container: ${TARGET_NAME} on ${TARGET_PORT}"
if sudo docker ps -a --format '{{.Names}}' | grep -qw "${TARGET_NAME}"; then
sudo docker stop "${TARGET_NAME}" || true
sudo docker rm "${TARGET_NAME}" || true
fi
echo "[4/9] Run new container"
sudo docker run -d \
--name "${TARGET_NAME}" \
--restart=always \
-p 127.0.0.1:${TARGET_PORT}:${APP_PORT} \
-e SPRING_PROFILES_ACTIVE=prod \
${IMAGE_NAME}:latest
echo "[5/9] Health check http://127.0.0.1:${TARGET_PORT}${HEALTHCHECK_PATH}"
ok=0
for i in $(seq 1 ${HEALTHCHECK_RETRY}); do
if curl -fsS --max-time ${HEALTHCHECK_TIMEOUT} "http://127.0.0.1:${TARGET_PORT}${HEALTHCHECK_PATH}" >/dev/null 2>&1; then
ok=1; break
fi
echo " retry $i/${HEALTHCHECK_RETRY}..."
sleep 3
done
if [ "$ok" -ne 1 ]; then
echo "[!] Health check failed. Rollback."
sudo docker logs --tail 200 "${TARGET_NAME}" || true
sudo docker stop "${TARGET_NAME}" || true
sudo docker rm "${TARGET_NAME}" || true
exit 1
fi
echo "[6/9] Switch upstream to ${TARGET_PORT}"
echo "server 127.0.0.1:${TARGET_PORT};" | sudo tee "${UPSTREAM_FILE}" >/dev/null
sudo nginx -t
sudo systemctl reload nginx
echo "[7/9] Stop old container: ${OLD_NAME} (if any)"
if sudo docker ps -a --format '{{.Names}}' | grep -qw "${OLD_NAME}"; then
sudo docker stop "${OLD_NAME}" || true
sudo docker rm "${OLD_NAME}" || true
fi
echo "[8/9] Cleanup old archives (keep last 3)"
cd "/home/${SSH_USER}" && ls -t image*.tar.gz | tail -n +4 | xargs -r rm -f
echo "[9/9] Done."
EOS
chmod +x deploy_blue_green.sh
scp -i key.pem -o StrictHostKeyChecking=yes deploy_blue_green.sh ${SSH_USER}@${LIGHTSAIL_HOST}:/home/${SSH_USER}/
# ====== 원격 실행 ======
- name: Remote Blue/Green deploy
run: |
ssh -i key.pem -o StrictHostKeyChecking=yes ${SSH_USER}@${LIGHTSAIL_HOST} \
"bash /home/${SSH_USER}/deploy_blue_green.sh"
# ====== Slack 알림 ======
- name: action-slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
author_name: "[PROD] 배포 결과를 알려드려요"
fields: repo,message,commit,author,eventName,ref,took
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: always()