diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7098287..49b0b9e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,9 +1,10 @@ name: Deploy on: + workflow_dispatch: push: branches: - - deploy + - main permissions: contents: read @@ -50,6 +51,7 @@ jobs: deploy-to-ec2: needs: build-and-push runs-on: ubuntu-latest + environment: production steps: - name: Checkout repository uses: actions/checkout@v4 @@ -85,6 +87,8 @@ jobs: BACKEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} BACKEND_CONTAINER_NAME=ontime-container BACKEND_HTTP_PORT=${{ secrets.BACKEND_HTTP_PORT || '8080' }} + BACKEND_MEMORY_LIMIT=${{ secrets.BACKEND_MEMORY_LIMIT || '768m' }} + BACKEND_CPU_LIMIT=${{ secrets.BACKEND_CPU_LIMIT || '1.0' }} SERVER_PORT=8080 SPRING_PROFILES_ACTIVE=prod JAVA_TOOL_OPTIONS=-XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom @@ -152,14 +156,33 @@ jobs: FLYWAY_BASELINE="$(get_env_value SPRING_FLYWAY_BASELINE_ON_MIGRATE)" NORMALIZED_DB_URL="$(printf '%s' "$DB_URL" | tr '[:upper:]' '[:lower:]')" NORMALIZED_DB_USERNAME="$(printf '%s' "$DB_USERNAME" | tr '[:upper:]' '[:lower:]')" + DB_URL_NO_PREFIX="${DB_URL#jdbc:mysql://}" + [ "$DB_URL_NO_PREFIX" != "$DB_URL" ] || fail_deploy "SPRING_DATASOURCE_URL must start with jdbc:mysql://." + DB_ADDRESS="${DB_URL_NO_PREFIX%%/*}" + DB_HOST="${DB_ADDRESS%%:*}" + DB_PORT="${DB_ADDRESS#*:}" + [ "$DB_PORT" != "$DB_ADDRESS" ] || DB_PORT="3306" + DB_NAME_AND_QUERY="${DB_URL_NO_PREFIX#*/}" + DB_NAME="$(printf '%s' "$DB_NAME_AND_QUERY" | sed 's/[?;].*$//')" [ -n "$DB_URL" ] || fail_deploy "SPRING_DATASOURCE_URL is required." [ -n "$DB_USERNAME" ] || fail_deploy "SPRING_DATASOURCE_USERNAME is required." [ -n "$DB_PASSWORD" ] || fail_deploy "SPRING_DATASOURCE_PASSWORD is required." + [ -n "$DB_HOST" ] || fail_deploy "SPRING_DATASOURCE_URL must include an RDS host." + [ "$DB_NAME" = "ontime_prod" ] || fail_deploy "SPRING_DATASOURCE_URL must use the ontime_prod database." [ "$NORMALIZED_DB_USERNAME" != "root" ] || fail_deploy "SPRING_DATASOURCE_USERNAME must not be root." [ "$DDL_AUTO" = "validate" ] || fail_deploy "SPRING_JPA_HIBERNATE_DDL_AUTO must be validate." [ "$FLYWAY_BASELINE" = "false" ] || fail_deploy "SPRING_FLYWAY_BASELINE_ON_MIGRATE must be false." + case "$NORMALIZED_DB_URL" in + *localhost*|*127.0.0.1*|*host.docker.internal*) fail_deploy "SPRING_DATASOURCE_URL must point to private RDS, not a local database." ;; + esac + + case "$(printf '%s' "$DB_HOST" | tr '[:upper:]' '[:lower:]')" in + *.rds.amazonaws.com) ;; + *) fail_deploy "SPRING_DATASOURCE_URL host must be an RDS endpoint." ;; + esac + case "$NORMALIZED_DB_URL" in *allowpublickeyretrieval=true*) fail_deploy "SPRING_DATASOURCE_URL must not enable allowPublicKeyRetrieval." ;; esac @@ -173,10 +196,17 @@ jobs: esac case "$NORMALIZED_DB_URL" in - *sslmode=required*|*sslmode=verify_ca*|*sslmode=verify_identity*) ;; - *) fail_deploy "SPRING_DATASOURCE_URL must declare sslMode=REQUIRED, VERIFY_CA, or VERIFY_IDENTITY." ;; + *sslmode=required*|*sslmode=verify_ca*|*sslmode=verify_identity*|*usessl=true*) ;; + *) fail_deploy "SPRING_DATASOURCE_URL must declare useSSL=true, sslMode=REQUIRED, VERIFY_CA, or VERIFY_IDENTITY." ;; esac + echo "Checking EC2-to-RDS TCP connectivity for $DB_HOST:$DB_PORT..." + if command -v nc >/dev/null 2>&1; then + nc -zv "$DB_HOST" "$DB_PORT" + else + timeout 5 bash -c "/dev/null 2>&1; then diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b2489e3..99ee7e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,16 +1,15 @@ -# PR이 올라왔을 때 자동으로 테스트코드를 실행하고 하나라도 Fail했을 시, PR을 Close하고 실패 코멘트를 달고 성공시에는 머지를 가능케하는 파이프라인. +# PR이 올라왔을 때 자동으로 테스트코드를 실행하고 실패 시 머지를 막는 파이프라인. name: PR Test on: pull_request: branches: - - main # main 브랜치로 머지되는 PR에서 실행 + - dev + - main jobs: test: runs-on: ubuntu-latest - outputs: - test_result: ${{ steps.run-tests.outcome }} # 테스트 결과를 output으로 저장 services: mysql: @@ -64,8 +63,6 @@ jobs: # 4. Gradle 빌드 & JUnit 테스트 실행 - name: Run Tests with Gradle - id: run-tests # 실행 결과를 output으로 저장할 id 추가 - continue-on-error: true env: SPRING_PROFILES_ACTIVE: test run: | @@ -76,43 +73,3 @@ jobs: run: | echo "Checking Flyway migration history..." mysql -h 127.0.0.1 -u test_user --password=test_password -e "SELECT * FROM test_db.flyway_schema_history;" - - - handle-failure: - needs: test - if: needs.test.outputs.test_result == 'failure' # test job의 output 값이 실패(failure)일 때 실행 - runs-on: ubuntu-latest - permissions: - pull-requests: write - issues: write - steps: - - name: Close PR - uses: octokit/request-action@v2.x - with: - route: PATCH /repos/{owner}/{repo}/pulls/{pull_number} - owner: ${{ github.repository_owner }} - repo: ${{ github.event.repository.name }} - pull_number: ${{ github.event.pull_request.number }} - state: closed - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Wait for PR to close - run: sleep 5 - - - name: Comment on PR - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const prNumber = context.payload.pull_request?.number || context.payload.issue?.number; - if (!prNumber) { - console.log("PR 번호를 찾을 수 없습니다."); - return; - } - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: "실패하는 테스트코드가 있어 PR이 자동으로 닫혔습니다.\nGithub Action에서 자세한 실패 로그를 확인하고 코드를 수정하세요." - }); diff --git a/docs/deployment.md b/docs/deployment.md index c8eb53d..a34dcf2 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -15,18 +15,24 @@ Deployment access: Runtime image and port: - `BACKEND_HTTP_PORT` (optional, defaults to `8080`) +- `BACKEND_MEMORY_LIMIT` (optional, defaults to `768m`; use `640m` if the EC2 instance is memory constrained) +- `BACKEND_CPU_LIMIT` (optional, defaults to `1.0`) Spring and database: - `SPRING_APPLICATION_NAME` -- `SPRING_DATASOURCE_URL` -- `SPRING_DATASOURCE_USERNAME` +- `SPRING_DATASOURCE_URL` (`jdbc:mysql://ontime-prod.cpoeguokwaq5.ap-northeast-2.rds.amazonaws.com:3306/ontime_prod?useSSL=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8`) +- `SPRING_DATASOURCE_USERNAME` (`ontimeadmin`) - `SPRING_DATASOURCE_PASSWORD` -- `SPRING_DATASOURCE_DRIVER_CLASS_NAME` -- `SPRING_JPA_HIBERNATE_DDL_AUTO` -- `SPRING_FLYWAY_URL` -- `SPRING_FLYWAY_USER` -- `SPRING_FLYWAY_PASSWORD` + +The deploy workflow hardcodes safe production defaults for the datasource driver, JPA DDL mode, and Flyway: + +- `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` + +It also fails before restart if the datasource URL does not point to an RDS MySQL endpoint using the `ontime_prod` database, or if EC2 cannot reach RDS on `3306`. Authentication and OAuth: @@ -77,7 +83,9 @@ base64 -i ontime-back/src/main/resources/key/AuthKey_743M7R5W3W.p8 | tr -d '\n' ## Build And Release Flow -Push to the `deploy` branch to trigger `.github/workflows/deploy.yml`. +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: @@ -87,8 +95,9 @@ The workflow: - `ghcr.io/devkor-github/ontime-back:deploy-latest` 3. Uploads `docker-compose.yml` to `/home/ubuntu/OnTime-back`. 4. Writes `/home/ubuntu/OnTime-back/.env` from GitHub secrets. -5. Runs `docker compose pull && docker compose up -d --remove-orphans`. -6. Waits until the `ontime-container` Docker health status is `healthy`. +5. Verifies EC2 can reach private RDS on `3306`. +6. Runs `docker compose pull && docker compose up -d --remove-orphans`. +7. Waits until the `ontime-container` Docker health status is `healthy`. ## Health Verification @@ -105,6 +114,7 @@ cd /home/ubuntu/OnTime-back sudo docker compose ps sudo docker inspect -f '{{.State.Health.Status}}' ontime-container curl -fsS http://localhost:8080/actuator/health/readiness +nc -zv ontime-prod.cpoeguokwaq5.ap-northeast-2.rds.amazonaws.com 3306 ``` ## Rollback diff --git a/docs/git-workflow.md b/docs/git-workflow.md new file mode 100644 index 0000000..1a0c6d4 --- /dev/null +++ b/docs/git-workflow.md @@ -0,0 +1,209 @@ +# Git Workflow And Production Deployment Strategy + +This document describes the recommended Git strategy for OnTime-back with one production EC2 server and one private production RDS instance. + +## Goals + +- Keep one clear production source of truth. +- 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. + +## Branch Model + +Use a lightweight environment-branch workflow: + +```text +feature/*, fix/*, chore/* -> short-lived work branches +dev -> integration branch for CI and review +main -> production server source +``` + +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 | +| `feature/*` | New feature work | No direct deployment | +| `fix/*` | Bug fixes | No direct deployment | +| `chore/*` | Maintenance, docs, config, CI changes | No direct deployment | + +Avoid keeping a long-lived `deploy` branch as the production source. Production should be represented by `main`, while deployment behavior should be handled by GitHub Actions environments and secrets. + +## Normal Development Workflow + +1. Start from the latest `dev`. + +```bash +git checkout dev +git pull origin dev +git checkout -b feat/some-feature +``` + +2. Commit focused changes on the feature branch. + +```bash +git add . +git commit -m "feat: add some feature" +``` + +3. Open a pull request into `dev`. + +```text +feature/* -> dev +``` + +4. CI runs tests and build checks. + +5. After review, merge into `dev`. + +6. Validate the integrated code without running a long-lived dev backend on EC2. + +7. When the release candidate is ready, open a pull request from `dev` into `main`. + +```text +dev -> main +``` + +8. After review and CI pass, merge into `main`. + +9. GitHub Actions deploys `main` to the production server. + +10. Tag the production release. + +```bash +git tag prod-YYYY-MM-DD +git push origin prod-YYYY-MM-DD +``` + +## Server Mapping + +Use branch-based CI and production deployment: + +```text +pull_request to dev/main -> test workflow +push to main -> production environment -> production server +``` + +Recommended GitHub environments: + +| Environment | Source Branch | Server | Approval | +| --- | --- | --- | --- | +| `production` | `main` | Production server | Manual approval recommended | + +## CI/CD Workflow + +Recommended simple setup: + +```text +.github/workflows/test.yml +.github/workflows/deploy.yml +``` + +Expected triggers: + +```text +pull_request to dev -> run tests +pull_request to main -> run tests +push to dev -> no deployment +push to main -> deploy to production server +workflow_dispatch -> allow manual redeploy or rollback support +``` + +Production deploy should use production secrets only: + +```text +EC2_HOST +EC2_USER +EC2_SSH_KEY +SPRING_DATASOURCE_URL +SPRING_DATASOURCE_USERNAME +SPRING_DATASOURCE_PASSWORD +``` + +Do not add `DEV_*` deployment secrets or a dev-server workflow unless the infrastructure plan changes deliberately. + +## Branch Protection + +Protect `dev`: + +- Require pull requests before merging. +- Require CI tests to pass. +- Allow squash merge. +- Allow faster review for low-risk changes. + +Protect `main` more strictly: + +- Require pull requests before merging. +- Require CI tests to pass. +- Require review approval. +- Require the PR branch to be up to date before merge. +- Block force pushes. +- Block direct pushes. +- Require production environment approval before deployment. + +## Release And Rollback + +Every production deployment should be traceable by commit SHA and tag. + +Recommended release tags: + +```text +prod-2026-05-07 +prod-2026-05-07-1 +v1.3.0 +``` + +Rollback options: + +1. Redeploy a previous image tag by commit SHA. +2. Revert the bad commit on `main` and deploy the revert. +3. If the issue only exists in configuration, update production secrets or environment config and rerun deployment. + +Prefer image-SHA rollback for urgent incidents, then follow up with a Git revert or fix PR so `main` still reflects production truth. + +## Migration From Current State + +The current repository has `main` and `deploy` as separate long-lived branches. The target state should be: + +```text +deploy branch -> retired +main branch -> production +dev branch -> integration and CI only +``` + +Recommended migration sequence: + +1. Compare `deploy` and `main`. +2. Merge or cherry-pick all intended production changes from `deploy` back into `main`. +3. Confirm `main` builds and tests successfully. +4. Create `dev` from the updated `main`. + +```bash +git checkout main +git pull origin main +git checkout -b dev +git push origin dev +``` + +5. Change production deployment to trigger from `main`. +6. Ensure there is no workflow that deploys from `dev`. +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. + +## Practical Rules + +- New work branches from `dev`. +- Normal PR target is `dev`. +- Release PR target is `main`. +- 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. +- Keep database migrations forward-only. +- Keep environment-specific values in GitHub secrets, not committed files. diff --git a/ontime-back/docker-compose.yml b/ontime-back/docker-compose.yml index e21529d..f336b47 100644 --- a/ontime-back/docker-compose.yml +++ b/ontime-back/docker-compose.yml @@ -14,6 +14,11 @@ services: stop_grace_period: 30s mem_limit: "${BACKEND_MEMORY_LIMIT:-768m}" cpus: "${BACKEND_CPU_LIMIT:-1.0}" + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" healthcheck: test: ["CMD-SHELL", "curl -fsS http://localhost:${SERVER_PORT:-8080}/actuator/health/readiness || exit 1"] interval: 30s diff --git a/ontime-back/docs/deployment/ec2.md b/ontime-back/docs/deployment/ec2.md index c1a0759..1ae690e 100644 --- a/ontime-back/docs/deployment/ec2.md +++ b/ontime-back/docs/deployment/ec2.md @@ -6,9 +6,9 @@ This service deploys to Amazon EC2 through `.github/workflows/deploy.yml`. 1. Make sure the EC2 instance has Docker installed and the security group allows inbound traffic for the service port, currently `8080`. 2. Add the required GitHub Actions secrets listed below. -3. Run the `Deploy` workflow manually from GitHub Actions, or push to the `deploy` branch. +3. Run the `Deploy` workflow manually from GitHub Actions, or push to the `main` branch. -The workflow builds the Spring Boot jar, creates deploy-only config files from GitHub Secrets, uploads them to `/home/ubuntu/OnTime-back`, and restarts Docker Compose on the EC2 instance. +The workflow builds a Docker image, pushes it to GHCR, uploads `docker-compose.yml` to `/home/ubuntu/OnTime-back`, writes a production `.env` from GitHub Secrets, verifies private RDS connectivity, and restarts Docker Compose on the EC2 instance. ## Required EC2 Secrets @@ -22,8 +22,6 @@ The workflow builds the Spring Boot jar, creates deploy-only config files from G - `SPRING_DATASOURCE_URL` - `SPRING_DATASOURCE_USERNAME` - `SPRING_DATASOURCE_PASSWORD` -- `SPRING_DATASOURCE_DRIVER_CLASS_NAME` -- `SPRING_JPA_HIBERNATE_DDL_AUTO` - `JWT_SECRETKEY` - `JWT_ACCESS_EXPIRATION` - `JWT_REFRESH_EXPIRATION` @@ -34,15 +32,14 @@ The workflow builds the Spring Boot jar, creates deploy-only config files from G - `APPLE_CLIENT_ID` - `APPLE_LOGIN_KEY` - `APPLE_TEAM_ID` -- `AUTHKEY_743M7R5W3W` -- `SPRING_FLYWAY_URL` -- `SPRING_FLYWAY_USER` -- `SPRING_FLYWAY_PASSWORD` -- `ONTIME_PUSH_FIREBASE_ADMINSDK` +- `APPLE_PRIVATE_KEY_BASE64` +- `FIREBASE_CREDENTIALS_BASE64` ## Optional Secrets -- `SPRING_JPA_DATABASE_PLATFORM` defaults to `org.hibernate.dialect.MySQL8Dialect`. +- `BACKEND_HTTP_PORT` defaults to `8080`. +- `BACKEND_MEMORY_LIMIT` defaults to `768m`; use `640m` if the EC2 instance is memory constrained. +- `BACKEND_CPU_LIMIT` defaults to `1.0`. - `FEATURE_APPLE_LOGIN_ENABLED` defaults to `true`. - Google and Kakao OAuth provider/registration secrets are included by the workflow when configured. @@ -50,11 +47,13 @@ The workflow builds the Spring Boot jar, creates deploy-only config files from G The deploy workflow writes these files under `/home/ubuntu/OnTime-back`: -- `project.jar` -- `Dockerfile` - `docker-compose.yml` -- `config/application.properties` -- `secrets/firebase-adminsdk.json` -- `secrets/AuthKey_743M7R5W3W.p8` +- `.env` + +Production uses the private RDS instance: + +```text +ontime-prod.cpoeguokwaq5.ap-northeast-2.rds.amazonaws.com:3306/ontime_prod +``` Do not commit local `application.properties`, Firebase service account JSON, Apple `.p8` keys, or `.env` files.