Merge pull request #300 from DevKor-github/codexd/request-validation-… #196
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: | |
| - main | |
| permissions: | |
| contents: read | |
| packages: write | |
| concurrency: | |
| group: deploy-production | |
| cancel-in-progress: false | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: devkor-github/ontime-back | |
| IMAGE_TAG: ${{ 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 }}:deploy-latest | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| deploy-to-ec2: | |
| needs: build-and-push | |
| runs-on: ubuntu-latest | |
| environment: production | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Upload compose file to EC2 | |
| uses: appleboy/scp-action@v0.1.7 | |
| with: | |
| host: ${{ secrets.EC2_HOST }} | |
| username: ${{ secrets.EC2_USER }} | |
| key: ${{ secrets.EC2_SSH_KEY }} | |
| source: "ontime-back/docker-compose.yml" | |
| target: "/home/ubuntu/OnTime-back" | |
| strip_components: 1 | |
| - name: Pull image and restart container | |
| uses: appleboy/ssh-action@v1.0.3 | |
| with: | |
| host: ${{ secrets.EC2_HOST }} | |
| username: ${{ secrets.EC2_USER }} | |
| key: ${{ secrets.EC2_SSH_KEY }} | |
| script: | | |
| set -eu | |
| DEPLOY_DIR="/home/ubuntu/OnTime-back" | |
| CONTAINER_NAME="ontime-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-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 | |
| SPRING_APPLICATION_NAME=${{ secrets.SPRING_APPLICATION_NAME }} | |
| SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }} | |
| SPRING_DATASOURCE_USERNAME=${{ secrets.SPRING_DATASOURCE_USERNAME }} | |
| SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_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.JWT_SECRETKEY }} | |
| JWT_ACCESS_EXPIRATION=${{ secrets.JWT_ACCESS_EXPIRATION }} | |
| JWT_REFRESH_EXPIRATION=${{ secrets.JWT_REFRESH_EXPIRATION }} | |
| JWT_ACCESS_HEADER=${{ secrets.JWT_ACCESS_HEADER }} | |
| JWT_REFRESH_HEADER=${{ secrets.JWT_REFRESH_HEADER }} | |
| GOOGLE_WEB_CLIENT_ID=${{ secrets.GOOGLE_WEB_CLIENT_ID }} | |
| GOOGLE_APP_CLIENT_ID=${{ secrets.GOOGLE_APP_CLIENT_ID }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_REDIRECT_URI=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_REDIRECT_URI }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_AUTHORIZATION_GRANT_TYPE=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_AUTHORIZATION_GRANT_TYPE }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_NAME=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_NAME }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_AUTHORIZATION_URI=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_AUTHORIZATION_URI }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_TOKEN_URI=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_TOKEN_URI }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_INFO_URI=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_INFO_URI }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_NAME_ATTRIBUTE=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_NAME_ATTRIBUTE }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_SCOPE=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_SCOPE }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_REDIRECT_URI=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_REDIRECT_URI }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_AUTHORIZATION_GRANT_TYPE=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_AUTHORIZATION_GRANT_TYPE }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_NAME=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_NAME }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_AUTHORIZATION_URI=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_AUTHORIZATION_URI }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_TOKEN_URI=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_TOKEN_URI }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_INFO_URI=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_INFO_URI }} | |
| SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_NAME_ATTRIBUTE=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_NAME_ATTRIBUTE }} | |
| APPLE_CLIENT_ID=${{ secrets.APPLE_CLIENT_ID }} | |
| APPLE_TEAM_ID=${{ secrets.APPLE_TEAM_ID }} | |
| APPLE_LOGIN_KEY=${{ secrets.APPLE_LOGIN_KEY }} | |
| APPLE_PRIVATE_KEY_BASE64=${{ secrets.APPLE_PRIVATE_KEY_BASE64 }} | |
| FEATURE_APPLE_LOGIN_ENABLED=${{ secrets.FEATURE_APPLE_LOGIN_ENABLED || 'true' }} | |
| FIREBASE_CREDENTIALS_BASE64=${{ secrets.FIREBASE_CREDENTIALS_BASE64 }} | |
| EOF | |
| fail_deploy() { | |
| echo "Unsafe production database configuration: $1" >&2 | |
| exit 1 | |
| } | |
| get_env_value() { | |
| grep -E "^$1=" .env | tail -n 1 | cut -d= -f2- | |
| } | |
| DB_URL="$(get_env_value SPRING_DATASOURCE_URL)" | |
| DB_USERNAME="$(get_env_value SPRING_DATASOURCE_USERNAME)" | |
| DB_PASSWORD="$(get_env_value SPRING_DATASOURCE_PASSWORD)" | |
| DDL_AUTO="$(get_env_value SPRING_JPA_HIBERNATE_DDL_AUTO)" | |
| 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 | |
| case "$NORMALIZED_DB_URL" in | |
| *createdatabaseifnotexist=true*) fail_deploy "SPRING_DATASOURCE_URL must not create databases at startup." ;; | |
| esac | |
| case "$NORMALIZED_DB_URL" in | |
| *usessl=false*) fail_deploy "SPRING_DATASOURCE_URL must not disable TLS." ;; | |
| esac | |
| case "$NORMALIZED_DB_URL" in | |
| *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/tcp/$DB_HOST/$DB_PORT" | |
| fi | |
| 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 | |
| sudo docker compose pull | |
| sudo docker compose up -d --remove-orphans | |
| else | |
| sudo docker-compose pull | |
| sudo docker-compose up -d --remove-orphans | |
| fi | |
| 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." | |
| exit 0 | |
| fi | |
| echo "Waiting for healthy container status; current status: ${STATUS:-unknown}" | |
| sleep 5 | |
| done | |
| sudo docker logs --tail=200 "$CONTAINER_NAME" || true | |
| exit 1 |