Skip to content

Commit 41b5e52

Browse files
Merge pull request #43 from DevKor-github/develop
새록 어드민 (운영) 배포
2 parents fe44702 + e07e30f commit 41b5e52

108 files changed

Lines changed: 7819 additions & 4 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.codex/brief.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# brief
2+
- Read BE contracts from /workspace/saerok-BE and context/saerok-BE/endpoints.txt.
3+
- Never start BE here. Use WireMock stubs in tests; no outbound network.
4+
- Use a single RestClient bean configured by `saerok.api.base-url`.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: Deploy Admin to Dev (EC2 + Docker Compose)
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
deploy:
8+
runs-on: ubuntu-latest
9+
permissions:
10+
contents: read
11+
packages: write
12+
environment: dev
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
# (옵션) BE와 동일 스타일 - 리포지토리명 소문자/메타 태그
18+
- name: Normalize image repo (lowercase)
19+
id: normalize
20+
run: echo "image_repo=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
21+
22+
- name: Docker Buildx
23+
uses: docker/setup-buildx-action@v3
24+
25+
- name: GHCR login
26+
uses: docker/login-action@v3
27+
with:
28+
registry: ghcr.io
29+
username: ${{ secrets.GHCR_USERNAME }}
30+
password: ${{ secrets.GHCR_TOKEN }}
31+
32+
- name: Meta (tags/labels)
33+
id: meta
34+
uses: docker/metadata-action@v5
35+
with:
36+
images: ${{ steps.normalize.outputs.image_repo }}
37+
tags: |
38+
type=raw,value=dev-latest
39+
type=sha,format=long,prefix=dev-
40+
41+
- name: Build & Push
42+
uses: docker/build-push-action@v6
43+
with:
44+
context: .
45+
file: ./Dockerfile
46+
push: true
47+
tags: ${{ steps.meta.outputs.tags }}
48+
labels: ${{ steps.meta.outputs.labels }}
49+
cache-from: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache
50+
cache-to: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache,mode=max
51+
52+
- name: Copy compose files to EC2
53+
uses: appleboy/scp-action@v0.1.7
54+
with:
55+
host: ${{ secrets.EC2_HOST }}
56+
username: ubuntu
57+
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
58+
source: "deploy/docker-compose.yml,deploy/docker-compose.dev.yml"
59+
target: "~/saerok-admin"
60+
strip_components: 1
61+
62+
- name: Deploy on EC2
63+
uses: appleboy/ssh-action@v1
64+
with:
65+
host: ${{ secrets.EC2_HOST }}
66+
username: ubuntu
67+
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
68+
script: |
69+
set -euo pipefail
70+
cd ~/saerok-admin
71+
72+
# 0) Docker & Compose must exist
73+
docker --version
74+
docker compose version
75+
76+
# 1) GHCR login for pull
77+
echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin
78+
79+
# 2) Create ephemeral env file in RAM (owned by current user) and ensure cleanup
80+
# /run 은 tmpfs라 재부팅 시 날아감(의도된 동작)
81+
sudo install -d -m 0700 -o "$(id -un)" -g "$(id -gn)" /run/saerok-admin
82+
83+
ENV_FILE="/run/saerok-admin/env.dev"
84+
umask 177
85+
: > "$ENV_FILE"
86+
cleanup() { shred -u "$ENV_FILE" || rm -f "$ENV_FILE"; }
87+
trap cleanup EXIT
88+
89+
add_kv() { printf '%s=%s\n' "$1" "$2" >> "$ENV_FILE"; }
90+
91+
# 2-1) Runtime envs (dev)
92+
add_kv SAEROK_API_BASE_URL "${{ secrets.SAEROK_API_BASE_URL }}"
93+
add_kv SAEROK_API_PREFIX "${{ secrets.SAEROK_API_PREFIX }}"
94+
95+
# Social login (admin 전용 redirect)
96+
add_kv KAKAO_CLIENT_ID "${{ secrets.KAKAO_CLIENT_ID }}"
97+
add_kv KAKAO_REDIRECT_URI "${{ secrets.KAKAO_REDIRECT_URI }}"
98+
add_kv APPLE_CLIENT_ID "${{ secrets.APPLE_CLIENT_ID }}"
99+
add_kv APPLE_REDIRECT_URI "${{ secrets.APPLE_REDIRECT_URI }}"
100+
add_kv UNSPLASH_ACCESS_KEY "${{ secrets.UNSPLASH_ACCESS_KEY }}"
101+
add_kv UNSPLASH_APP_NAME "${{ secrets.UNSPLASH_APP_NAME }}"
102+
103+
# 3) Pull & Up with dev overlay (limits in dev.yml)
104+
docker compose -p saerok-admin -f docker-compose.yml -f docker-compose.dev.yml pull --quiet
105+
docker compose -p saerok-admin -f docker-compose.yml -f docker-compose.dev.yml up -d --force-recreate --quiet-pull
106+
107+
# 4) Healthcheck: /login (HEAD OK)
108+
for i in {1..45}; do
109+
if curl -fsS -I http://localhost:8081/login > /dev/null; then
110+
echo "✅ admin up"
111+
docker image prune -af --filter "until=24h" || true
112+
docker builder prune -af --filter "until=24h" || true
113+
exit 0
114+
fi
115+
echo "⏳ ($i/45)"; sleep 3
116+
done
117+
118+
echo "❌ admin health check failed"
119+
docker logs saerok-admin-dev --tail=200 || true
120+
exit 1
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
name: Deploy Admin to Prod (EC2 + Docker Compose)
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
workflow_dispatch:
8+
9+
jobs:
10+
deploy:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
packages: write
15+
environment: prod
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
# (옵션) BE와 동일 스타일 - 리포지토리명 소문자/메타 태그
21+
- name: Normalize image repo (lowercase)
22+
id: normalize
23+
run: echo "image_repo=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
24+
25+
- name: Docker Buildx
26+
uses: docker/setup-buildx-action@v3
27+
28+
- name: GHCR login
29+
uses: docker/login-action@v3
30+
with:
31+
registry: ghcr.io
32+
username: ${{ secrets.GHCR_USERNAME }}
33+
password: ${{ secrets.GHCR_TOKEN }}
34+
35+
- name: Meta (tags/labels)
36+
id: meta
37+
uses: docker/metadata-action@v5
38+
with:
39+
images: ${{ steps.normalize.outputs.image_repo }}
40+
tags: |
41+
type=raw,value=prod-latest
42+
type=sha,format=long,prefix=prod-
43+
44+
- name: Build & Push
45+
uses: docker/build-push-action@v6
46+
with:
47+
context: .
48+
file: ./Dockerfile
49+
push: true
50+
tags: ${{ steps.meta.outputs.tags }}
51+
labels: ${{ steps.meta.outputs.labels }}
52+
cache-from: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache
53+
cache-to: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache,mode=max
54+
55+
- name: Copy compose files to EC2
56+
uses: appleboy/scp-action@v0.1.7
57+
with:
58+
host: ${{ secrets.EC2_HOST }}
59+
username: ubuntu
60+
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
61+
source: "deploy/docker-compose.yml,deploy/docker-compose.prod.yml"
62+
target: "~/saerok-admin"
63+
strip_components: 1
64+
65+
- name: Deploy on EC2
66+
uses: appleboy/ssh-action@v1
67+
with:
68+
host: ${{ secrets.EC2_HOST }}
69+
username: ubuntu
70+
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
71+
script: |
72+
set -euo pipefail
73+
cd ~/saerok-admin
74+
75+
# 0) Docker & Compose must exist
76+
docker --version
77+
docker compose version
78+
79+
# 1) GHCR login for pull
80+
echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin
81+
82+
# 2) Create ephemeral env file in RAM (owned by current user) and ensure cleanup
83+
# /run 은 tmpfs라 재부팅 시 날아감(의도된 동작)
84+
sudo install -d -m 0700 -o "$(id -un)" -g "$(id -gn)" /run/saerok-admin
85+
86+
ENV_FILE="/run/saerok-admin/env.prod"
87+
umask 177
88+
: > "$ENV_FILE"
89+
cleanup() { shred -u "$ENV_FILE" || rm -f "$ENV_FILE"; }
90+
trap cleanup EXIT
91+
92+
add_kv() { printf '%s=%s\n' "$1" "$2" >> "$ENV_FILE"; }
93+
94+
# 2-1) Runtime envs (prod)
95+
add_kv SAEROK_API_BASE_URL "${{ secrets.SAEROK_API_BASE_URL }}"
96+
add_kv SAEROK_API_PREFIX "${{ secrets.SAEROK_API_PREFIX }}"
97+
98+
# Social login (admin 전용 redirect)
99+
add_kv KAKAO_CLIENT_ID "${{ secrets.KAKAO_CLIENT_ID }}"
100+
add_kv KAKAO_REDIRECT_URI "${{ secrets.KAKAO_REDIRECT_URI }}"
101+
add_kv APPLE_CLIENT_ID "${{ secrets.APPLE_CLIENT_ID }}"
102+
add_kv APPLE_REDIRECT_URI "${{ secrets.APPLE_REDIRECT_URI }}"
103+
add_kv UNSPLASH_ACCESS_KEY "${{ secrets.UNSPLASH_ACCESS_KEY }}"
104+
add_kv UNSPLASH_APP_NAME "${{ secrets.UNSPLASH_APP_NAME }}"
105+
106+
# 3) Pull & Up with prod overlay (limits in prod.yml)
107+
docker compose -p saerok-admin -f docker-compose.yml -f docker-compose.prod.yml pull --quiet
108+
docker compose -p saerok-admin -f docker-compose.yml -f docker-compose.prod.yml up -d --force-recreate --quiet-pull
109+
110+
# 4) Healthcheck: /login (HEAD OK)
111+
for i in {1..45}; do
112+
if curl -fsS -I http://localhost:8081/login > /dev/null; then
113+
echo "✅ admin up"
114+
docker image prune -af --filter "until=24h" || true
115+
docker builder prune -af --filter "until=24h" || true
116+
exit 0
117+
fi
118+
echo "⏳ ($i/45)"; sleep 3
119+
done
120+
121+
echo "❌ admin health check failed"
122+
docker logs saerok-admin-prod --tail=200 || true
123+
exit 1

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ out/
3535

3636
### VS Code ###
3737
.vscode/
38+
39+
.env
40+
context/

AGENTS.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# AGENTS
2+
3+
- Editable code: /workspace/saerok-admin
4+
- Read-only context: /workspace/saerok-BE (cloned by setup), context/saerok-BE/endpoints.txt
5+
- Do NOT run saerok-BE in this container. (BE runs locally via ./up.sh ./down.sh)
6+
7+
- API contract source:
8+
- Controllers/DTOs under /workspace/saerok-BE/src/main/**
9+
- Quick index: context/saerok-BE/endpoints.txt
10+
- Global prefix in BE is configurable (api_prefix). Treat it as external config.
11+
12+
- How to run admin here:
13+
- Build: ./gradlew clean build --console=plain
14+
- Run: ./gradlew bootRun --console=plain --args='--server.port=8081'
15+
- Inject BE base URL via: --saerok.api.base-url=... or env SAEROK_API_BASE_URL
16+
17+
## Coding conventions
18+
19+
To keep the admin service consistent with the existing style in `saerok-BE`, follow these conventions for all Java and Kotlin source under this repository:
20+
21+
- **Indentation:** Use 4 spaces per indentation level. Do not use tab characters. Method bodies, control-flow blocks, and class members should be indented once under their declarations, mirroring the backend project.
22+
- **Braces:** Place opening braces on the same line as the declaration (classes, methods, `if`/`else`, `try`/`catch`, etc.) and closing braces on their own line aligned with the start of the declaration.
23+
- **Spacing:** Keep a single space between keywords and parentheses (e.g., `if (`), around binary operators, and after commas. Avoid trailing whitespace at the end of lines.
24+
- **Imports:** Group imports by package (standard library, third-party, project) and keep each group sorted alphabetically without unused imports. Leave a single blank line between groups, matching the structure in `saerok-BE`.
25+
- **Blank lines:** Use blank lines to separate logical sections—between package and import statements, between import groups, and between class members when it improves readability. Avoid multiple consecutive blank lines.
26+
- **Naming:** Follow Java naming conventions as seen in `saerok-BE`: `PascalCase` for classes, `camelCase` for methods and variables, `UPPER_SNAKE_CASE` for constants.
27+
- **Annotations:** Place annotations directly above the element they decorate without blank lines, preserving the order used in the backend (Spring stereotypes closest to the declaration, followed by additional annotations).
28+
29+
These rules should be treated as mandatory for any code you add or modify so that both repositories share the same formatting baseline.
30+
31+
### Thymeleaf templates
32+
33+
- Prefer Thymeleaf literal syntax (`|...|`) or dedicated `th:onclick`, `th:href`, etc. attributes when interpolating strings that contain quotes or concatenating literals with expressions. Avoid building inline JavaScript or attribute values with nested quotes inside `th:attr`, as it triggers parsing errors.
34+
- Use the utility methods that actually exist in Thymeleaf expression objects. For example, prefer `#strings.isEmpty(...)`/`!#strings.isEmpty(...)` rather than `#strings.hasText(...)`, which is unavailable in Thymeleaf 3.
35+
36+
### Gotchas
37+
38+
- When building ternary expressions that concatenate literals and values (e.g., `cond ? 'text' : value + 'suffix'`), always wrap the entire expression with Thymeleaf's literal syntax (`|...|`). This prevents parsing errors like the one we recently fixed in `reports/list.html` and keeps the expression engine from misinterpreting string concatenations.

Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# ===== Build stage =====
2+
FROM gradle:8.8-jdk21 AS builder
3+
WORKDIR /app
4+
5+
COPY build.gradle settings.gradle gradlew ./
6+
COPY gradle gradle
7+
RUN chmod +x gradlew
8+
RUN ./gradlew --no-daemon dependencies || true
9+
10+
COPY . .
11+
RUN chmod +x gradlew
12+
RUN ./gradlew --no-daemon clean bootJar
13+
14+
# ===== Runtime stage =====
15+
FROM eclipse-temurin:21-jre
16+
ENV TZ=Asia/Seoul
17+
WORKDIR /app
18+
19+
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
20+
21+
COPY --from=builder /app/build/libs/*.jar app.jar
22+
EXPOSE 8081
23+
24+
ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar /app/app.jar --server.port=${SERVER_PORT:-8081} --spring.profiles.active=${SPRING_PROFILES_ACTIVE:-local}"]

build.gradle

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ dependencies {
3333
developmentOnly 'org.springframework.boot:spring-boot-devtools'
3434
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
3535
annotationProcessor 'org.projectlombok:lombok'
36-
testImplementation 'org.springframework.boot:spring-boot-starter-test'
37-
testImplementation 'org.springframework.security:spring-security-test'
38-
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
36+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
37+
testImplementation 'org.springframework.security:spring-security-test'
38+
testImplementation 'org.wiremock:wiremock-standalone:3.6.0'
39+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
3940
}
4041

4142
tasks.named('test') {

deploy/docker-compose.dev.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
services:
2+
app:
3+
image: ghcr.io/devkor-github/saerok-admin:dev-latest
4+
container_name: saerok-admin-dev
5+
restart: unless-stopped
6+
stop_grace_period: 15s
7+
ports: ["8081:8081"]
8+
9+
# t2.micro 안전 제한 (BE와 합쳐도 1GiB 넘지 않게)
10+
cpus: "0.10"
11+
mem_limit: "256m"
12+
13+
# 런타임에 만드는 RAM env 파일
14+
env_file:
15+
- /run/saerok-admin/env.dev
16+
17+
environment:
18+
# dev 오버레이에서 명시적으로 고정
19+
SPRING_PROFILES_ACTIVE: "dev"
20+
# 프록시 뒤 HTTPS 인식 (X-Forwarded-Proto 처리)
21+
SERVER_FORWARD_HEADERS_STRATEGY: "native"
22+
# 자바 메모리/타임존 튜닝
23+
JAVA_OPTS: >-
24+
-Duser.timezone=Asia/Seoul
25+
-Xms64m -Xmx110m
26+
-XX:MaxMetaspaceSize=64m
27+
-XX:MaxDirectMemorySize=16m
28+
-XX:ReservedCodeCacheSize=32m
29+
-XX:+UseSerialGC
30+
-XX:+ExitOnOutOfMemoryError

0 commit comments

Comments
 (0)