SSAFY 15기 A705 팀 PinLog 프로젝트의 배포 인프라. k3s + ArgoCD GitOps 구조이며, 이 저장소가 클러스터 상태의 단일 진실 원천이다.
Terraform을 쓰지 않는 이유 서버는 SSAFY 소유 AWS 계정에 이미 프로비저닝되어 배정된 자원이고, 팀에 AWS API 자격증명이 없다. 클라우드 API로 생성·관리할 대상이 없으므로 이 저장소는 인프라 프로비저닝이 아니라 배포 인프라를 담는다.
| 문서 | 내용 | 읽어야 할 사람 |
|---|---|---|
| docs/onboarding.md | 프로젝트 전체 그림, 주소 모음, 역할별 안내 | 새로 합류한 모든 팀원 (여기부터) |
| docs/architecture.md | 시스템 구조, 설계 결정과 근거, 구축 중 겪은 함정 | 인프라 담당, 구조를 이해해야 하는 사람 |
| docs/runbook.md | 장애 대응, 트러블슈팅, 자주 쓰는 명령 | 배포가 안 될 때 보는 문서 |
| PostgreSQL pgvector 전환 | 승인·백업·검증·단계별 rollback 절차 | 인프라 담당 |
| docs/monitoring.md | Grafana 접속, 로그·메트릭 보는 법, 서비스 메트릭 연동 | 메트릭·로그를 보려는 모든 팀원 |
| Git/CI 거버넌스 | 브랜치 보호, PR·CI, Dependabot, 공급망 보안 | 저장소를 변경하는 모든 팀원 |
| 운영 알림 | Alertmanager·Sentinel·외부 HTTPS 모니터와 알림 정책 | 인프라·운영 담당 |
| NetworkPolicy | namespace ingress 격리, 허용 통신 계약, 검증·rollback | 인프라·서비스 담당 |
| AI dev Infra 선행조건 | DB/Flyway·runtime secret·bootstrap gate preflight와 rollback | AI·Backend·인프라 담당 |
| Pod Security Admission | restricted audit/warn, 서비스 보안 계약, enforce 전환 조건 | 인프라·서비스 담당 |
| 컨테이너 runtime | K3s embedded containerd 계약, 검증·migration·rollback | 인프라 담당 |
| metrics-server | kubelet stats 지연 tuning, 검증·rollback | 인프라 담당 |
| docs/backend-conventions.md | 경로 규약, 설정 방법, 체크리스트 | 백엔드 개발하는 모든 팀원 (필독) |
| examples/README.md | 새 서비스 추가 절차와 규약 | 서비스를 만드는 팀원 |
| secrets/README.md | 시크릿 관리 (Sealed Secrets) | 시크릿을 다루는 사람 |
| 이 문서 | 부트스트랩 절차, 환경 개요 | 클러스터를 처음 세우는 사람 |
처음 오셨다면: 서비스를 배포하려는 팀원은 examples/README.md만 읽으면 됩니다. 배포가 실패했다면 docs/runbook.md의 증상별 대응을 보세요.
| 항목 | 값 |
|---|---|
| 호스트 | i15a705.p.ssafy.io (15.165.74.216) |
| 사양 | 4 vCPU / 15Gi RAM / 309G 디스크, swap 없음 |
| OS | Ubuntu 24.04.3, 커널 6.17, cgroup v2 |
| 컨테이너 runtime | K3s embedded containerd |
| 리전 | ap-northeast-2a |
| 공개 포트 | 22, 443 (Ingress), 8989 (SSAFY Gerrit) |
| 서비스 | 포트 | 비고 |
|---|---|---|
| Gerrit 3.13.1 | 8988, 29418 | 팀은 GitHub를 사용. 저장소 0개 |
| Apache httpd | 8989 | Gerrit 리버스 프록시 |
Java 21 (/opt/java) |
— | Gerrit 전용 |
k3s는 80/443만 사용하므로 위와 충돌하지 않는다.
⚠️ Gerrit에 팀 코드를 올리지 말 것.auth.type = DEVELOPMENT_BECOME_ANY_ACCOUNT가 설정되어 있어 누구나 아무 계정으로 로그인할 수 있고, 이 설정은 인터넷에서 확인 가능하다. SSAFY 기본 템플릿이라 팀이 임의로 바꾸지 않고, 대신 GitHub를 주 저장소로 쓴다.
⚠️ /opt/httpd/conf/extra/httpd-ssl.conf36행에Listen 443이 있다. 현재httpd.conf510행에서 Include가 주석 처리되어 무해하지만, 누군가 주석을 풀면 Apache와 Traefik이 443을 두고 충돌한다.
infra/
├── bootstrap/ 1회성 호스트 스크립트 (GitOps 대상 아님)
├── charts/microservice 모든 서비스가 공용하는 Helm 차트 1개
├── apps/{prod,dev}/ 서비스별 values.yaml — 여기에 디렉터리를 추가하면 배포된다
├── platform/ 네임스페이스, Ingress, PostgreSQL, Redis
├── argocd/ AppProject, 루트 앱, ApplicationSet
└── secrets/ SealedSecret (공개 저장소에 안전)
bootstrap/ 스크립트를 순서대로 실행한다.
sudo ./bootstrap/00-preflight.sh # ufw/CNI 규칙 — k3s보다 반드시 먼저
sudo ./bootstrap/01-install-k3s.sh # k3s + embedded containerd 설치·검증
sudo ./bootstrap/sync-tls-secret.sh # TLS Secret 주입
sudo ./bootstrap/02-install-sealed-secrets.sh
sudo ./bootstrap/03-install-argocd.sh
sudo ./bootstrap/04-bootstrap-root-app.sh # 마지막 수동 apply이 서버의 ufw는 routed 정책이 deny다. 그대로 k3s를 설치하면
파드는 Running인데 DNS/통신이 안 되는 상태가 되고 원인 파악이 어렵다.
00-preflight.sh가 CNI 포워딩을 열어준다.
sudo cp bootstrap/k3s/traefik-config.yaml \
/var/lib/rancher/k3s/server/manifests/이 파일은 노드에만 존재하는 드리프트 지점이다. git의 사본이 원본이고, 변경 시 여기를 먼저 고친 뒤 복사한다.
sudo mkdir -p /opt/pinlog
sudo cp -r bootstrap /opt/pinlog/
sudo cp bootstrap/pinlog-tls-sync.{service,timer} /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now pinlog-tls-sync.timer- Sealed Secrets 개인키 백업 →
secrets/README.md참고 - ArgoCD admin 비밀번호 변경 후
argocd-initial-admin-secret삭제 postgres-credentialsSealedSecret 생성
디렉터리 하나 추가가 전부다.
git switch -c feat/S15P11A705-123-add-auth-service
mkdir -p apps/prod/auth-service
cat > apps/prod/auth-service/values.yaml <<'EOF'
image:
repository: ghcr.io/team-pinlog/auth-service
tag: sha-PLACEHOLDER
ingress:
path: /api/auth
service:
targetPort: 8080
EOF
git add apps/prod/auth-service/values.yaml
git commit -m "feat: auth-service 추가"
git push -u origin HEAD
gh pr create --base mainservices-prod ApplicationSet이 새 디렉터리를 감지해 ArgoCD Application을
자동 생성한다. ArgoCD YAML을 직접 쓸 일이 없다.
설정 가능한 값은 charts/microservice/values.yaml에 전부 주석과 함께 있다.
서브도메인을 쓸 수 없다. 와일드카드 인증서 *.p.ssafy.io는 한 레벨만
매칭하므로 i15a705.p.ssafy.io는 되지만 api.i15a705.p.ssafy.io는 안 된다.
게다가 p.ssafy.io DNS는 SSAFY가 관리해서 팀이 레코드를 만들 수도 없다.
따라서 호스트 하나에 경로 기반 라우팅을 쓴다.
| 경로 | 서비스 |
|---|---|
/api/auth |
auth-service |
/api/post |
post-service |
/ |
프론트엔드 |
각 서비스가 자기 경로 prefix를 그대로 소유한다. StripPrefix를 쓰지 않는다 — prefix를 벗기면 생성된 리다이렉트, Swagger UI, OAuth 콜백이 깨진다.
Spring Boot라면:
server:
servlet:
context-path: /api/auth # values.yaml의 ingress.path와 동일하게Backend 또는 Frontend 코드 PR·필수 CI·dev merge
→ 서비스 Actions가 full commit SHA 태그로 private GHCR 이미지 게시
→ Infra service image updater가 현재 dev의 successful run ID·publish digest·GHCR tag digest 검증
→ 서비스별 automation branch에서 해당 values의 tag/digest만 변경
→ Infra PR의 pr-policy + guardrails + 병렬 render nodes + helm 성공
→ trusted service auto-merge가 source SHA·manifest·변경 파일·exact head를 재검증
→ required checks가 모두 성공한 exact head를 즉시 squash merge
→ Argo CD 반영 (Frontend는 application/deployment activation gate가 닫혀 있어 image metadata만 준비)
각 서비스 updater는 Actions graph에서 다음 4개 Job으로 독립 표시된다.
Backend · 1 Detect source → 2 Verify source CI → 3 Verify immutable image → 4 Create Infra PR
Frontend · 1 Detect source → 2 Verify source CI → 3 Verify immutable image → 4 Create Infra PR
AI · 1 Detect source → 2 Verify source CI → 3 Verify immutable image → 4 Create Infra PR
Job output은 source SHA → run ID → digest 순서로 전달하며, Infra values와 PR을
변경할 수 있는 단계는 마지막 Create Infra PR Job뿐이다.
infra/main은 관리자까지 직접 push가 금지되어 있다. 자동화도 고정 기능 브랜치와
PR·필수 checks를 거치며, 같은 image가 이미 반영됐으면 아무 branch나 PR도 만들지
않는다. Backend의 backend-image-update·backend-image-auto-merge와 Frontend의
frontend-image-update·frontend-image-auto-merge가 검증된 updater 경로를
담당한다. 그 외 서비스 image는 각 서비스의 공급망 자동화가 추가되기 전까지
운영자가 기능 브랜치와 PR로 반영한다.
Backend updater의 사전 조건:
- Infra repository secret
PINLOG_IMAGE_UPDATER_TOKEN은 선택한infra·back두 저장소에만 범위를 제한한다. Infra에는 Contents·Pull requests read/write, Backend에는 Contents·Actions read, private GHCR에는 Packages read만 부여한다. - 이 token은 cross-repository source 조회·GHCR pull·PR 이벤트 발생에만 step-local로
사용하고, trusted exact-head merge는 Infra의
GITHUB_TOKEN으로 수행한다. - repository variable
PINLOG_IMAGE_UPDATER_USERNAME에는 PAT 소유자 GitHub username을 넣는다. GitHub App installation token이면x-access-token을 사용한다. - secret, username variable 또는 package read가 없으면 workflow는 fail-closed하며 main과 live cluster를 변경하지 않는다.
Frontend updater는 별도 PINLOG_FRONT_IMAGE_UPDATER_TOKEN과
PINLOG_FRONT_IMAGE_UPDATER_USERNAME을 사용한다. Token은 infra Contents·Pull
requests read/write, front Contents·Actions read, private Front GHCR Packages read로
제한한다. 클러스터 pull용 ghcr-front-pull credential과 공유하지 않는다. 두 값을
준비하고 source·registry 사전 검증을 마친 뒤에만 repository variable
FRONTEND_IMAGE_AUTOMATION_APPROVED=true로 PR 생성 스케줄을 활성화한다. Front
dev branch protection과 provenance 계약을 별도 검증하기 전에는
FRONTEND_IMAGE_AUTO_MERGE_APPROVED를 설정하지 않고 생성된 PR을 수동 병합한다.
브랜치·PR·TDD 증거·Dependabot 정책은 Git/CI 거버넌스를 기준으로 한다.
Backend 태그는 소문자 40자리 full commit SHA와 sha256: digest를 함께 쓴다.
latest 같은 mutable 태그는 금지한다.
Backend GHCR 패키지는 private이다. Pod는 pinlog-prod의 서비스 전용
ghcr-back-pull imagePullSecret으로만 pull한다. Actions의 package access와 클러스터
pull credential은 서로 분리한다.
ArgoCD (인터넷에 노출하지 않는다)
# 로컬에서
ssh -L 8080:localhost:8080 ubuntu@i15a705.p.ssafy.io
# 서버에서
sudo k3s kubectl port-forward svc/argocd-server -n argocd 8080:80
# → http://localhost:8080dev 환경 — Ingress 없음. port-forward 또는 Tailscale로 접근.
kubectl -n pinlog-dev scale deploy/auth-service --replicas=1
kubectl -n pinlog-dev port-forward svc/auth-service 8080:80| 구성요소 | 메모리 |
|---|---|
| OS + SSAFY(Gerrit/Apache) | ~2.2Gi |
| k3s + 시스템 파드 | ~1.3Gi |
| ArgoCD | ~1.0Gi |
| Traefik + Sealed Secrets | ~130Mi |
| PostgreSQL + Redis | ~0.9Gi |
| 모니터링 + Alertmanager | ~1.3Gi |
| 애플리케이션 가용분 | ~8.2Gi |
서비스 기본값은 requests 384Mi / limits 768Mi다.
주의사항
- dev/prod 전체 미러링은 안 들어간다. dev는
replicaCount: 0이 기본이고 작업 중인 것만 올린다. - 이 서버에 self-hosted Actions runner를 돌리지 말 것. Gradle 빌드가 4 vCPU를 다 먹고 파드를 축출한다. public 저장소는 GitHub 러너가 무료다.
- swap이 없어서 메모리 압박은 느려짐이 아니라 즉사다. JVM 서비스는
JAVA_TOOL_OPTIONS의MaxRAMPercentage를 반드시 유지한다.
*.p.ssafy.io 인증서는 수동 DNS-01로 발급되어(/etc/letsencrypt/renewal/p.ssafy.io.conf)
팀이 갱신할 수 없다. 다만 프로젝트가 만료일 전에 종료되므로 실제 영향은 없다.
pinlog-tls-sync.timer가 매일 호스트 인증서를 확인하고 있어서,
SSAFY가 그 전에 갱신하면 24시간 내 클러스터가 자동으로 반영한다.
만약 일정이 밀려 9월 21일을 넘기게 되면 그날 HTTPS가 통째로 죽는다.
그 경우 SSAFY에 80/tcp 개방을 요청하고 cert-manager로 i15a705.p.ssafy.io
단일 인증서를 HTTP-01로 발급받으면 영구 자동 갱신된다 (DNS가 이미 우리 IP를
가리키므로 SSAFY DNS 개입 불필요).
인스턴스 장애 하나 또는 SSAFY 재이미징으로 전부 사라진다.
주 1회 서버 밖 백업 복사는 선택이 아니라 필수다. 담당자를 지정해
스프린트 체크리스트에 넣을 것. 클러스터 안 백업은 DROP TABLE은 막아도
박스를 잃는 건 못 막는다.
분실 시 저장소의 모든 SealedSecret이 영구 복호화 불가. 첫날 백업할 것.
*.p.ssafy.io인증서(2026-09-21 만료) 갱신 주체와 일정, 갱신본이/etc/letsencrypt/live/p.ssafy.io/에 들어가는지- 80/tcp 보안그룹 개방 가능 여부 (HTTPS 리다이렉트 + 자체 ACME 갱신용)
- 이 인스턴스의 재이미징·회수 가능성 (백업 정책이 여기 달림)
- A705 팀에 서버 추가 배정 여부
- (참고) Gerrit 기본 템플릿의
DEVELOPMENT_BECOME_ANY_ACCOUNT설정은 15기 전체 서버 공통 이슈일 가능성이 높음
# 클러스터
k3s kubectl get nodes
k3s kubectl get pods -A
# 저용량 profile은 metrics-server replicas 0이므로 top 대신 host 지표를 본다.
vmstat 1 6
cat /proc/pressure/cpu
# 파드 네트워킹 (ufw 카나리아)
k3s kubectl run t --image=busybox@sha256:b7f3d86d6e84fc17718c48bcde1450807faa2d56704205c697b4bd5df7b9e29f --rm -it --restart=Never -- nslookup kubernetes.default
# TLS — 반드시 서버 "밖"에서. 안에서 하면 보안그룹 문제가 가려진다.
openssl s_client -connect i15a705.p.ssafy.io:443 \
-servername i15a705.p.ssafy.io </dev/null 2>/dev/null \
| openssl x509 -noout -subject -dates
# ArgoCD
k3s kubectl -n argocd get applications # 전부 Synced / Healthy
# 백업 — 테스트하지 않은 백업은 백업이 아니다
k3s kubectl -n pinlog-prod create job --from=cronjob/postgres-backup manual-test
k3s kubectl -n pinlog-prod logs job/manual-test실제 기능이 생기기 전에 hello-service로 전체 왕복을 뚫는다.
Team-PinLog/hello-service생성 —GET /api/hello가 빌드 SHA 반환- 서비스 기능 브랜치와 PR에서 CI를 통과한 뒤 merge
- Actions 빌드 → GHCR 불변 태그 생성
- infra 기능 브랜치에서 image tag 변경 → PR 필수 checks → merge
- ArgoCD 동기화 → 롤링
- 노트북에서
curl https://i15a705.p.ssafy.io/api/hello→ 새 SHA 확인
현재 서비스 CI는 미구현이므로 이 왕복은 목표 계약이다. 자동화를 구현한 뒤 실제 E2E 증거가 있어야 플랫폼 배포 자동화 완료로 본다.