Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.git
.github
.gradle
.idea
.codex
build
out
context
.env
.env.*
!.env.example
*.log
logs
HELP.md
157 changes: 105 additions & 52 deletions .github/workflows/deploy-to-dev-ec2-docker.yml
Original file line number Diff line number Diff line change
@@ -1,35 +1,49 @@
name: Deploy Admin to Dev (EC2 + Docker Compose)
name: Deploy Admin to Dev (ARM64 EC2 + Docker Compose)

on:
workflow_dispatch:
push:
branches: [ develop ]
workflow_dispatch: {}

concurrency:
group: deploy-admin-dev
cancel-in-progress: false

jobs:
deploy:
# 서버 전환이 끝나기 전에는 develop 머지가 새 서버를 자동으로 변경하지 않게 한다.
# 검증이 끝난 뒤 저장소 변수 DEV_ARM64_AUTODEPLOY를 true로 설정한다.
if: ${{ github.event_name == 'workflow_dispatch' || vars.DEV_ARM64_AUTODEPLOY == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
environment: dev

steps:
- uses: actions/checkout@v4
- name: 🧾 Checkout
uses: actions/checkout@v4

# (옵션) BE와 동일 스타일 - 리포지토리명 소문자/메타 태그
- name: Normalize image repo (lowercase)
- name: 🔡 Normalize image repo (lowercase)
id: normalize
run: echo "image_repo=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"

- name: Docker Buildx
- name: 🏗️ Setup QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64

- name: 🧱 Setup Buildx
uses: docker/setup-buildx-action@v3

- name: GHCR login
- name: 🔐 Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USERNAME }}
password: ${{ secrets.GHCR_TOKEN }}

- name: Meta (tags/labels)
- name: 🏷️ Meta (tags/labels)
id: meta
uses: docker/metadata-action@v5
with:
Expand All @@ -38,83 +52,122 @@ jobs:
type=raw,value=dev-latest
type=sha,format=long,prefix=dev-

- name: Build & Push
- name: 🏗️ Build & Push ARM64 Image
id: build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache
cache-to: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache,mode=max
cache-from: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache-arm64
cache-to: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache-arm64,mode=max

- name: 📁 Prepare deployment directory
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
script: |
set -euo pipefail
# 최초 배포 시에도 사용자가 디렉터리를 직접 만들 필요가 없게 한다.
install -d -m 0750 "${HOME}/saerok-admin"

- name: Copy compose files to EC2
- name: 📤 Copy compose files to EC2
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
source: "deploy/docker-compose.yml,deploy/docker-compose.dev.yml"
target: "~/saerok-admin"
# 기존 서버와 동일하게 ubuntu 사용자의 홈 아래에 배포한다.
target: ~/saerok-admin/
strip_components: 1

- name: Deploy on EC2
- name: 🚀 Deploy on EC2
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
script: |
set -euo pipefail
cd ~/saerok-admin

# 0) Docker & Compose must exist

DEPLOY_DIR="${HOME}/saerok-admin"
IMAGE_REF="${{ steps.normalize.outputs.image_repo }}@${{ steps.build.outputs.digest }}"
cd "$DEPLOY_DIR"

# 0) Docker와 Compose가 설치되어 있는지 확인한다.
docker --version
docker compose version
# 1) GHCR login for pull

# 1) 관리자 이미지 pull을 위해 GHCR에 로그인한다.
echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin

# 2) Create ephemeral env file in RAM (owned by current user) and ensure cleanup
# /run 은 tmpfs라 재부팅 시 날아감(의도된 동작)

# 2) 인증정보는 디스크의 배포 폴더가 아닌 /run의 임시 파일로 전달한다.
sudo install -d -m 0700 -o "$(id -un)" -g "$(id -gn)" /run/saerok-admin

ENV_FILE="/run/saerok-admin/env.dev"
umask 177
: > "$ENV_FILE"
cleanup() { shred -u "$ENV_FILE" || rm -f "$ENV_FILE"; }
trap cleanup EXIT

add_kv() { printf '%s=%s\n' "$1" "$2" >> "$ENV_FILE"; }

# 2-1) Runtime envs (dev)

add_kv() { printf '%s\n' "$1=$2" >> "$ENV_FILE"; }
add_kv SPRING_PROFILES_ACTIVE "dev"
add_kv SAEROK_API_BASE_URL "${{ secrets.SAEROK_API_BASE_URL }}"
add_kv SAEROK_API_PREFIX "${{ secrets.SAEROK_API_PREFIX }}"

# Social login (admin 전용 redirect)
add_kv KAKAO_CLIENT_ID "${{ secrets.KAKAO_CLIENT_ID }}"
add_kv KAKAO_REDIRECT_URI "${{ secrets.KAKAO_REDIRECT_URI }}"
add_kv APPLE_CLIENT_ID "${{ secrets.APPLE_CLIENT_ID }}"
add_kv APPLE_REDIRECT_URI "${{ secrets.APPLE_REDIRECT_URI }}"
add_kv SAEROK_API_PREFIX "${{ secrets.SAEROK_API_PREFIX }}"
add_kv KAKAO_CLIENT_ID "${{ secrets.KAKAO_CLIENT_ID }}"
add_kv KAKAO_REDIRECT_URI "${{ secrets.KAKAO_REDIRECT_URI }}"
add_kv APPLE_CLIENT_ID "${{ secrets.APPLE_CLIENT_ID }}"
add_kv APPLE_REDIRECT_URI "${{ secrets.APPLE_REDIRECT_URI }}"
add_kv UNSPLASH_ACCESS_KEY "${{ secrets.UNSPLASH_ACCESS_KEY }}"
add_kv UNSPLASH_APP_NAME "${{ secrets.UNSPLASH_APP_NAME }}"

# 3) Pull & Up with dev overlay (limits in dev.yml)
docker compose -p saerok-admin -f docker-compose.yml -f docker-compose.dev.yml pull --quiet
docker compose -p saerok-admin -f docker-compose.yml -f docker-compose.dev.yml up -d --force-recreate --quiet-pull

# 4) Healthcheck: /login (HEAD OK)
add_kv UNSPLASH_APP_NAME "${{ secrets.UNSPLASH_APP_NAME }}"
add_kv IMAGE_REF "$IMAGE_REF"
add_kv ENV_FILE "$ENV_FILE"

# 3) 이번 실행에서 만든 digest 이미지로 관리자 컨테이너를 교체한다.
docker pull "$IMAGE_REF"
docker compose \
--env-file "$ENV_FILE" \
-p saerok-admin \
-f docker-compose.yml \
-f docker-compose.dev.yml \
up -d --force-recreate --quiet-pull app

docker compose \
--env-file "$ENV_FILE" \
-p saerok-admin \
-f docker-compose.yml \
-f docker-compose.dev.yml \
ps

# 4) Nginx를 거치기 전 localhost에서 로그인 화면을 확인한다.
echo "Waiting for admin login page..."
for i in {1..45}; do
if curl -fsS -I http://localhost:8081/login > /dev/null; then
echo "✅ admin up"
docker image prune -af --filter "until=24h" || true
docker builder prune -af --filter "until=24h" || true
exit 0
fi
echo "⏳ ($i/45)"; sleep 3
if curl -fsS -I http://127.0.0.1:8081/login > /dev/null; then
echo "Admin healthcheck passed"
docker image prune -af --filter "until=24h" || true
docker builder prune -af --filter "until=24h" || true
exit 0
fi
echo "Waiting ($i/45)..."
sleep 3
done

echo "❌ admin health check failed"
docker logs saerok-admin-dev --tail=200 || true
exit 1

echo "Admin healthcheck failed after retries"
# 세션이나 인증정보가 포함될 수 있는 원문 로그는 공개 Actions에 출력하지 않는다.
docker inspect --format 'admin status={{.State.Status}} health={{.State.Health.Status}} restart={{.RestartCount}}' saerok-admin-dev || true
exit 1

- name: Summarize deployment
if: ${{ success() }}
run: |
{
echo "## Dev admin arm64 deploy"
echo "- Platform: linux/arm64"
echo "- Image: ${{ steps.normalize.outputs.image_repo }}@${{ steps.build.outputs.digest }}"
echo "- Commit: ${{ github.sha }}"
} >> "$GITHUB_STEP_SUMMARY"
39 changes: 34 additions & 5 deletions .github/workflows/deploy-to-prod-ec2-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,30 @@ on:
branches:
- main
workflow_dispatch:
inputs:
deploy_mode:
description: 앱을 배포하거나 이전 검증을 위해 이미지만 빌드·업로드합니다
type: choice
required: true
default: full
options:
- full
- image-only

concurrency:
group: saerok-admin-prod-deploy
cancel-in-progress: false

jobs:
deploy:
if: ${{ github.event_name == 'workflow_dispatch' || vars.PROD_ARM64_AUTODEPLOY == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
environment: prod
env:
DEPLOY_MODE: ${{ inputs.deploy_mode || 'full' }}

steps:
- uses: actions/checkout@v4
Expand All @@ -22,6 +38,11 @@ jobs:
id: normalize
run: echo "image_repo=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"

- name: Set up ARM64 emulation
uses: docker/setup-qemu-action@v3
with:
platforms: arm64

- name: Docker Buildx
uses: docker/setup-buildx-action@v3

Expand All @@ -38,21 +59,25 @@ jobs:
with:
images: ${{ steps.normalize.outputs.image_repo }}
tags: |
type=raw,value=prod-latest
type=sha,format=long,prefix=prod-
type=raw,value=prod-latest,enable=${{ env.DEPLOY_MODE == 'full' }}
type=sha,format=long,prefix=prod-,enable=${{ env.DEPLOY_MODE == 'full' }}
type=raw,value=migration-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }},enable=${{ env.DEPLOY_MODE == 'image-only' }}

- name: Build & Push
id: build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache
cache-to: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache,mode=max

- name: Copy compose files to EC2
if: ${{ env.DEPLOY_MODE == 'full' }}
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
Expand All @@ -63,6 +88,7 @@ jobs:
strip_components: 1

- name: Deploy on EC2
if: ${{ env.DEPLOY_MODE == 'full' }}
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
Expand All @@ -84,6 +110,7 @@ jobs:
sudo install -d -m 0700 -o "$(id -un)" -g "$(id -gn)" /run/saerok-admin

ENV_FILE="/run/saerok-admin/env.prod"
export IMAGE_REF="${{ steps.normalize.outputs.image_repo }}@${{ steps.build.outputs.digest }}"
umask 177
: > "$ENV_FILE"
cleanup() { shred -u "$ENV_FILE" || rm -f "$ENV_FILE"; }
Expand All @@ -109,7 +136,7 @@ jobs:

# 4) Healthcheck: /login (HEAD OK)
for i in {1..45}; do
if curl -fsS -I http://localhost:8081/login > /dev/null; then
if curl -fsS --max-time 2 -I http://localhost:8081/login > /dev/null; then
echo "✅ admin up"
docker image prune -af --filter "until=24h" || true
docker builder prune -af --filter "until=24h" || true
Expand All @@ -119,5 +146,7 @@ jobs:
done

echo "❌ admin health check failed"
docker logs saerok-admin-prod --tail=200 || true
exit 1
# 세션이나 인증정보가 포함될 수 있는 원문 로그는 공개 Actions에 출력하지 않는다.
# 상세 원인은 EC2에 접속해 권한이 있는 운영자가 직접 확인한다.
docker inspect --format 'admin status={{.State.Status}} oom={{.State.OOMKilled}} restart={{.RestartCount}}' saerok-admin-prod || true
exit 1
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ===== Build stage =====
FROM gradle:8.8-jdk21 AS builder
# syntax=docker/dockerfile:1

# Spring Boot JAR는 CPU 아키텍처에 종속되지 않으므로 빌드는 CI 러너에서 수행한다.
FROM --platform=$BUILDPLATFORM gradle:8.8-jdk21 AS builder
WORKDIR /app

COPY build.gradle settings.gradle gradlew ./
Expand All @@ -11,7 +13,7 @@ COPY . .
RUN chmod +x gradlew
RUN ./gradlew --no-daemon clean bootJar

# ===== Runtime stage =====
# 실행 이미지는 Buildx가 요청한 대상 아키텍처(개발 arm64, 운영 amd64)를 따른다.
FROM eclipse-temurin:21-jre
ENV TZ=Asia/Seoul
WORKDIR /app
Expand Down
Loading
Loading