From 96266f74ee464dee39744e8aa9df461f419be037 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Sat, 9 May 2026 04:16:39 +0900 Subject: [PATCH] Add dev remote PC deploy workflow --- .github/workflows/deploy-dev.yml | 169 ++++++++++++++++++ docs/deployment.md | 81 ++++++++- docs/git-workflow.md | 42 +++-- ontime-back/docker-compose.dev.yml | 37 ++++ .../main/resources/application-dev.properties | 53 ++++++ 5 files changed, 362 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/deploy-dev.yml create mode 100644 ontime-back/docker-compose.dev.yml create mode 100644 ontime-back/src/main/resources/application-dev.properties diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..836aaab --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,169 @@ +name: Deploy Dev + +on: + workflow_dispatch: + push: + branches: + - dev + +permissions: + contents: read + packages: write + +concurrency: + group: deploy-development + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: devkor-github/ontime-back + IMAGE_TAG: dev-${{ github.sha }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - 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 }} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: ./ontime-back + file: ./ontime-back/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev-latest + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy-to-remote-pc: + needs: build-and-push + runs-on: ubuntu-latest + environment: development + env: + DEV_DEPLOY_DIR: ${{ secrets.DEV_DEPLOY_DIR || format('/home/{0}/OnTime-back-dev', secrets.DEV_REMOTE_USER) }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Prepare dev deploy directory + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEV_REMOTE_HOST }} + username: ${{ secrets.DEV_REMOTE_USER }} + key: ${{ secrets.DEV_REMOTE_SSH_KEY }} + script: | + set -eu + mkdir -p "${{ env.DEV_DEPLOY_DIR }}" + + - name: Upload compose files to remote PC + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.DEV_REMOTE_HOST }} + username: ${{ secrets.DEV_REMOTE_USER }} + key: ${{ secrets.DEV_REMOTE_SSH_KEY }} + source: "ontime-back/docker-compose.yml,ontime-back/docker-compose.dev.yml" + target: ${{ env.DEV_DEPLOY_DIR }} + strip_components: 1 + + - name: Pull image and restart dev containers + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEV_REMOTE_HOST }} + username: ${{ secrets.DEV_REMOTE_USER }} + key: ${{ secrets.DEV_REMOTE_SSH_KEY }} + script: | + set -eu + + DEPLOY_DIR="${{ env.DEV_DEPLOY_DIR }}" + CONTAINER_NAME="ontime-dev-container" + + mkdir -p "$DEPLOY_DIR" + cd "$DEPLOY_DIR" + + umask 077 + cat > .env <<'EOF' + IMAGE_TAG=${{ env.IMAGE_TAG }} + BACKEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + BACKEND_CONTAINER_NAME=ontime-dev-container + BACKEND_HTTP_PORT=${{ secrets.DEV_BACKEND_HTTP_PORT || '8081' }} + BACKEND_MEMORY_LIMIT=${{ secrets.DEV_BACKEND_MEMORY_LIMIT || '768m' }} + BACKEND_CPU_LIMIT=${{ secrets.DEV_BACKEND_CPU_LIMIT || '1.0' }} + SERVER_PORT=8080 + SPRING_PROFILES_ACTIVE=dev + JAVA_TOOL_OPTIONS=-XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom + + MYSQL_DATABASE=${{ secrets.DEV_MYSQL_DATABASE || 'ontime_dev' }} + MYSQL_USER=${{ secrets.DEV_MYSQL_USER || 'ontime_dev' }} + MYSQL_PASSWORD=${{ secrets.DEV_MYSQL_PASSWORD || 'ontime_dev_password' }} + MYSQL_ROOT_PASSWORD=${{ secrets.DEV_MYSQL_ROOT_PASSWORD || 'ontime_dev_root_password' }} + + SPRING_APPLICATION_NAME=${{ secrets.DEV_SPRING_APPLICATION_NAME || 'ontime-back-dev' }} + SPRING_DATASOURCE_URL=${{ secrets.DEV_SPRING_DATASOURCE_URL || 'jdbc:mysql://mysql:3306/ontime_dev?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true' }} + SPRING_DATASOURCE_USERNAME=${{ secrets.DEV_SPRING_DATASOURCE_USERNAME || secrets.DEV_MYSQL_USER || 'ontime_dev' }} + SPRING_DATASOURCE_PASSWORD=${{ secrets.DEV_SPRING_DATASOURCE_PASSWORD || secrets.DEV_MYSQL_PASSWORD || 'ontime_dev_password' }} + SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver + SPRING_JPA_HIBERNATE_DDL_AUTO=validate + + SPRING_FLYWAY_ENABLED=true + SPRING_FLYWAY_BASELINE_ON_MIGRATE=false + + JWT_SECRET_KEY=${{ secrets.DEV_JWT_SECRETKEY || 'dev_secret_key_for_ontime_back_remote_pc_development_1234567890' }} + JWT_ACCESS_EXPIRATION=${{ secrets.DEV_JWT_ACCESS_EXPIRATION || '3600000' }} + JWT_REFRESH_EXPIRATION=${{ secrets.DEV_JWT_REFRESH_EXPIRATION || '1209600000' }} + JWT_ACCESS_HEADER=${{ secrets.DEV_JWT_ACCESS_HEADER || 'Authorization' }} + JWT_REFRESH_HEADER=${{ secrets.DEV_JWT_REFRESH_HEADER || 'Authorization-refresh' }} + + GOOGLE_WEB_CLIENT_ID=${{ secrets.DEV_GOOGLE_WEB_CLIENT_ID || 'dev-google-web-client-id' }} + GOOGLE_APP_CLIENT_ID=${{ secrets.DEV_GOOGLE_APP_CLIENT_ID || 'dev-google-app-client-id' }} + + APPLE_CLIENT_ID=${{ secrets.DEV_APPLE_CLIENT_ID || 'dev-apple-client-id' }} + APPLE_TEAM_ID=${{ secrets.DEV_APPLE_TEAM_ID || 'dev-apple-team-id' }} + APPLE_LOGIN_KEY=${{ secrets.DEV_APPLE_LOGIN_KEY || 'dev-apple-key-id' }} + APPLE_PRIVATE_KEY_BASE64=${{ secrets.DEV_APPLE_PRIVATE_KEY_BASE64 }} + FEATURE_APPLE_LOGIN_ENABLED=${{ secrets.DEV_FEATURE_APPLE_LOGIN_ENABLED || 'false' }} + + FIREBASE_CREDENTIALS_BASE64=${{ secrets.DEV_FIREBASE_CREDENTIALS_BASE64 }} + EOF + + echo "${{ secrets.GHCR_READ_TOKEN }}" | sudo docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin + + if sudo docker compose version >/dev/null 2>&1; then + COMPOSE="sudo docker compose" + else + COMPOSE="sudo docker-compose" + fi + + $COMPOSE -f docker-compose.yml -f docker-compose.dev.yml pull + $COMPOSE -f docker-compose.yml -f docker-compose.dev.yml up -d --remove-orphans + + HEALTHY=false + for attempt in $(seq 1 30); do + STATUS="$(sudo docker inspect -f '{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || true)" + if [ "$STATUS" = "healthy" ]; then + echo "Container is healthy." + HEALTHY=true + break + fi + echo "Waiting for healthy container status; current status: ${STATUS:-unknown}" + sleep 5 + done + + if [ "$HEALTHY" != "true" ]; then + sudo docker logs --tail=200 "$CONTAINER_NAME" || true + exit 1 + fi + + curl -fsS "http://127.0.0.1:${{ secrets.DEV_BACKEND_HTTP_PORT || '8081' }}/actuator/health/readiness" diff --git a/docs/deployment.md b/docs/deployment.md index 4d1f565..3b3b9b8 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,8 +1,10 @@ -# Production Deployment +# Deployment -This service deploys as an immutable Docker image published to GitHub Container Registry (GHCR). Runtime configuration is injected through the EC2 `.env` file generated by GitHub Actions; private resource files are not copied into the image or bind-mounted from the host. +This service deploys as an immutable Docker image published to GitHub Container Registry (GHCR). Runtime configuration is injected through `.env` files generated by GitHub Actions; private resource files are not copied into the image or bind-mounted from the host. -## Required GitHub Secrets +Production deploys to EC2 from `main`. Development deploys to a remote Ubuntu/Linux PC from `dev`. + +## Production GitHub Secrets Deployment access: @@ -85,9 +87,7 @@ base64 -i ontime-back/src/main/resources/key/AuthKey_743M7R5W3W.p8 | tr -d '\n' Push to the `main` branch, or run `.github/workflows/deploy.yml` manually, to deploy production. -Pushes to `dev` run CI only. There is no dev-server deploy workflow in the one-EC2 plan. - -The workflow: +The production workflow: 1. Builds `ontime-back/Dockerfile` from the `ontime-back/` context. 2. Pushes two GHCR tags: @@ -100,6 +100,65 @@ The workflow: 7. Waits until the `ontime-container` Docker health status is `healthy`. 8. Installs Caddy if needed, configures `/etc/caddy/Caddyfile`, and verifies HTTPS for `ontime-back.duckdns.org`. +## Development Remote PC Deployment + +Push to the `dev` branch, or run `.github/workflows/deploy-dev.yml` manually, to deploy the development backend to the remote PC. + +The development workflow: + +1. Builds `ontime-back/Dockerfile` from the `ontime-back/` context. +2. Pushes two GHCR tags: + - `ghcr.io/devkor-github/ontime-back:dev-` + - `ghcr.io/devkor-github/ontime-back:dev-latest` +3. Uploads `docker-compose.yml` and `docker-compose.dev.yml` to the remote PC. +4. Writes a development `.env` from GitHub secrets and safe dev defaults. +5. Runs `docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --remove-orphans`. +6. Starts MySQL as a private Docker Compose service with persistent volume `ontime-dev-mysql-data`. +7. Waits until the `ontime-dev-container` Docker health status is `healthy`. + +Required development secrets: + +- `DEV_REMOTE_HOST` +- `DEV_REMOTE_USER` +- `DEV_REMOTE_SSH_KEY` +- `GHCR_USERNAME` +- `GHCR_READ_TOKEN` + +Optional development secrets: + +- `DEV_DEPLOY_DIR` (defaults to `/home//OnTime-back-dev`) +- `DEV_BACKEND_HTTP_PORT` (defaults to `8081`) +- `DEV_BACKEND_MEMORY_LIMIT` (defaults to `768m`) +- `DEV_BACKEND_CPU_LIMIT` (defaults to `1.0`) +- `DEV_MYSQL_DATABASE` (defaults to `ontime_dev`) +- `DEV_MYSQL_USER` (defaults to `ontime_dev`) +- `DEV_MYSQL_PASSWORD` (defaults to `ontime_dev_password`) +- `DEV_MYSQL_ROOT_PASSWORD` (defaults to `ontime_dev_root_password`) +- `DEV_SPRING_APPLICATION_NAME` (defaults to `ontime-back-dev`) +- `DEV_SPRING_DATASOURCE_URL` (defaults to the Compose MySQL service) +- `DEV_SPRING_DATASOURCE_USERNAME` (defaults to the dev MySQL user) +- `DEV_SPRING_DATASOURCE_PASSWORD` (defaults to the dev MySQL password) +- `DEV_JWT_SECRETKEY` +- `DEV_JWT_ACCESS_EXPIRATION` +- `DEV_JWT_REFRESH_EXPIRATION` +- `DEV_JWT_ACCESS_HEADER` +- `DEV_JWT_REFRESH_HEADER` +- `DEV_GOOGLE_WEB_CLIENT_ID` +- `DEV_GOOGLE_APP_CLIENT_ID` +- `DEV_APPLE_CLIENT_ID` +- `DEV_APPLE_TEAM_ID` +- `DEV_APPLE_LOGIN_KEY` +- `DEV_APPLE_PRIVATE_KEY_BASE64` +- `DEV_FEATURE_APPLE_LOGIN_ENABLED` (defaults to `false`) +- `DEV_FIREBASE_CREDENTIALS_BASE64` + +Remote PC prerequisites: + +- Ubuntu/Linux host with normal SSH access from GitHub Actions. +- Docker and the Docker Compose plugin installed. +- Inbound firewall access for the backend HTTP port, default `8081`. +- No public inbound MySQL port is required; MySQL stays inside the Docker network. + ## HTTPS Prerequisites Before running the production deploy, configure AWS and DNS: @@ -129,6 +188,16 @@ curl -fsS https://ontime-back.duckdns.org/actuator/health/readiness nc -zv ontime-prod.cpoeguokwaq5.ap-northeast-2.rds.amazonaws.com 3306 ``` +Manual checks on the remote development PC: + +```bash +cd /home//OnTime-back-dev +sudo docker compose -f docker-compose.yml -f docker-compose.dev.yml ps +sudo docker inspect -f '{{.State.Health.Status}}' ontime-dev-container +sudo docker logs --tail=200 ontime-dev-container +curl -fsS http://:8081/actuator/health/readiness +``` + ## Rollback Every deploy is tagged by commit SHA. To roll back, set `IMAGE_TAG` in `/home/ubuntu/OnTime-back/.env` to the previous known-good SHA, then restart from the existing Compose file: diff --git a/docs/git-workflow.md b/docs/git-workflow.md index 1a0c6d4..fc7d6f6 100644 --- a/docs/git-workflow.md +++ b/docs/git-workflow.md @@ -1,6 +1,6 @@ -# Git Workflow And Production Deployment Strategy +# Git Workflow And Deployment Strategy -This document describes the recommended Git strategy for OnTime-back with one production EC2 server and one private production RDS instance. +This document describes the recommended Git strategy for OnTime-back with one production EC2 server, one private production RDS instance, and one remote-PC development server. ## Goals @@ -8,7 +8,7 @@ This document describes the recommended Git strategy for OnTime-back with one pr - Make every server deployment traceable to a Git branch and commit. - Avoid using deployment branches as places where product code diverges. - Keep feature branches short-lived and easy to review. -- Keep `dev` as an integration branch only; it must not deploy a long-running dev backend. +- Use `dev` as the integration branch and remote-PC development deploy source. ## Branch Model @@ -25,7 +25,7 @@ Branch responsibilities: | Branch | Purpose | Deployment | | --- | --- | --- | | `main` | Production-ready code and source of truth | Production server | -| `dev` | Integrated code for QA, frontend/mobile testing, and pre-release validation | No direct deployment | +| `dev` | Integrated code for QA, frontend/mobile testing, and pre-release validation | Remote PC development server | | `feature/*` | New feature work | No direct deployment | | `fix/*` | Bug fixes | No direct deployment | | `chore/*` | Maintenance, docs, config, CI changes | No direct deployment | @@ -59,7 +59,7 @@ feature/* -> dev 5. After review, merge into `dev`. -6. Validate the integrated code without running a long-lived dev backend on EC2. +6. Validate the integrated code on the remote PC development backend. 7. When the release candidate is ready, open a pull request from `dev` into `main`. @@ -84,6 +84,7 @@ Use branch-based CI and production deployment: ```text pull_request to dev/main -> test workflow +push to dev -> development environment -> remote PC development server push to main -> production environment -> production server ``` @@ -91,6 +92,7 @@ Recommended GitHub environments: | Environment | Source Branch | Server | Approval | | --- | --- | --- | --- | +| `development` | `dev` | Remote PC development server | Optional | | `production` | `main` | Production server | Manual approval recommended | ## CI/CD Workflow @@ -99,6 +101,7 @@ Recommended simple setup: ```text .github/workflows/test.yml +.github/workflows/deploy-dev.yml .github/workflows/deploy.yml ``` @@ -107,9 +110,9 @@ Expected triggers: ```text pull_request to dev -> run tests pull_request to main -> run tests -push to dev -> no deployment +push to dev -> deploy to remote PC development server push to main -> deploy to production server -workflow_dispatch -> allow manual redeploy or rollback support +workflow_dispatch -> allow manual redeploy for each deploy workflow ``` Production deploy should use production secrets only: @@ -123,7 +126,17 @@ SPRING_DATASOURCE_USERNAME SPRING_DATASOURCE_PASSWORD ``` -Do not add `DEV_*` deployment secrets or a dev-server workflow unless the infrastructure plan changes deliberately. +Development deploy should use development secrets only: + +```text +DEV_REMOTE_HOST +DEV_REMOTE_USER +DEV_REMOTE_SSH_KEY +GHCR_USERNAME +GHCR_READ_TOKEN +``` + +Optional `DEV_*` secrets can override the default dev deploy directory, HTTP port, MySQL credentials, and non-production OAuth/Firebase settings. ## Branch Protection @@ -171,7 +184,7 @@ The current repository has `main` and `deploy` as separate long-lived branches. ```text deploy branch -> retired main branch -> production -dev branch -> integration and CI only +dev branch -> integration and remote PC development deployment ``` Recommended migration sequence: @@ -189,19 +202,20 @@ git push origin dev ``` 5. Change production deployment to trigger from `main`. -6. Ensure there is no workflow that deploys from `dev`. +6. Ensure `dev` deploys only to the remote PC development environment. 7. Update GitHub production environment secrets. -8. Protect `main` and `dev`. -9. Stop using `deploy` for new work. -10. Delete or archive stale merged feature branches after confirming they are no longer needed. +8. Update GitHub development environment secrets for the remote PC. +9. Protect `main` and `dev`. +10. Stop using `deploy` for new work. +11. Delete or archive stale merged feature branches after confirming they are no longer needed. ## Practical Rules - New work branches from `dev`. - Normal PR target is `dev`. - Release PR target is `main`. +- `dev` deploys only to the remote PC development server. - Production deploys only from `main`. -- `dev` runs CI only and does not deploy. - Do not commit directly to `main`. - Do not commit directly to `dev` unless it is an emergency coordination fix. - Delete feature branches after merge. diff --git a/ontime-back/docker-compose.dev.yml b/ontime-back/docker-compose.dev.yml new file mode 100644 index 0000000..1d2bb07 --- /dev/null +++ b/ontime-back/docker-compose.dev.yml @@ -0,0 +1,37 @@ +services: + backend: + container_name: "${BACKEND_CONTAINER_NAME:-ontime-dev-container}" + depends_on: + mysql: + condition: service_healthy + + mysql: + image: mysql:8.0 + container_name: ontime-dev-mysql + environment: + MYSQL_DATABASE: "${MYSQL_DATABASE:-ontime_dev}" + MYSQL_USER: "${MYSQL_USER:-ontime_dev}" + MYSQL_PASSWORD: "${MYSQL_PASSWORD:-ontime_dev_password}" + MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:-ontime_dev_root_password}" + TZ: Asia/Seoul + volumes: + - ontime-dev-mysql-data:/var/lib/mysql + restart: unless-stopped + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --default-time-zone=+09:00 + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root --password=$$MYSQL_ROOT_PASSWORD"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +volumes: + ontime-dev-mysql-data: diff --git a/ontime-back/src/main/resources/application-dev.properties b/ontime-back/src/main/resources/application-dev.properties new file mode 100644 index 0000000..953a8bd --- /dev/null +++ b/ontime-back/src/main/resources/application-dev.properties @@ -0,0 +1,53 @@ +# Database Configuration +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:mysql://mysql:3306/ontime_dev?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:ontime_dev} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:ontime_dev_password} +spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver} + +# JPA / Hibernate +spring.jpa.database=mysql +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=${SPRING_JPA_HIBERNATE_DDL_AUTO:validate} +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# Flyway +spring.flyway.enabled=${SPRING_FLYWAY_ENABLED:true} +spring.flyway.baseline-on-migrate=${SPRING_FLYWAY_BASELINE_ON_MIGRATE:false} + +# JWT Configuration +jwt.secret.key=${JWT_SECRET_KEY:dev_secret_key_for_ontime_back_remote_pc_development_1234567890} +jwt.access.expiration=${JWT_ACCESS_EXPIRATION:3600000} +jwt.refresh.expiration=${JWT_REFRESH_EXPIRATION:1209600000} +jwt.access.header=${JWT_ACCESS_HEADER:Authorization} +jwt.refresh.header=${JWT_REFRESH_HEADER:Authorization-refresh} + +# Google OAuth +google.web.client-id=${GOOGLE_WEB_CLIENT_ID:dev-google-web-client-id} +google.app.client-id=${GOOGLE_APP_CLIENT_ID:dev-google-app-client-id} + +# Apple OAuth +apple.client.id=${APPLE_CLIENT_ID:dev-apple-client-id} +apple.team.id=${APPLE_TEAM_ID:dev-apple-team-id} +apple.login.key=${APPLE_LOGIN_KEY:dev-apple-key-id} +apple.client.secret=${APPLE_CLIENT_SECRET:} +apple.private-key.base64=${APPLE_PRIVATE_KEY_BASE64:} + +# Firebase +firebase.credentials.base64=${FIREBASE_CREDENTIALS_BASE64:} + +# Logging +logging.level.root=INFO +logging.level.devkor.ontime_back=DEBUG + +# Feature flags +feature.apple-login.enabled=${FEATURE_APPLE_LOGIN_ENABLED:false} + +# Actuator +management.endpoint.health.probes.enabled=true +management.endpoints.web.exposure.include=health +management.endpoint.health.show-details=always +management.health.readinessstate.enabled=true +management.health.livenessstate.enabled=true +server.shutdown=graceful +spring.lifecycle.timeout-per-shutdown-phase=30s