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
80 changes: 0 additions & 80 deletions .github/workflows/deploy-to-test-server.yml

This file was deleted.

72 changes: 72 additions & 0 deletions .github/workflows/publish-homelab-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Publish homelab image
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: homelab-image-${{ github.ref }}
cancel-in-progress: false
jobs:
publish:
if: github.repository == 'redotlabs/server' && github.ref == 'refs/heads/develop'
runs-on: ubuntu-24.04
timeout-minutes: 35
permissions:
contents: read
packages: write
env:
IMAGE: ghcr.io/redotlabs/server-homelab
DOCKER_BUILD_SUMMARY: 'false'
DOCKER_BUILD_RECORD_UPLOAD: 'false'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: PostgreSQL concurrency regression tests
run: ./gradlew clean test
- name: Isolated tests, Flyway/Hibernate validation and secret-free JAR
run: python3 scripts/test-kubernetes.py
- name: Verify packaged configuration allowlist
run: python3 scripts/check-kubernetes-jar.py
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Refuse to overwrite a published commit tag
shell: bash
run: |
set -euo pipefail
tag="$IMAGE:sha-$GITHUB_SHA"
if docker buildx imagetools inspect "$tag" > /dev/null 2> lookup-error.txt; then
echo 'This commit is already published; use its existing digest.' >&2
exit 1
fi
# Authentication/network failures must not be mistaken for an absent tag.
if ! grep -Eqi 'manifest unknown|not found' lookup-error.txt; then
echo 'Cannot establish tag absence; stop without publishing.' >&2
exit 1
fi
rm lookup-error.txt
- name: Build and publish amd64 image
id: image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: Dockerfile.kubernetes
platforms: linux/amd64
push: true
tags: ghcr.io/redotlabs/server-homelab:sha-${{ github.sha }}
labels: |
org.opencontainers.image.source=https://github.com/redotlabs/server
org.opencontainers.image.revision=${{ github.sha }}
provenance: mode=min
- name: Record deployment reference (not credentials)
env:
IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
run: printf '%s@%s\n' "$IMAGE" "$IMAGE_DIGEST" >> "$GITHUB_STEP_SUMMARY"
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ permissions:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 35
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
Expand All @@ -19,6 +19,10 @@ jobs:
java-version: '21'
- name: Build and test with disposable PostgreSQL
run: ./gradlew clean build
- name: Homelab application and runtime validation
run: python3 scripts/test-kubernetes.py
- name: Verify packaged configuration allowlist
run: python3 scripts/check-kubernetes-jar.py
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ __pycache__/

devtools/
devtools/**

# Public runtime template only; never commit actual environment-specific credentials.
!src/main/resources/application-homelab.yml
10 changes: 10 additions & 0 deletions DEPLOY.md
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
# 배포 경로

새 홈서버는 `eraser502/homelab`을 GitOps 기준으로 사용한다. 소스 통합 후 검증·수동 이미지 발행·GitOps PR·수동 Sync 순서는 [홈랩 발행 문서](docs/HOMELAB-IMAGE-PUBLISHING.md)를 따른다.

기존 develop 테스트 서버 자동 배포는 폐기했다. main의 기존 EC2 workflow는 별도이며 유지한다.

## 기존 기록

- 251130 deploy test

Kubernetes 배포 준비는 [별도 문서](docs/KUBERNETES-PREPARATION.md)를 따른다. 기존 EC2 workflow의 image는 runtime 설정을 포함하므로 새 홈랩 image로 재사용하지 않는다.
8 changes: 8 additions & 0 deletions Dockerfile.kubernetes
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Built and tested by python3 scripts/test-kubernetes.py.
# Pin the linux/amd64 runtime manifest; first deployment is N100 only.
FROM eclipse-temurin:21-jre@sha256:628467024d428251da635661936d6180717b1ac1119d41c76d28a03b7293c43c
WORKDIR /app
COPY --chown=10001:10001 build/kubernetes/app.jar /app/app.jar
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
6 changes: 6 additions & 0 deletions Dockerfile.kubernetes.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
**
!Dockerfile.kubernetes
!Dockerfile.kubernetes.dockerignore
!build/
!build/kubernetes/
!build/kubernetes/app.jar
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Redot Server

Kubernetes 배포 준비와 런타임 변수 계약: [KUBERNETES-PREPARATION](docs/KUBERNETES-PREPARATION.md). 아직 배포하지 않았으며 기존 production과 분리된 경로다.

홈랩 전용 이미지 발행 준비: [HOMELAB-IMAGE-PUBLISHING.md](docs/HOMELAB-IMAGE-PUBLISHING.md). 아직 commit/push 및 배포하지 않았습니다.
44 changes: 43 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'
compileOnly 'org.projectlombok:lombok'
Expand Down Expand Up @@ -75,7 +76,7 @@ dependencies {
}

tasks.named('test') {
useJUnitPlatform { excludeTags 'experiment' }
useJUnitPlatform { excludeTags 'integration', 'experiment' }
}

// Opt-in local service/DB measurement; no production endpoint is contacted.
Expand All @@ -88,3 +89,44 @@ tasks.register('inquiryExperiment', Test) {
systemProperty 'experiment.output', providers.gradleProperty('experimentOutput').getOrElse('build/inquiry-experiment')
systemProperty 'experiment.variant', providers.gradleProperty('experimentVariant').getOrElse('fixed')
}

tasks.register('integrationTest', Test) {
group = 'verification'
description = 'Run full application/Flyway tests against isolated PostgreSQL and Redis'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform { includeTags 'integration' }
shouldRunAfter tasks.named('test')
}


// Kubernetes packaging is separate from legacy prod/dev workflows.
// Only reviewed, credential-free configuration and migrations enter this image.
def kubernetesResources = tasks.register('kubernetesResources', Sync) {
from('src/main/resources') {
include 'application.yml', 'application-homelab.yml', 'db/migration/*.sql'
}
into layout.buildDirectory.dir('kubernetes-resources')
}

tasks.register('kubernetesBootJar', org.springframework.boot.gradle.tasks.bundling.BootJar) {
group = 'build'
description = 'Build an environment-independent JAR without legacy injected profile files'
dependsOn tasks.named('classes'), kubernetesResources
archiveFileName = 'app.jar'
destinationDirectory = layout.buildDirectory.dir('kubernetes')
mainClass = 'redot.redot_server.RedotServerApplication'
targetJavaVersion = JavaVersion.VERSION_21
classpath = sourceSets.main.runtimeClasspath.filter {
it != sourceSets.main.output.resourcesDir
} + files(kubernetesResources.map { it.destinationDir })
preserveFileTimestamps = false
reproducibleFileOrder = true
}

// Container-backed tests run on the host; this task needs no Docker socket.
tasks.register('homelabUnitTest', Test) {
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform { excludeTags 'integration', 'experiment', 'container' }
}
67 changes: 67 additions & 0 deletions docs/HOMELAB-IMAGE-PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Homelab 이미지 발행

## 2026-10-14 통합 이후 절차

- 소스 기준: `develop`. 문의번호 동시성 수정과 기존 Kubernetes 준비 브랜치를 통합한다.
- PR 검증: 호스트 Java 21 + Docker에서 `./gradlew clean build`, 이어서 `python3 scripts/test-kubernetes.py`와 `python3 scripts/check-kubernetes-jar.py`.
- `test`는 PostgreSQL Testcontainers 회귀 테스트와 홈랩 단위 테스트를 실행한다. `homelabUnitTest`는 Docker 소켓이 없는 테스트 컨테이너에서 실행할 단위 테스트만 선택한다. `integrationTest`는 실제 임시 PostgreSQL/Redis에 연결한 homelab 프로필을 검증한다. 두 경로를 합쳐야 전체 검증이다.
- 이미지 발행: `Publish homelab image`를 `develop`에서 **수동 실행**한다. PR 생성·develop push/merge는 이미지 발행이나 배포를 실행하지 않는다. 현재 GitHub 기본 브랜치는 develop으로 확인했다. PR 병합 후 해당 브랜치에서 수동 실행할 수 있다.
- 발행 workflow도 양쪽 테스트 및 공개 리소스 허용목록을 통과해야 GHCR에 `sha-<full commit>`을 게시한다. 애플리케이션 비밀값 없이 빌드한다.
- 그 다음 `eraser502/homelab`의 `applications/redot/resources.yaml`에서 검증된 image tag와 registry digest를 갱신하는 별도 PR을 준비한다. Argo CD 수동 Sync 후 readiness·문의 생성/조회·로그를 확인한다.
- 예전 develop → Docker Hub → `eraser502/gitops-repo/apps/redot-test` 배포 workflow는 제거한다. main의 EC2 workflow는 이번 범위에 포함하지 않는다.
- 이번 통합 작업 범위는 테스트와 이슈·PR까지다. 실제 이미지 발행, GitOps digest 변경, Sync와 운영 데이터 쓰기는 후속 배포 단계다.
- CI의 일반 `test` 보고서만 업로드한다. 임시 DB/Redis 연결 정보를 포함할 수 있는 homelab 통합 보고서는 로컬/runner에만 둔다.

아래는 최초 준비 당시의 **2026-09-27 이력**이다. 당시의 미배포·브랜치·테스트 수·승인 단계는 현재 상태를 나타내지 않는다.

---

# Homelab 이미지 발행 준비

2026-09-27: 파일 작성/로컬 검증만 완료. commit/push/image publish는 미실행.

- 별도 workflow: `.github/workflows/publish-homelab-image.yml`.
- 초기 배포 브랜치: `prepare/kubernetes-runtime`. main/develop의 기존 EC2/test workflow는 그대로다.
- 승인된 Registry/image: GHCR `ghcr.io/redotlabs/server-homelab`, Private. Organization의 Private package 생성 허용 여부는 아직 확인되지 않았으며 승인 후 실제 최초 publish 결과로 검증한다.
- push trigger는 준비 브랜치만. workflow_dispatch는 준비 브랜치/main으로 제한하지만 default branch에 workflow가 있어야 UI 사용 가능하다. 최초는 준비 브랜치 push 방식.
- Test: `python3 scripts/test-kubernetes.py`. 고정 PostgreSQL/Redis의 임시 Docker fixture로 17개 테스트/Flyway/Hibernate 및 Redis AOF 검증 후 `kubernetesBootJar` 생성.
- `python3 scripts/check-kubernetes-jar.py`로 JAR resource allowlist/공개 소스 일치를 검사.
- Dockerfile.kubernetes, amd64, non-root UID10001, pinned Java21 JRE.
- GHCR publish는 job의 GITHUB_TOKEN contents:read/packages:write. 기존 repository secret YAML이나 DockerHub/EC2 credential을 참조하지 않는다.
- tag는 sha-전체commit. 이미 있는 tag는 덮어쓰기 거부. registry 조회 실패 시 발행 중단.
- registry 자체 불변성을 가정하지 않는다. Homelab에는 발행된 `:sha-<commit>@sha256:<digest>`를 사용한다. local image ID/JAR SHA를 registry digest 대신 넣지 않는다.
- Private package를 유지한다. Kubernetes pull credential은 실제 package 생성 후에만 준비한다. package Read 권한 계정의 classic PAT read:packages + SOPS Secret을 사용한다. 별도 publish PAT는 생성하지 않는다. Argo SSH key는 image pull credential이 아니다.
- DB/OAuth/JWT/SMTP/AWS/Discord 값은 JAR/image/Actions secret YAML에서 분리하여 Kubernetes runtime에만 주입한다.
- workflow나 이미지에는 실제 application credential이 필요 없다. Secret은 image build-arg로도 전달하지 않는다.
- test logs/reports는 runtime fixture 정보가 들어갈 수 있어 자동 artifact 업로드하지 않는다. Buildx build record 업로드도 비활성화했다.

로컬 검증: unit12/integration5 통과, Docker build 통과, packaging allowlist 통과, actionlint1.7.12 통과. 실제 GitHub workflow/registry push는 승인 전 미실행이다.

Homelab 상세 설계는 homelab repo의 `docs/19-redot-image-gitops-preparation.md`와 `applications/redot`에 있다. frontend는 아직 미확정이다. Google callback은 `https://redot-api.dhxxn.kr/api/v1/sign-in/redot-member/social/callback/redot-member-google`.

다음 단계는 파일 리뷰 후 Application repository commit/push 및 그에 따른 최초 publish 승인이다. 현재는 commit/push 직전에 멈춘다. 준비 브랜치 push는 publish workflow를 자동 실행하므로 push와 image publish를 별개인 것처럼 취급하지 않는다. 이 workflow는 Homelab Git을 수정하거나 Kubernetes에 접근하지 않는다.


## 승인된 실행 순서와 담당 (2026-09-27)

현재 단계에서는 모든 실제 credential 입력을 보류한다. frontend origin/redirect도 미정 상태다.

| 순서 | 담당 | 수행할 작업 | Secret 입력 |
|---|---|---|---|
| 1 | Codex | 로컬 브랜치/17개 테스트/JAR/image/workflow 최종 검사와 변경 보고 후 중단 | 없음 |
| 2 | 사용자 | Application repo commit/push와 이에 따른 최초 image publish를 명시적으로 승인 | 없음 |
| 3 | Codex | 승인된 파일만 commit. 인증이 이미 제공된 범위면 준비 브랜치에 push; 인증 부재 시 사용자 N100 push로 인계 | Codex가 값을 받지 않음 |
| 4 | 사용자 N100 터미널 | 인계받은 경우에만 git push -u origin prepare/kubernetes-runtime. 개인 terminal의 Git 인증 prompt에서 입력; credential을 command line/history/docs에 넣지 않음 | Git push 인증이 필요할 때만 |
| 5 | GitHub Actions / Codex | GITHUB_TOKEN으로 최초 발행 시도. Actions 결과와 오류를 확인해 org package 생성 권한을 실제 검증. SHA/digest/amd64 확인 | publish PAT 불필요 |
| 6 | 사용자 GitHub UI | 성공하면 Package settings에서 Private, redotlabs/server 연결/Actions 접근 확인. package 생성 정책 오류면 조직 Settings→Packages→Package Creation의 Private 허용을 관리자가 수정. 기존 package 접근 오류면 Manage Actions access에서 repository Write 확인 | 없음 |
| 7 | Codex | 실패 시 credential을 추가하는 대신 원인별 수정 계획 제시. 성공 시 실제 package read 권한/visibility 확인 및 pull 준비. 권한 오류 때 재실행도 사용자와 조율 | 없음 |
| 8 | 사용자 GitHub UI | package 생성 성공 후 pull 계정의 package Read 접근을 부여하고 classic PAT read:packages 생성, 필요한 SSO 승인 | PAT 생성; 채팅 공유 금지 |
| 9 | Codex / 사용자 N100 터미널 | 그 시점에 registry 전용 숨김 입력 절차를 준비한 뒤 사용자 입력→SOPS pull Secret 파일 준비→안전한 pull 인증 검증 | pull credential만 |
| 10 | 사용자 / Codex | 별도 단계에서 frontend 일반 설정과 application credential을 준비하고 runtime 설정 검증 | Google/SMTP/AWS 등은 이 단계까지 보류 |
| 11 | 사용자 / Codex | Homelab 변경 review 및 별도 commit/push 승인. Kubernetes 등록/Sync는 다시 별도 배포 승인 후 | 기존 SOPS 파일 사용 |

현재 기존 `prepare-redot-secrets.py`는 application credential과 pull credential을 함께 묻는다. 지금 실행하지 않는다. package 생성 후 pull만 먼저 준비할 때는 registry 전용 입력 절차를 별도로 준비한다. 장기 Git push PAT와 registry pull PAT도 용도/권한을 혼동하지 않는다.

GitHub UI에서 workflow를 수동 실행하거나 기존 EC2 workflow를 실행할 필요는 없다. 첫 준비 브랜치 push가 신규 workflow의 실행 계기다. 워크플로우는 private visibility를 강제로 설정하는 API 작업을 하지 않으므로 실제 package 설정은 최초 발행 후 확인한다. package가 이미 존재한다면 그 visibility/접근 권한 확인이 필요하며 public으로 된 기존 package를 private이라고 간주하지 않는다.

최종 검사에서 Gradle UP-TO-DATE 재사용을 발견하여 홈랩 검증 스크립트에 `--rerun-tasks`를 추가했다. 임시 DB/Redis가 매번 새로 생성되므로 테스트도 매번 실제 실행한다. 이전 XML 존재만으로 신규 DB migration 검증을 주장하지 않는다. 기존 EC2 workflow 명령에는 영향이 없다.
Loading
Loading