-
Notifications
You must be signed in to change notification settings - Fork 0
History
인프라 작업을 날짜별로 정리한 기록. 세부 트러블슈팅 로그는 팀 공유 문서(Notion 예정) 참고.
- ERD 공유 (company/user_account/worker/task/document/ticket/access_audit_log)
- OpenStack으로 직접 관리하고 싶다는 요청 → 검토 결과 OpenStack은 소프트웨어일 뿐 서버 자체를 제공하지 않고, 오히려 DevStack 기준 최소 8vCPU/16GB급 서버가 추가로 필요해 소규모 데모엔 과함 → **k3s(경량 쿠버네티스)**로 결정
- Oracle Cloud Always Free(ARM, 완전 무료)로 k3s 2노드 구성 계획, VCN·서브넷·API 키·CLI 세팅 진행
-
client(#99)·server(#31)에 임시 Dockerfile PR 올림
- 도쿄 리전에서
Out of host capacity반복 — 수동+자동 재시도 합산 138회 이상, 5일간 인스턴스 생성 실패 - Oracle이 2026-06-15부로 Always Free ARM 할당량을 4→2 OCPU(12GB)로 공지 없이 축소한 사실 확인
- 춘천 리전으로 우회하는 방안도 검토했으나, 춘천은 애초에 ARM Always Free 제외 대상이라 처음부터 막힌 경로였음을 확인
- 클라우드별 가격 재조사(Oracle/GCP/AWS/Hetzner/DigitalOcean/Vultr/NHN/네이버) → 한 차례 Vultr(서울)으로 결정했다가, AWS 신규계정 $200 크레딧 + 재고 문제 없음을 재확인하고 최종적으로 AWS로 재결정
- KT 에이블스쿨 크레딧 지원 여부 확인 → 없음
- IAM 사용자(
fowoco-admin)·보안그룹·EC2 인스턴스(fowoco-node-1, m7i-flex.large) 생성 → 당일 완료 (Oracle 5일 대비 극적으로 빠름) - k3s 싱글 노드 설치, 로컬 kubectl 연결 확인
-
fowoco/infra저장소에k8s/매니페스트(namespace/postgres/server/ai/client/ingress) 작성 → PR #1 - 작업 범위를 infra 저장소로 한정하기로 원칙 정함 (client/server/ai는 팀원이 매일 커밋하는 활성 저장소라 부수적으로 건드리지 않기로)
- 문서를 저장소 README 대신 GitHub Wiki로 이전 (Home / Architecture / Deployment Guide / Deployment Plan)
- 용량 기준선(서버 1대로 데모 트래픽 충분한지)과 향후 업그레이드 조건·절차를 문서화
- 배포 전 재점검에서 문제 2건 발견:
-
퍼블릭 IP가 고정이 아니었음 → Elastic IP 할당해 즉시 고정 (
3.35.21.43→3.35.105.80), k3s 인증서·kubeconfig·Ingress 매니페스트 갱신 - 보안그룹이 SSH(22)·k3s API(6443)를
0.0.0.0/0으로 전체 공개 — 접속 IP 확정되면 좁힐 예정 (아직 미조치)
-
퍼블릭 IP가 고정이 아니었음 → Elastic IP 할당해 즉시 고정 (
-
client/server/ai에 실제 Dockerfile + GitHub Actions 배포 워크플로우를 PR로 올림 -
KUBE_CONFIG는 조직 레벨 대신 3개 저장소 개별 시크릿으로 등록 (조직 레벨은admin:org라는 더 넓은 권한이 필요해서 최소 권한 원칙에 따라 변경)
- 그동안 3개 저장소
deploy.yml의kubectl set image스텝 실패가|| echo "..."fallback으로 조용히 가려져 있어서, CI는 계속 초록불이었지만 실제로는 한 번도 배포에 성공한 적이 없었음을 확인 (클러스터에kubectl apply -f k8s/가 실제로 실행된 적이 없었음 — Deployment Guide의 최초 배포 순서가 미실행 상태로 남아있었던 것) -
client부터 self-heal 방식으로 전환 —deploy.yml이 매 배포마다 이infra저장소를 체크아웃해서00-namespace.yaml/04-client.yaml/05-ingress.yaml을 자동kubectl apply후set image/rollout status진행하도록 수정 (client#261 → client#262) - 첫 실행에서 bootstrap은 성공했지만 Rollout이 타임아웃 → 진단 로그 스텝 추가 후(client#263 → client#264) 원인이 **readiness probe가 아니라
ImagePullBackOff**였음을 확인 - 근본 원인: GHCR
client이미지 패키지가 기본 private 상태였고 클러스터엔imagePullSecrets가 전혀 없었음. Deployment Plan에 이미 "GHCR 패키지는 Public 전환 권장"이라고 적어뒀던 항목인데 client에 대해 실제로는 실행이 안 되어 있던 상태였음 - 해결: GHCR org 정책(
Settings → Packages)에서 Public 허용 활성화 → 패키지 설정에서client패키지를 Public으로 전환 (private→public 전환은 GitHub API로는 막혀있어서 웹 UI로만 가능) - 재배포 후 검증 완료:
curl http://fowoco.3.35.105.80.nip.io/→ HTTP 200.client는 이제 main에 push하면 실제로 배포까지 확인되는 상태 - https는 아직 000 — nip.io 임시 도메인이라 TLS 인증서 미설정 (별도 사안, 도메인 확정 후 처리 예정)
-
남은 일:
server/ai도 같은 패턴(self-heal bootstrap + GHCR 패키지 private 여부 확인 후 필요시 public 전환) 적용 예정.server는 추가로 Deployment Guide의postgres-secret/server-envSecret이 클러스터에 실제로 생성되어 있는지부터 확인 필요 (미확인 상태)
배포 자체는 살아났지만, PR 검증·리소스 제한·머지 규칙이 전혀 없다는 걸 확인해서 마저 정비:
-
client에.github/workflows/deploy.yml밖에 없었음 — PR을 아무 검증 없이 머지할 수 있는 상태.pull_request트리거로 lint/test/build를 도는ci.yml추가 (client#265 → client#266) - 이 CI를 처음 돌리자마자 pre-existing flaky 테스트를 잡음 —
LinkRequestPage.test.tsx가 클릭 핸들러의 비동기 체인(fetch→navigate)이 끝나기 전에getByText로 동기 단언해서 타이밍에 따라 실패하던 문제.findByText로 교체해서 같은 PR에 반영 -
mainbranch protection이 전혀 없어서 CI가 강제력 없는 상태였음 —verify(CI) 통과를 required status check로 설정 (리뷰 승인은 미요구, force-push/삭제 금지) -
k8s/04-client.yaml의 client Deployment에 resource requests/limits이 없어서 단일 노드에서 server/ai/postgres와 무제한 경쟁 중이었음 — requests 50m cpu/64Mi mem, limits 200m cpu/128Mi mem 추가 (infra #4 → #5), client 재배포로 클러스터에 바로 반영·검증 - 결과:
client는 이제 배포도 되고, PR 검증도 강제되고, 리소스 제한도 걸린 상태.server/ai는 이 세 가지(CI/branch protection/resource limits) 전부 아직 없음
5개 저장소 전부 오늘 활동이 있어서 훑어봄. client는 근로자/업무함 API 연동(#269/#271) 정상 진행 중. server가 예상보다 훨씬 활발했음 — AI 연동(OCR 계약 검증, AI 후보 결정→Task 생성, AiRun SSE, RLS 강화 등 다수 merge)과 별개로, PR #97("데모 배포 환경과 운영 검증 절차를 구성")이 client와 거의 동일한 패턴(Dockerfile, deploy.yml, runbook)을 이미 준비해놓고 명시적으로 "infra 저장소에서 namespace/Secret/PostgreSQL/deployment 최초 배포"를 기다리는 상태였음. 클러스터 확인 결과 실제로 postgres-secret/server-env 둘 다 없고 client만 떠 있었음.
ai의 deploy.yml도 client의 원래 문제(deployment/ai 없으면 그냥 실패)를 그대로 갖고 있어서 client와 동일한 self-heal 패턴 적용: ai#21 → [ai#22](https://github.com/fowoco/ai/pull/22, 팀에서 직접 머지). server는 PR #97이 이미 deploy.yml을 다루고 있어서 충돌 방지 차원에서 코드는 안 건드리고 server#104로 필요한 것(Secret 키 목록 등)만 트래킹.
fowoco-sg(sg-02aef781851a7cde2)의 SSH(22)가 0.0.0.0/0으로 전체 공개된 채 2026-07-29부터 방치돼 있었음(당시 "아직 미조치"로 기록). 실제 SSH 사용자가 아무도 없음을 확인(kubectl이 KUBE_CONFIG로 직접 붙는 구조라 SSH 자체가 불필요)하고 규칙을 완전히 삭제(revoke)함. k3s API(6443)는 그대로 0.0.0.0/0 유지 — GitHub Actions가 매 배포마다 이 포트로 직접 kubectl을 실행하는데 러너 IP 대역이 7,300개 이상이라 allowlist가 불가능함. TLS client-cert 인증이 실질적 방어선.
ai PR #22가 머지되고 실제로 rollout되면서 client와 똑같은 **GHCR private → ImagePullBackOff**가 재현됨. 조직 정책은 이미 켜져있어서 패키지 개별 Public 전환만으로 해결 — 다만 전환 직후 익명 pull 토큰 발급이 짧게(약 1분) 전파 지연을 겪어서 pod를 강제로 재시작시켜 바로 해결.
이 흐름을 보고 "server도 이번에 같이 하자"로 결정, 수동으로 최초 배포 진행:
-
postgres-secret생성,01-postgres.yaml적용 - PostgreSQL role을
fowoco_migration(테이블 소유, Flyway 전용)/fowoco_runtime(DML만,NOSUPERUSER NOBYPASSRLS)으로 분리 생성 —docs/database/postgresql-rls-rollout.md가 이 role 분리를 명시적으로 infra 책임으로 못박아둔 부분을 그대로 따름.ALTER DEFAULT PRIVILEGES로 runtime role이 migration role이 만드는 테이블에 자동으로 DML 권한을 받도록 설정 -
server-envSecret 생성.SPRING_PROFILES_ACTIVE=dev로 띄움(prod는 TLS + 릴리즈된 Workflow Catalog가 필수인데 아직 없음 — server 자체 runbook도 이 단계는dev+DRAFT 카탈로그로 하라고 명시) -
ghcr.io/fowoco/server도 역시 private → GHCR private 문제가 3/3(client/ai/server) 전부에서 재현, Public 전환으로 동일하게 해결. 이제 새 fowoco 패키지는 기본적으로 이 전환이 필요하다고 가정할 것
배포 중 새로 발견한 버그 2개:
-
SERVER_PORT충돌 — k8s가 네임스페이스 내 모든 Service에 대해<SVC이름>_PORT형태 env var를 자동 주입하는데(레거시 Docker links 호환 기능, 기본 켜짐), Service 이름이 하필server라서SERVER_PORT=tcp://<clusterIP>:8080이 주입되고 Spring Boot의server.port프로퍼티랑 이름이 충돌해서 부팅 자체가 실패함.enableServiceLinks: false를 pod spec에 추가하면 이 자동 주입이 꺼짐 — server뿐 아니라 postgres/ai/client도 같은 클래스의 버그를 예방 차원에서 동일 적용 (infra#7, 머지 후 클러스터에도 바로 반영) -
AI_RUNTIME_SERVICE_CREDENTIAL필수 검증 —AI_RUNTIME_ENABLED=true일 때 이 값이 비어있으면 Spring 기동 자체가 실패하도록 server가 자체 검증하고 있었음(.env.example엔 선택 항목처럼 주석 처리돼 있어서 놓치기 쉬움). 랜덤 값 생성해서 채움 —ai는 아직 이 값을 검증하지 않아서 당장은 문제없음
kubectl -n fowoco get pods → client/server/ai/postgres 전부 1/1 Running. /actuator/health·/health 둘 다 200. Flyway가 30개 테이블을 fowoco_migration 소유로 정상 생성 (\dt로 확인). POST /api/v1/auth/signup 스모크 테스트 — 구조화된 400 INVALID_REQUEST 응답(테스트 payload가 틀렸을 뿐 앱은 정상 서빙 중). 외부 도메인 경유도 확인: http://fowoco.3.35.105.80.nip.io/api/v1/auth/signup → 400 (ingress의 /api → server 라우팅이 실제로 동작). client/server/ai 세 앱이 전부 같은 도메인 하나로 붙는 상태가 처음으로 완성됨.
실제 비밀값(DB 비밀번호, JWT 시크릿, AI 서비스 credential)은 k8s Secret에만 존재 — git이나 Wiki 어디에도 평문으로 남기지 않음.
-
server의deploy.yml에 self-heal 패턴 적용 — PR #97이 아직 열려있어서 머지된 뒤에 진행 (지금 건드리면 같은 파일 충돌) -
server의 CI 워크플로우/branch protection/resource limits —client에 했던 것과 동일하게 아직 안 함 -
prod프로필 전환은 TLS + 릴리즈된 Workflow Catalog 준비 후