Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 135 additions & 131 deletions .github/workflows/deploy.yml

Large diffs are not rendered by default.

121 changes: 121 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Production 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.

## Required GitHub Secrets

Deployment access:

- `EC2_HOST`
- `EC2_USER`
- `EC2_SSH_KEY`
- `GHCR_USERNAME`
- `GHCR_READ_TOKEN`

Runtime image and port:

- `BACKEND_HTTP_PORT` (optional, defaults to `8080`)

Spring and database:

- `SPRING_APPLICATION_NAME`
- `SPRING_DATASOURCE_URL`
- `SPRING_DATASOURCE_USERNAME`
- `SPRING_DATASOURCE_PASSWORD`
- `SPRING_DATASOURCE_DRIVER_CLASS_NAME`
- `SPRING_JPA_HIBERNATE_DDL_AUTO`
- `SPRING_FLYWAY_URL`
- `SPRING_FLYWAY_USER`
- `SPRING_FLYWAY_PASSWORD`

Authentication and OAuth:

- `JWT_SECRETKEY`
- `JWT_ACCESS_EXPIRATION`
- `JWT_REFRESH_EXPIRATION`
- `JWT_ACCESS_HEADER`
- `JWT_REFRESH_HEADER`
- `GOOGLE_WEB_CLIENT_ID`
- `GOOGLE_APP_CLIENT_ID`
- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET`
- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE`
- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_REDIRECT_URI`
- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_AUTHORIZATION_GRANT_TYPE`
- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_NAME`
- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_AUTHORIZATION_URI`
- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_TOKEN_URI`
- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_INFO_URI`
- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_NAME_ATTRIBUTE`
- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID`
- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_SCOPE`
- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_REDIRECT_URI`
- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_AUTHORIZATION_GRANT_TYPE`
- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_NAME`
- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_AUTHORIZATION_URI`
- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_TOKEN_URI`
- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_INFO_URI`
- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_NAME_ATTRIBUTE`
- `APPLE_CLIENT_ID`
- `APPLE_TEAM_ID`
- `APPLE_LOGIN_KEY`
- `APPLE_PRIVATE_KEY_BASE64`
- `FEATURE_APPLE_LOGIN_ENABLED` (optional, defaults to `true`)

Firebase:

- `FIREBASE_CREDENTIALS_BASE64`

Set the base64 secrets from the ignored local credential files:

```bash
base64 -i ontime-back/src/main/resources/ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json | tr -d '\n' | gh secret set FIREBASE_CREDENTIALS_BASE64 --repo DevKor-github/OnTime-back
```

```bash
base64 -i ontime-back/src/main/resources/key/AuthKey_743M7R5W3W.p8 | tr -d '\n' | gh secret set APPLE_PRIVATE_KEY_BASE64 --repo DevKor-github/OnTime-back
```

## Build And Release Flow

Push to the `deploy` branch to trigger `.github/workflows/deploy.yml`.

The workflow:

1. Builds `ontime-back/Dockerfile` from the `ontime-back/` context.
2. Pushes two GHCR tags:
- `ghcr.io/devkor-github/ontime-back:<commit-sha>`
- `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`.

## Health Verification

The production image exposes a Docker healthcheck against:

```text
/actuator/health/readiness
```

Manual checks on EC2:

```bash
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
```

## 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:

```bash
cd /home/ubuntu/OnTime-back
sudo docker compose pull
sudo docker compose up -d --remove-orphans
sudo docker inspect -f '{{.State.Health.Status}}' ontime-container
```

Keep the previous SHA in the release notes or GitHub Actions deploy history so rollback does not depend on `deploy-latest`.
21 changes: 21 additions & 0 deletions ontime-back/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.env
.git
.gitignore
.gradle
build
out
bin
.idea
.vscode
*.iml
*.iws
*.ipr
.DS_Store

src/main/resources/application.properties
src/main/resources/*.json
src/main/resources/key
src/main/resources/**/*.p8

Dockerfile
docker-compose*.yml
46 changes: 38 additions & 8 deletions ontime-back/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,40 @@
FROM eclipse-temurin:17-jre
RUN apt-get update && \
apt-get install -y tzdata && \
ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \
echo "Asia/Seoul" > /etc/timezone && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
FROM eclipse-temurin:17-jdk-jammy AS builder

WORKDIR /workspace

COPY gradlew build.gradle settings.gradle ./
COPY gradle ./gradle
RUN chmod +x ./gradlew

COPY src ./src
RUN ./gradlew clean bootJar --no-daemon

FROM eclipse-temurin:17-jre-jammy

ENV TZ=Asia/Seoul \
LANG=C.UTF-8 \
SPRING_PROFILES_ACTIVE=prod \
JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom"

RUN apt-get update \
&& apt-get install -y --no-install-recommends curl tzdata \
&& ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \
&& echo "${TZ}" > /etc/timezone \
&& groupadd --system app \
&& useradd --system --gid app --home-dir /app --shell /usr/sbin/nologin app \
&& mkdir -p /app \
&& chown -R app:app /app \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY project.jar app.jar
COPY --from=builder --chown=app:app /workspace/build/libs/*.jar /app/app.jar

USER app
EXPOSE 8080
STOPSIGNAL SIGTERM

HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD curl -fsS "http://localhost:${SERVER_PORT:-8080}/actuator/health/readiness" || exit 1

ENTRYPOINT ["java", "-jar", "/app/app.jar"]
30 changes: 19 additions & 11 deletions ontime-back/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
services:
backend:
build:
context: .
dockerfile: Dockerfile
image: ontime-backend
container_name: ontime-backend
restart: unless-stopped
image: "${BACKEND_IMAGE:-ghcr.io/devkor-github/ontime-back}:${IMAGE_TAG:?IMAGE_TAG is required}"
container_name: "${BACKEND_CONTAINER_NAME:-ontime-container}"
env_file:
- .env
environment:
SPRING_PROFILES_ACTIVE: "${SPRING_PROFILES_ACTIVE:-prod}"
SERVER_PORT: "${SERVER_PORT:-8080}"
JAVA_TOOL_OPTIONS: "${JAVA_TOOL_OPTIONS:--XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom}"
ports:
- "8080:8080"
volumes:
- ./config/application.properties:/app/config/application.properties:ro
- ./secrets/firebase-adminsdk.json:/app/secrets/firebase-adminsdk.json:ro
- ./secrets/AuthKey_743M7R5W3W.p8:/app/secrets/AuthKey_743M7R5W3W.p8:ro
- "${BACKEND_HTTP_PORT:-8080}:${SERVER_PORT:-8080}"
restart: unless-stopped
stop_grace_period: 30s
mem_limit: "${BACKEND_MEMORY_LIMIT:-768m}"
cpus: "${BACKEND_CPU_LIMIT:-1.0}"
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:${SERVER_PORT:-8080}/actuator/health/readiness || exit 1"]
interval: 30s
timeout: 5s
start_period: 60s
retries: 3
40 changes: 40 additions & 0 deletions ontime-back/docs/logging-redaction-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Logging Redaction Policy

Production logs must only contain operational metadata needed to debug routing,
ownership, status, and latency. Request payloads are not safe log data.

## Request Logs

Controller request logging is centralized in `LoggingAspect` and
`RequestLogPolicy`. Request logs must include only:

- request ID
- route
- method
- actor identifier
- client IP
- response status
- timing in milliseconds

The request logger must not inspect or render `@RequestBody` arguments.

## Sensitive Fields

Never log values or raw key/value payloads for credentials, OAuth material,
profile text, notes, or request bodies. Sensitive key names include:

- `authorization`
- `firebaseToken`
- `password`
- `secret`
- `token`

When field-level logging is genuinely needed, add the field to
`RequestLogPolicy`'s allowlist and keep the log statement metadata-only.

## Regression Guard

`SensitiveLoggingPolicyTest` scans application source for sensitive key names in
logger calls, request-body logging in `LoggingAspect`, and DTO-generated
`toString` methods. Any future exception must update this policy and the
central allowlist in the same change.
Loading