diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8f0940d..49b0b9e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -87,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 @@ -154,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 @@ -175,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/docs/deployment.md b/docs/deployment.md index ccd1bd5..4ee20dd 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: @@ -97,8 +103,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 @@ -115,6 +122,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/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..df6ed5b 100644 --- a/ontime-back/docs/deployment/ec2.md +++ b/ontime-back/docs/deployment/ec2.md @@ -8,7 +8,7 @@ This service deploys to Amazon EC2 through `.github/workflows/deploy.yml`. 2. Add the required GitHub Actions secrets listed below. 3. Run the `Deploy` workflow manually from GitHub Actions, or push to the `deploy` 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.