diff --git a/.github/workflows/cd-release-deploy.yml b/.github/workflows/cd-release-deploy.yml new file mode 100644 index 0000000..68eb949 --- /dev/null +++ b/.github/workflows/cd-release-deploy.yml @@ -0,0 +1,182 @@ +name: CD (release) - Deploy to Prod + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: "Deploy version tag (e.g. 1.2.3)" + required: true + sha: + description: "Commit SHA to deploy (optional if version tag exists)" + required: false + update_latest: + description: "Also move :latest to this version?" + required: false + type: choice + options: ["true", "false"] + default: "true" + +permissions: + contents: read + packages: write + id-token: write + +concurrency: + group: deploy-prod-${{ github.ref }} + cancel-in-progress: false + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + + # App 설정 + APP_NAME: book-preprocessing-worker + APP_PORT: 9002 + + # AWS 설정 + AWS_REGION_PROD: ap-northeast-2 + + # SSM Tags (서버 1대) + PROD_ENV_TAG_KEY: "tag:Env" + PROD_ENV_TAG_VALUE: "prod" + PROD_NODE_TAG_KEY: "tag:Node" + PROD_NODE_TAG_VALUE: "worker-bundle" + +jobs: + # 1) 이미지 준비 및 태그 승격 + prepare-image: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.out.outputs.version }} + update_latest: ${{ steps.out.outputs.update_latest }} + steps: + - name: Resolve VERSION & UPDATE_LATEST + shell: bash + run: | + if [[ "${{ github.event_name }}" == "release" ]]; then + echo "VERSION=${GITHUB_REF_NAME}" >> "$GITHUB_ENV" + echo "UPDATE_LATEST=true" >> "$GITHUB_ENV" + else + echo "VERSION=${{ inputs.version }}" >> "$GITHUB_ENV" + echo "UPDATE_LATEST=${{ inputs.update_latest || 'true' }}" >> "$GITHUB_ENV" + fi + + - name: Resolve SHA + shell: bash + run: | + if [[ "${{ github.event_name }}" == "release" ]]; then + echo "SHA=${{ github.sha }}" >> "$GITHUB_ENV" + else + echo "SHA=${{ inputs.sha }}" >> "$GITHUB_ENV" + fi + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Ensure/Promote Image + shell: bash + run: | + set -euo pipefail + TARGET="${REGISTRY}/${IMAGE_NAME}:${VERSION}" + if docker buildx imagetools inspect "$TARGET" >/dev/null 2>&1; then + echo "✅ Image $TARGET already exists." + else + SRC="${REGISTRY}/${IMAGE_NAME}:main-${SHA}" + echo "🚀 Promoting $SRC to $TARGET" + docker buildx imagetools create "$SRC" --tag "$TARGET" + fi + + - id: out + shell: bash + run: | + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "update_latest=${UPDATE_LATEST}" >> "$GITHUB_OUTPUT" + + # 2) PROD 배포 (서버 1대) + deploy-prod: + needs: prepare-image + runs-on: ubuntu-latest + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ env.AWS_REGION_PROD }} + role-to-assume: ${{ secrets.AWS_DEPLOY_PROD_ROLE_ARN }} + + - name: Deploy to prod (worker-bundle) + shell: bash + env: + VERSION: ${{ needs.prepare-image.outputs.version }} + GHCR_USER: ${{ secrets.GHCR_USER }} + GHCR_PAT: ${{ secrets.GHCR_PAT }} + APP_NAME: ${{ env.APP_NAME }} + APP_PORT: ${{ env.APP_PORT }} + run: | + set -euo pipefail + + RAW_SCRIPT=$(cat <<'EOF' + #!/bin/bash + set -euo pipefail + + export APP_NAME="$APP_NAME" + export APP_PORT="$APP_PORT" + export VERSION="$VERSION" + + cd "/srv/todaybook/${APP_NAME}" + echo "$GHCR_PAT" | sudo docker login ghcr.io -u "$GHCR_USER" --password-stdin + chmod +x deploy.sh + ./deploy.sh + EOF + ) + + export GHCR_USER GHCR_PAT APP_NAME APP_PORT VERSION + export SCRIPT=$(echo "$RAW_SCRIPT" | envsubst '$GHCR_USER $GHCR_PAT $APP_NAME $APP_PORT $VERSION') + + PARAMS=$(python3 -c "import json, os; print(json.dumps({'commands': os.environ['SCRIPT'].splitlines()}))") + + CMD_ID=$(aws ssm send-command \ + --document-name "AWS-RunShellScript" \ + --targets "Key=${PROD_ENV_TAG_KEY},Values=${PROD_ENV_TAG_VALUE}" "Key=${PROD_NODE_TAG_KEY},Values=${PROD_NODE_TAG_VALUE}" \ + --parameters "$PARAMS" \ + --query "Command.CommandId" --output text) + + echo "Waiting for SSM Command: $CMD_ID" + while true; do + STATUS=$(aws ssm list-command-invocations \ + --command-id "$CMD_ID" --details \ + --query "CommandInvocations[0].Status" --output text 2>/dev/null || echo "Pending") + echo "Current status: $STATUS" + if [[ "$STATUS" == "Success" ]]; then break; + elif [[ "$STATUS" =~ ^(Failed|Cancelled|TimedOut)$ ]]; then + aws ssm list-command-invocations --command-id "$CMD_ID" --details + exit 1 + fi + sleep 5 + done + + # 3) latest 태그 갱신 (배포 성공 후) + update-latest: + needs: [prepare-image, deploy-prod] + if: ${{ needs.prepare-image.outputs.update_latest == 'true' }} + runs-on: ubuntu-latest + steps: + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Tag Latest + shell: bash + run: | + set -euo pipefail + VERSION="${{ needs.prepare-image.outputs.version }}" + docker buildx imagetools create "${REGISTRY}/${IMAGE_NAME}:${VERSION}" --tag "${REGISTRY}/${IMAGE_NAME}:latest" \ No newline at end of file diff --git a/.github/workflows/ci-main-build-push.yml b/.github/workflows/ci-main-build-push.yml new file mode 100644 index 0000000..ed87ac3 --- /dev/null +++ b/.github/workflows/ci-main-build-push.yml @@ -0,0 +1,69 @@ +name: CI (main) - Build & Push Image + +on: + push: + branches: ["main"] + workflow_dispatch: + +permissions: + contents: read + packages: write # GHCR push에 필요 + +concurrency: + group: main-image-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build_push: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + cache: gradle + + - name: Build (skip tests optional) + run: ./gradlew clean build --no-daemon + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # 태그: main-, main-latest + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=main-${{ github.sha }} + type=raw,value=main-latest + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max