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
43 changes: 41 additions & 2 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -452,12 +452,51 @@ jobs:

# 업로드물은 컨테이너 밖에 둔다. 볼륨이 없으면 교체할 때마다 /app/uploads 가 컨테이너와
# 함께 사라진다 — 사용자가 올린 진단서·프로필 이미지가 배포마다 전부 날아간다는 뜻이다.
# (여러 인스턴스로 늘릴 때는 S3 구현체로 바꿔야 한다. 이슈 #49)
# (STORAGE_TYPE=s3 로 MinIO·S3 를 쓰면 이 볼륨은 쓰이지 않지만, 되돌릴 때를 위해 둔다.)
UPLOADS_DIR=/opt/carecode/uploads
mkdir -p "$UPLOADS_DIR"

# 오브젝트 스토리지(MinIO)를 쓰는 배포라면 그 컨테이너는 앱과 생애주기가 다르다.
# 앱만 교체하고 MinIO 는 건드리지 않는다 — 데이터가 그 안에 있다.
#
# 서버 .env 에 STORAGE_TYPE=s3 와 MINIO_ROOT_USER/PASSWORD 를 두면 아래가 한 번
# 띄우고, 이미 떠 있으면 그대로 둔다. 실측 메모리는 유휴 54MB, 100MB 업로드 후 66MB 다.
# 엔드포인트가 이 호스트를 가리킬 때만 MinIO 를 띄운다. 외부 S3·원격 MinIO 를 쓰는
# 배포에서는 띄우지 않는다. (grep -E 는 부정 전방탐색을 지원하지 않아 case 로 쓴다.)
S3_ENDPOINT="$(grep -E '^STORAGE_S3_ENDPOINT=' "$ENV_FILE" 2>/dev/null | tail -1 | cut -d= -f2-)"
LOCAL_OBJECT_STORE=no
if grep -qE '^STORAGE_TYPE=s3$' "$ENV_FILE" 2>/dev/null; then
case "$S3_ENDPOINT" in
""|*127.0.0.1*|*localhost*|*host.docker.internal*) LOCAL_OBJECT_STORE=yes ;;
esac
fi

if [ "$LOCAL_OBJECT_STORE" = yes ]; then
MINIO_MEMORY="$(grep -E '^MINIO_MEMORY=' "$ENV_FILE" 2>/dev/null | tail -1 | cut -d= -f2)"
MINIO_MEMORY="${MINIO_MEMORY:-192m}"

if ! docker ps --format '{{.Names}}' | grep -qx carecode-minio; then
echo "===== MinIO 기동 (메모리 $MINIO_MEMORY) ====="
mkdir -p /opt/carecode/minio
# 127.0.0.1 에만 바인딩한다. 오브젝트 스토리지를 외부에 열 이유가 없고,
# 앱은 같은 호스트에서 붙는다.
docker run -d --name carecode-minio --restart unless-stopped --memory="$MINIO_MEMORY" --memory-swap="$MINIO_MEMORY" -p 127.0.0.1:9000:9000 --env-file "$ENV_FILE" -v /opt/carecode/minio:/data minio/minio:RELEASE.2024-08-29T01-40-52Z server /data || {
echo "::error::MinIO 기동 실패. STORAGE_TYPE=s3 인데 저장소가 없으면 업로드가 전부 실패합니다."
exit 1
}
else
echo "MinIO 가 이미 떠 있습니다 (앱만 교체합니다)."
fi
fi

# host.docker.internal 을 호스트로 매핑한다.
#
# 앱은 브리지 네트워크에서 돌고, MariaDB·Redis·MinIO 는 호스트의 127.0.0.1 에 묶여 있다.
# 컨테이너 안에서 127.0.0.1 은 "그 컨테이너 자신" 이라 그 주소로는 닿지 않는다.
# 그래서 서버 .env 의 접속 주소는 host.docker.internal 이어야 한다
# (예: jdbc:mariadb://host.docker.internal:3306/carecode).
run_app() {
docker run -d --name "$APP" --restart unless-stopped --memory="$APP_MEMORY" --memory-swap="$APP_MEMORY" -v "$UPLOADS_DIR:/app/uploads" -p "$PORT:8082" --env-file "$ENV_FILE" "$1" >/dev/null
docker run -d --name "$APP" --restart unless-stopped --memory="$APP_MEMORY" --memory-swap="$APP_MEMORY" --add-host=host.docker.internal:host-gateway -v "$UPLOADS_DIR:/app/uploads" -p "$PORT:8082" --env-file "$ENV_FILE" "$1" >/dev/null
}

wait_healthy() {
Expand Down
16 changes: 16 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'

implementation 'io.micrometer:micrometer-registry-prometheus'

// S3 호환 오브젝트 스토리지. 엔드포인트를 설정으로 받으므로 AWS S3·MinIO·R2 에 같은 코드가 붙는다.
// (netty 는 비동기 클라이언트용이다. 동기 클라이언트만 쓰므로 apache-client 하나로 충분하다.)
implementation platform('software.amazon.awssdk:bom:2.28.29')
implementation('software.amazon.awssdk:s3') {
exclude group: 'software.amazon.awssdk', module: 'netty-nio-client'
}
implementation 'software.amazon.awssdk:apache-client'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
Expand All @@ -61,6 +69,8 @@ dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
testImplementation 'org.testcontainers:junit-jupiter:1.21.3'
testImplementation 'org.testcontainers:mariadb:1.21.3'
// MinIO 컨테이너로 S3 경로를 실제로 테스트한다. AWS 자격증명 없이 돈다.
testImplementation 'org.testcontainers:minio:1.21.3'

runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
// 소셜 로그인(OAuth2)
Expand Down Expand Up @@ -113,6 +123,12 @@ tasks.withType(Test).configureEach {
// API 스펙 파일 갱신 스위치. Gradle 속성으로 받아 테스트 JVM 까지 넘긴다
// (-D 로 주면 Gradle JVM 에만 붙고 테스트 워커에는 전달되지 않는다).
systemProperty 'openapi.lock.update', project.findProperty('openapi.lock.update') ?: 'false'

// MinIO 테스트 이미지. 기본값은 공식 이미지이고, 그것을 받을 수 없는 환경에서는
// -PminioImage=... 로 S3 호환 대체 이미지를 지정한다.
if (project.hasProperty('minioImage')) {
systemProperty 'minioImage', project.property('minioImage')
}
}

tasks.named('test') {
Expand Down
51 changes: 51 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ services:
REFRESH_COOKIE_SAME_SITE: ${REFRESH_COOKIE_SAME_SITE:-Lax}

STORAGE_ROOT: /app/uploads
# 저장소 종류. local 이 기본이고, `STORAGE_TYPE=s3 docker compose --profile s3 up` 으로
# MinIO 를 함께 띄워 S3 경로를 그대로 써 볼 수 있다.
STORAGE_TYPE: ${STORAGE_TYPE:-local}
STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT:-http://minio:9000}
STORAGE_S3_BUCKET: ${STORAGE_S3_BUCKET:-carecode-uploads}
STORAGE_S3_ACCESS_KEY: ${MINIO_ROOT_USER:-carecode}
STORAGE_S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD:-carecode-local-dev}
# MinIO 는 버킷을 서브도메인으로 쓰려면 DNS 가 필요해서 켠다.
STORAGE_S3_PATH_STYLE: "true"
STORAGE_S3_REGION: ${STORAGE_S3_REGION:-ap-northeast-2}

# `SEED_SAMPLE_DATA=true docker compose up` 처럼 셸에서 넘긴 값이 컨테이너까지 가도록 명시한다.
SEED_SAMPLE_DATA: ${SEED_SAMPLE_DATA:-false}
Expand All @@ -116,6 +126,46 @@ services:
# 512m 에서도 뜨지만(측정 464MB) 여유가 없어 640m 로 둔다.
mem_limit: ${APP_MEMORY:-640m}

# 선택: S3 호환 오브젝트 스토리지. 로컬에서 S3 경로를 AWS 자격증명 없이 써 보기 위한 것이다.
#
# STORAGE_TYPE=s3 docker compose --profile s3 up
#
# 콘솔은 http://localhost:9001 (기본 계정 carecode / carecode-local-dev).
# 실측 메모리: 유휴 54MB, 100MB 업로드 후 66MB.
minio:
image: minio/minio:RELEASE.2024-08-29T01-40-52Z
profiles: ["s3"]
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-carecode}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-carecode-local-dev}
ports:
- "${MINIO_PORT:-9000}:9000"
- "${MINIO_CONSOLE_PORT:-9001}:9001"
volumes:
- minio-data:/data
mem_limit: ${MINIO_MEMORY:-256m}
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 3s
retries: 12

# 버킷을 한 번 만들어 주고 끝난다. 앱이 버킷을 만들지 않는 이유: 운영에서는 버킷 생성 권한을
# 주지 않는 편이 안전하고, 그러면 앱이 기동할 때마다 권한 오류를 만난다.
minio-init:
image: minio/minio:RELEASE.2024-08-29T01-40-52Z
profiles: ["s3"]
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set local http://minio:9000 ${MINIO_ROOT_USER:-carecode} ${MINIO_ROOT_PASSWORD:-carecode-local-dev} &&
mc mb --ignore-existing local/${STORAGE_S3_BUCKET:-carecode-uploads} &&
echo '버킷 준비 완료'
"

# 선택: 업로드 악성코드 검사기. 시그니처 DB 를 받느라 첫 기동에 몇 분 걸리고 메모리를 1GB 넘게 쓴다.
clamav:
image: clamav/clamav:1.4
Expand All @@ -126,3 +176,4 @@ services:
volumes:
mariadb-data:
uploads:
minio-data:
22 changes: 21 additions & 1 deletion docs/features/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@ ssh 인자로 넘기면 서버의 프로세스 목록에 그대로 보입니다.
기동 단계에서 실패합니다(의도된 fail-fast). 검증 단계에서 걸리므로 **운영은 무사합니다**. 이슈 #90
- 컨테이너 이름은 `carecode` 로 통일합니다. 예전 워크플로가 만들던
`carecode-blue` / `carecode-green` 은 교체 단계에서 함께 정리합니다
- **오브젝트 스토리지(MinIO)를 쓰려면** 서버 `.env` 에 `STORAGE_TYPE=s3` 와
`MINIO_ROOT_USER`/`MINIO_ROOT_PASSWORD` 를 둡니다. 배포가 `carecode-minio` 를 한 번
띄우고(메모리 기본 192m, 실측 유휴 54MB·100MB 업로드 후 66MB), 이후 배포는 **앱만
교체합니다** — 데이터가 그 컨테이너 안에 있습니다. 엔드포인트는
`STORAGE_S3_ENDPOINT=http://host.docker.internal:9000`, `STORAGE_S3_PATH_STYLE=true` 입니다.
외부 S3·원격 MinIO 를 쓰면 배포는 MinIO 를 띄우지 않습니다
- **업로드물은 `/opt/carecode/uploads` 에 둡니다.** 배포가 그 디렉터리를 컨테이너의
`/app/uploads` 로 마운트합니다. 볼륨이 없으면 교체할 때마다 업로드물이 컨테이너와 함께
사라집니다 — 사용자가 올린 진단서·프로필 이미지가 **배포마다 전부 날아갑니다.**
Expand Down Expand Up @@ -354,7 +360,21 @@ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

### DB·Redis 준비

배포는 앱만 교체하므로 이 둘은 미리 띄워 둡니다. 앱은 같은 호스트의 `127.0.0.1` 로 붙습니다.
배포는 앱만 교체하므로 이 둘은 미리 띄워 둡니다.

> **접속 주소는 `host.docker.internal` 입니다.** 아래 명령은 DB·Redis 를 호스트의
> `127.0.0.1` 에만 묶는데, 앱은 브리지 네트워크에서 돌기 때문에 **컨테이너 안의
> `127.0.0.1` 은 그 컨테이너 자신**입니다. 그 주소로는 닿지 않습니다.
> 배포가 `--add-host=host.docker.internal:host-gateway` 를 붙이므로 서버 `.env` 에는
> 이렇게 적습니다.
>
> ```
> SPRING_DATASOURCE_URL=jdbc:mariadb://host.docker.internal:3306/carecode
> SPRING_DATA_REDIS_HOST=host.docker.internal
> ```
>
> (`127.0.0.1` 로 적혀 있으면 첫 배포가 기동 단계에서 DB 연결 실패로 멈춥니다. 운영 배포가
> 아직 한 번도 성공하지 못해 드러나지 않은 항목입니다 — 이슈 #94)

```bash
docker run -d --name carecode-mariadb --restart unless-stopped --memory=320m -p 127.0.0.1:3306:3306 -e MARIADB_DATABASE=carecode -e MARIADB_USER=carecode -e MARIADB_PASSWORD=... -e MARIADB_ROOT_PASSWORD=... -e TZ=Asia/Seoul -v carecode-db:/var/lib/mysql mariadb:10.11 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --lower-case-table-names=0 --innodb-buffer-pool-size=96M --performance-schema=OFF --max-connections=30
Expand Down
Loading
Loading