Test
제2장 환경구성
Notiflex라는 B2B 알림 SaaS플랫폼을 만든다. 고객사 시스템에서 이벤트가 발생하면 이메일, SMS 그리고 웹훅으로 알림을 보내주는 서비스이다. 혼자서 시작하는 스타트업이라 빠르게 첫 버전을 올려야한다.
2.1 GCP 계정 생성과 무료 크레딧 활용전략
GCP 신규계정에게 제공하는 $300 무료 크레딧(90일)을 사용하여 실습한다.
비용절감 팁
미국 리전을 쓰면 더 저렴하고 사용하지 않는 시간에 클러스터를 삭제하면 추가로 절감된다. 실습에서는 Spot VM을 사용하는데 일반 VM 대비 60~70%저렴하며 60~70% 저렴한 대신에 공급사에서 상황에 따라 동의없이 중단시킬 수도 있다.
2.2 클로드 설치
1
2
3
4
5
6
7
# MacOS / Linux 기준
# 네이티브 인스톨러 (권장) - 백그라운드 자동 업데이트 지원
curl -fsSL https://claude.ai/install.sh | bash
# Homebrew 방식 - 자동 업데이트 미지원, brew upgrade claude-code 로 수동 갱신
brew install --cask claude-code
주의: 백그라운드 자동 업데이트는 네이티브 인스톨러만 지원합니다. Homebrew로 설치하면
brew upgrade claude-code를 직접 실행해야 하고, stable 채널 특성상 최신 릴리스보다 1주일가량 늦을 수 있습니다. 버전 관리를 신경 쓰고 싶지 않다면 네이티브 인스톨러를 선택하세요.
1
2
claude --version # 버전이 출력되면 성공
claude doctor # 설치 상태·업데이트 이력 진단 (문제가 생기면 가장 먼저 실행)
프로젝트 디렉터리에서 claude를 실행하면 브라우저 인증 창이 열립니다. 클로드 코드는 유료 플랜(Pro/Max/Team/Enterprise) 또는 Console API 계정이 필요하므로, 회사 계정을 쓴다면 관리자에게 좌석 배정을 요청해야 할 수 있다.
명령에 대해 승인 없이 자동으로 클로드 코드가 진행할 수 있는 옵션
1
claude --dangerously-skip-permissions
⚠️ 경고: 이름에 dangerously가 붙은 데는 이유가 있습니다. 이 옵션을 켜면 파일 수정과 셸 명령 실행이 승인 없이 진행됩니다. 지금 우리 터미널에는 gcloud 자격증명과 kubectl 컨텍스트가 살아 있습니다. 즉 클로드가 실수하면 클러스터의 리소스가 실제로 지워질 수 있다는 뜻입니다. 격리된 환경(컨테이너, 일회용 VM)이거나 모든 변경을 Git으로 되돌릴 수 있는 저장소 안에서만 사용하고, 실습 초반에는 켜지 않는 것을 권합니다. 세션 중
/permissions명령으로 특정 도구만 선별적으로 허용하는 절충안도 있습니다.
2.3 gcloud CLI 설치
공식 문서의 안내에 따라 gcloud CLI를 설치한 뒤, 기본 설정부터 잡아둡니다. 여기서 프로젝트와 리전을 지정해두면 이후 모든 명령이 짧아집니다.
1
2
gcloud config set project notiflex-lab-2026
gcloud config set compute/region asia-northeast3 # 서울 리전
kubectl은 별도로 설치하지 않고 gcloud 컴포넌트로 받습니다.
1
gcloud components install kubectl
- gcloud SDK로 받으면 1)
gcloud components update로 한 번에 관리할 수 있고, 2) GKE 인증에 필요한 gke-gcloud-auth-plugin이 함께 관리되어 나중에 kubectl이 GKE 클러스터에 접속할 때 인증이 자동으로 붙습니다.
다음은 인증입니다. 비슷해 보이는 두 명령의 용도가 다릅니다.
1
2
gcloud auth login # gcloud CLI 명령용 사용자 인증
gcloud auth application-default login # ADC 생성 - 클라이언트 라이브러리, 서드파티 도구용
첫 번째는 gcloud ... 명령을 내 계정 권한으로 실행하기 위한 인증입니다. 두 번째는 ADC(Application Default Credentials)를 만드는데, Terraform이나 각종 SDK, 그리고 클로드 코드가 실행하는 스크립트가 GCP API를 호출할 때 이 자격증명을 사용합니다. “gcloud는 되는데 스크립트에서는 인증 에러가 나요”라는 질문의 답이 대부분 여기에 있습니다.
Artifact Registry 인증 설정
컨테이너 이미지를 push/pull 하려면 Docker가 Artifact Registry에 인증할 수 있어야 합니다.
1
gcloud auth configure-docker asia-northeast3-docker.pkg.dev
이 명령은 Docker 설정에 credHelper를 등록해서, 해당 리전 도메인으로 push할 때 gcloud 자격증명을 자동으로 사용하게 만듭니다. 리전마다 도메인이 다르므로(asia-northeast3-docker.pkg.dev, us-central1-docker.pkg.dev 등) 사용할 리전을 정확히 지정합니다.
2.4 깃허브 저장소 구성
이제 코드가 살 집을 만듭니다. GitOps에서는 이 저장소가 곧 “진실의 원천”이 되므로 처음부터 구조를 잡아둡니다.
1
2
3
4
5
6
7
8
9
10
11
.
├── .gitignore
├── .github
│ └── workflows
│ └── .gitkeep
├── app
│ └── .gitkeep
├── CLAUDE.md
└── k8s
└── smb
└── .gitkeep
app/— Notiflex 애플리케이션 소스와 Dockerfile이 들어갈 자리k8s/— 쿠버네티스 매니페스트. GitOps에서 “클러스터가 되어야 할 상태”가 선언되는 곳.github/workflows/— 이후 장에서 채울 CI 파이프라인 자리CLAUDE.md— 클로드 코드가 세션마다 자동으로 읽는 프로젝트 컨텍스트 파일
CLAUDE.md에는 클로드에게 매번 반복해서 말하기 귀찮은 것들을 적어둡니다. 초기 버전은 이 정도면 충분합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Notiflex 프로젝트
GKE 위에서 GitOps로 운영하는 실습 프로젝트.
## 구조
- app/: Go API 서버 소스와 Dockerfile
- k8s/: 쿠버네티스 매니페스트 (모든 클러스터 변경은 반드시 여기서)
- JOURNEY.md: 실습 진행 기록
## 규칙
- 컨테이너 이미지 태그에 latest 사용 금지. 커밋 SHA 또는 시맨틱 버전 사용
- 매니페스트를 kubectl로 직접 수정하지 말고 반드시 파일 수정 후 apply
- 시크릿, 서비스 계정 키는 절대 커밋하지 않는다
- 리전: asia-northeast3, 프로젝트: notiflex-lab-2026
이 파일 하나가 있고 없고에 따라 자연어 명령의 결과 품질이 눈에 띄게 달라집니다. 클로드가 “우리 팀 규칙”을 아는 상태로 일을 시작하기 때문입니다.
.gitignore에는 최소한 다음을 넣습니다.
1
2
3
4
5
6
7
8
9
10
# 자격증명 - 절대 커밋 금지
*.json
.env
# 빌드 산출물
app/notiflex
*.exe
# 로컬 설정
.DS_Store
2.5 GKE 클러스터 생성
드디어 클러스터입니다. H군은 처음으로 클로드에게 인프라를 맡겨봅니다.
1
2
3
[명령] asia-northeast3 리전에 실습용 GKE Standard 클러스터를 만들어줘.
비용을 아끼고 싶으니 노드는 e2-small 2대로, 이름은 notiflex-cluster로.
실행하기 전에 명령을 먼저 보여줘.
클로드가 제안한 명령은 다음과 같습니다.
1
2
3
4
5
gcloud container clusters create notiflex-cluster \
--region asia-northeast3 \
--num-nodes 1 \
--machine-type e2-small \
--disk-size 32
리전 클러스터에서
--num-nodes 1은 “존(zone)당 1대”를 의미합니다. asia-northeast3에는 존이 여러 개이므로 실제 노드 수는 그보다 많아집니다. 존 하나로 제한하고 싶다면--node-locations asia-northeast3-a를 추가합니다. 이런 미묘한 지점이야말로 클로드의 제안을 그대로 승인하지 말고 한 번 읽어봐야 하는 이유입니다. 노드 수, 머신 타입, 디스크 크기는 전부 비용에 직결됩니다.
Autopilot이 아니라 Standard를 고른 이유: Autopilot은 노드 관리를 GCP에 맡기는 대신 파드 단위로 과금되어 편리하지만, 이 책에서는 노드·데몬셋·시스템 파드가 눈에 보이는 Standard 쪽이 학습에 유리합니다. 비용은 앞서 2.1에서 건 예산 알림이 지켜줍니다.
클러스터가 만들어지면 kubectl 자격증명을 받아오고 접속을 확인합니다.
1
2
gcloud container clusters get-credentials notiflex-cluster --region asia-northeast3
kubectl get nodes
1
2
3
NAME STATUS ROLES AGE VERSION
gke-notiflex-cluster-default-pool-93efe171-m68w Ready <none> 1m v1.35.x-gke.x
gke-notiflex-cluster-default-pool-93efe171-r70c Ready <none> 1m v1.35.x-gke.x
노드가 Ready면 준비 완료입니다.
2.6 Notiflex 앱 빌드와 배포
먼저 GET /health와 GET /id 두 개의 엔드포인트를 가지는 Notiflex 앱을 생성합니다. 단순해 보이지만 각자 역할이 있습니다. /health는 이후 장에서 liveness/readiness probe에 연결될 것이고, /id는 자기가 떠 있는 파드의 이름을 반환해서 로드밸런싱과 롤링 업데이트를 눈으로 확인하는 창이 됩니다.
1) Go API 서버 작성 — app/main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main
import (
"encoding/json"
"log"
"net/http"
"os"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
mux.HandleFunc("GET /id", func(w http.ResponseWriter, r *http.Request) {
hostname, _ := os.Hostname() // 파드 안에서는 파드 이름이 곧 hostname
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": hostname})
})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("notiflex listening on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, mux))
}
1
2
3
cd app
go mod init github.com/<your-id>/notiflex
go run main.go # 로컬에서 먼저 확인: curl localhost:8080/health
2) Dockerfile 작성 — app/Dockerfile
빌드 도구가 들어 있는 이미지와 실행 이미지를 분리하는 멀티스테이지 빌드를 사용합니다. 최종 이미지는 셸조차 없는 distroless라서 작고, 공격 표면도 좁습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# --- 빌드 스테이지 ---
FROM golang:1.24 AS builder
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /notiflex .
# --- 실행 스테이지 ---
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /notiflex /notiflex
EXPOSE 8080
USER nonroot
ENTRYPOINT ["/notiflex"]
3) Cloud Build로 빌드하고 Artifact Registry에 push
로컬 Docker 없이도 GCP가 대신 빌드해줍니다. 먼저 이미지가 들어갈 저장소를 만들고, 소스를 제출합니다.
1
2
3
4
5
6
gcloud artifacts repositories create notiflex \
--repository-format=docker --location=asia-northeast3
cd app
gcloud builds submit \
--tag asia-northeast3-docker.pkg.dev/notiflex-lab-2026/notiflex/notiflex-api:0.1.0
CLAUDE.md의 규칙대로 latest 대신 버전 태그를 붙입니다.
4) 쿠버네티스 매니페스트 작성 — k8s/smb/notiflex.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
apiVersion: v1
kind: Namespace
metadata:
name: notiflex
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: notiflex-api
namespace: notiflex
spec:
replicas: 1
selector:
matchLabels:
app: notiflex-api
template:
metadata:
labels:
app: notiflex-api
spec:
containers:
- name: notiflex-api
image: asia-northeast3-docker.pkg.dev/notiflex-lab-2026/notiflex/notiflex-api:0.1.0
ports:
- containerPort: 8080
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: notiflex-api
namespace: notiflex
spec:
selector:
app: notiflex-api
ports:
- port: 80
targetPort: 8080
5) 배포 후 동작 확인
1
2
kubectl apply -f k8s/smb/notiflex.yaml
kubectl get pods -n notiflex
1
2
NAME READY STATUS RESTARTS AGE
notiflex-api-6888588879-kvz56 1/1 Running 0 30s
전체 네임스페이스를 보면(kubectl get pods -A) kube-system과 GKE 관리 컴포넌트들이 함께 보이지만, 우리가 확인할 것은 notiflex 네임스페이스의 파드가 Running인지 하나입니다. 마지막으로 실제 응답까지 받아봅니다.
1
2
3
4
5
6
kubectl -n notiflex port-forward svc/notiflex-api 8080:80
# 다른 터미널에서
curl localhost:8080/health
# {"status":"ok"}
curl localhost:8080/id
# {"id":"notiflex-api-6888588879-kvz56"}
/id가 파드 이름을 그대로 돌려줍니다. 이후 replicas를 늘리고 이 엔드포인트를 반복 호출하면 요청이 여러 파드로 분산되는 것을 직접 볼 수 있습니다.
2.7 깃허브에 첫 커밋
GitOps에서 변경될 모든 사항은 중앙 소스코드 관리 저장소에서 관리되어야 합니다. 다시 말해 클러스터가 되어야 할 상태는 Git에 선언적으로 존재하고, Git이 유일한 진실의 원천이며, 변경은 커밋을 통해서만 이뤄집니다. 방금 kubectl apply로 직접 배포한 것은 이번이 마지막이고, 다음 장부터는 이 저장소가 그 역할을 대신하게 됩니다.
JOURNEY.md를 만들어 실습 진행 상황을 기록해둡니다.
1
2
3
4
5
6
7
8
# JOURNEY
## 2026-07-04
- GCP 실습 프로젝트(notiflex-lab-2026) 생성, 예산 알림 설정
- 클로드 코드, gcloud CLI 설치 및 인증 구성
- GKE Standard 클러스터(notiflex-cluster) 생성 @ asia-northeast3
- Notiflex API(/health, /id) 작성 → Cloud Build → Artifact Registry → 배포 완료
- 배운 점: 리전 클러스터의 --num-nodes는 존당 개수다. 승인 전에 명령을 읽자.
1
2
3
git add .
git commit -m "chore: initial scaffold - notiflex app, k8s manifests, project docs"
git push origin main
커밋 전에 한 번 더:
git status에 서비스 계정 키(*.json)나 .env가 보인다면 멈추세요. 한 번 push된 시크릿은 히스토리에 남아 삭제가 매우 번거롭습니다..gitignore가 제 역할을 하고 있는지 첫 커밋에서 확인하는 습관을 들입니다.
2.8 마무리: /update-docs 스킬 만들기
지금까지 작업한 내용을 모두 문서로 정리해둡니다. 문서 정리 역시 자연어로 명령하는데, 매번 반복될 작업이므로 클로드 코드의 커스텀 명령으로 등록해둡니다. 이후 매번 재사용이 가능합니다.
1
2
3
.claude/commands/<name>.md : 단일 파일 형태. 단순한 슬래시 명령에 적합
.claude/skills/<name>/SKILL.md : 디렉터리 형태. 보조 파일이나 프론트매터로
클로드 자동 호출 같은 고급 기능을 쓸 때 선택
우리 용도는 단순하므로 단일 파일이면 충분합니다. .claude/commands/update-docs.md를 만듭니다.
1
2
3
4
5
이 세션에서 수행한 작업을 요약해서 다음을 수행해줘.
1. JOURNEY.md 맨 아래에 오늘 날짜 항목으로 한 일과 배운 점을 추가
2. README.md의 "현재 상태" 섹션이 있다면 최신 내용으로 갱신
3. 변경된 파일을 보여주고, 커밋 메시지를 제안 (커밋은 내가 승인한 뒤에)
이후부터는 /update-docs 명령 하나로 위 절차가 자동 실행됩니다.
참고로 skills 형태(SKILL.md)를 선택하면 프론트매터의 name과 description이 클로드가 스킬을 자동으로 호출할지 판단하는 기준이 됩니다. “사용자가 문서 정리를 요청하면”처럼 조건을 적어두면 슬래시 명령 없이도 클로드가 알아서 스킬을 꺼내 씁니다. 단순 반복 명령은 commands, 조건부 자동화가 필요하면 skills로 기억해두면 됩니다.
마지막으로 .claude/ 디렉터리도 커밋합니다. 이 스킬 자체가 Git으로 관리되므로 저장소를 clone한 팀원 누구나 같은 /update-docs 명령을 쓰게 됩니다. 명령어까지 코드처럼 관리한다 — 이것도 작은 GitOps입니다.
1
2
3
git add .claude JOURNEY.md
git commit -m "docs: add /update-docs custom command and journey log"
git push origin main