쿠버네티스, AWS, 관측성. 인프라를 운영하면서 부딪힌 것들을 정리합니다.
Istio 서비스 메시 6주 스터디 — 주차별 실습 기록
Istio를 실무에 도입하기 전에 6주 동안 스터디하며 로컬 클러스터에 직접 띄워 실습했습니다. KinD와 Bookinfo 예제로 트래픽 라우팅·복원력·mTLS·관찰성·컨트롤 플레인까지, 주차별로 확인한 것들을 정리합니다.
Argo CD Degraded — progressDeadlineSeconds 만료
DESIRED=READY=AVAILABLE인데 콘솔은 ProgressDeadlineExceeded로 Degraded를 표시합니다. 과거에 한 번 타임아웃 난 기록이 안 지워져서 그렇습니다. 파드를 안 건드리고 상태만 재평가시킵니다.
Docker COPY와 빌드 컨텍스트 — 디렉토리 중첩
빌드 컨텍스트 루트에서 COPY . 를 하니 소스 디렉토리가 한 겹 더 중첩됐습니다. WORKDIR에는 package.json이 없고, 한 겹 아래에 있었습니다.
EKS aws-node ipamd 행(hang) — 파드 ContainerCreating 멈춤
Karpenter가 띄운 새 노드에서 모든 파드가 IP를 못 받았습니다. aws-node는 2/2 Running인데 ipamd 프로세스가 초기화 직후 멈춰 있었습니다. 재시작도 안 먹어서 노드를 갈았습니다.
Elastic APM Agent vs RUM Agent
APM Agent는 서버 사이드를, RUM Agent는 브라우저 사이드를 봅니다. 백엔드 응답은 빠른데 사용자는 느리다고 할 때, 그 간극이 RUM에만 보입니다.
FluxCD 무한 reconcile — ignoreDifferences
FluxCD는 Git 상태로 되돌리고 HPA는 부하에 맞춰 바꾸고, 둘이 같은 replicas 필드를 두고 계속 싸웁니다. ignoreDifferences로 그 필드만 FluxCD 관리에서 뺍니다.
FluxCD vs ArgoCD — 역할 분리
둘 다 GitOps 도구지만 하나는 UI가 아예 없고 하나는 UI가 전부라, 자동으로 반영돼야 하는 인프라는 FluxCD에 두고 사람이 보고 롤백하는 앱은 ArgoCD에 맡겨 층을 나눕니다.
Git 히스토리의 대용량 파일 제거 — filter-repo
2.3GB JSON을 커밋해버렸습니다. 이미 히스토리에 들어간 파일은 .gitignore로 안 지워집니다. 히스토리를 다시 써야 합니다.
쿠버네티스 오토스케일링 계층 — HPA · KEDA · Karpenter
셋 다 스케일링인데 겹치는 게 아니라 층으로 동작합니다. KEDA가 Pod를 늘리라 하고, 자리가 없으면 Karpenter가 노드를 띄웁니다.
EKS Pod에 IAM 권한 주기 — IRSA vs Pod Identity
둘 다 Pod마다 다른 IAM 권한을 주려는 겁니다. IRSA는 클러스터마다 OIDC를 세팅하고, Pod Identity는 그 과정을 없앴습니다.
EKS 노드 오토스케일링 — Karpenter vs Cluster Autoscaler
Cluster Autoscaler는 미리 만든 Node Group을 늘립니다. Karpenter는 Pod 요구사항을 보고 EC2를 그 자리에서 고릅니다. 이 차이가 비용과 속도로 이어집니다.
Lambda@Edge IAM — 리전 제약
함수는 us-east-1에만 있는데 콘솔이 서울 리전으로도 조회를 던집니다. ListFunctions는 리소스 제한이 안 돼서 Resource '*'가 강제되고, 다 맞아도 안 되면 상위 제약을 봐야 합니다.
LiteLLM CPU 병목 — 단일 워커에 묶인 LLM 게이트웨이
VM에 띄운 LiteLLM이 4코어 서버에서 CPU가 한 코어(약 27.6%)에서 천장을 쳤습니다. LiteLLM은 FastAPI 앱이라 워커 하나가 1코어만 씁니다. VM에선 워커를 늘려 임시로 막고, 이후 EKS로 Helm 마이그레이션해 파드 수평 확장으로 정리했습니다.
LLM 게이트웨이 패턴 — 프록시를 두는 이유
서비스마다 OpenAI·Bedrock을 직접 부르면 키가 흩어지고 비용을 못 추적하고 provider를 못 바꿉니다. 프록시 하나를 두면 이게 다 한 곳으로 모입니다.
nginx Ingress vs ALB Ingress — L7과 L4
nginx Ingress는 클러스터 안에서 라우팅하고, ALB Ingress는 클러스터 밖 AWS 인프라가 합니다. 그리고 Ingress 스펙 자체가 L7 전용이라 NLB(L4)는 Service로 노출합니다.
nginx 변수 proxy_pass와 resolver — 일부 요청만 실패하는 DNS 조회
proxy_pass에 변수를 쓰면 nginx가 매 요청마다 resolver로 DNS를 조회합니다. resolver 목록에 그 존을 모르는 서버를 섞어놨더니 일부 요청만 실패했습니다.
PromQL rate() vs irate() — 평균 증가율과 순간 증가율
둘 다 Counter의 초당 증가율인데, rate는 범위 전체의 평균, irate는 마지막 두 샘플의 순간값입니다. 알람엔 rate, 실시간 대시보드엔 irate.
Redis SETNX 분산 락 — TTL·소유권·원자적 해제
여러 Pod가 같은 스케줄 작업을 중복 실행하지 않게 SETNX로 락을 겁니다. 그런데 TTL 없으면 데드락, 값이 단순하면 남의 락을 풀고, GET·DEL 사이엔 경합이 생깁니다.
rootless Docker — br_netfilter 로드 실패
root 없이 Docker를 돌리려는데 데몬이 br_netfilter 커널 모듈을 못 올려서 죽었습니다. rootless는 bridge 네트워크를 안 쓰는데 기본 설정이 bridge를 요구합니다.
Spot Jenkins 에이전트 — 부팅마다 git pull
Spot 인스턴스는 언제든 새로 뜹니다. 그때마다 AMI 생성 시점의 옛날 설정을 쓰면 안 됩니다. @reboot에 git pull을 걸어 시작 시점에 최신화합니다.
Terraform S3 네이티브 락 — DynamoDB 제거
state 락 하나 때문에 DynamoDB 테이블을 따로 두고 있었습니다. OpenTofu 1.10 / Terraform 1.11부터 S3 conditional write로 락이 되어서 테이블을 지웠습니다.
Vault UI 503 — 서비스 등록 endpoints 비어 있음
vault-active 서비스가 Active 파드를 못 찾고 있었습니다. 파드에 vault-active=true 라벨을 붙이는 건 service_registration 설정인데, 그게 빠져 있었습니다.
Vault KV v1 vs v2 — 경로 차이
v1로 마운트된 경로에 v2로 접근하면 404가 납니다. v2는 데이터와 메타데이터를 분리해서 실제 HTTP 경로에 /data/가 끼어듭니다.
ARC — 쿠버네티스 위의 GitHub Actions 셀프호스트 러너
self-hosted 러너가 개수 고정이라 job이 대기했습니다. ARC로 옮기니 job마다 파드가 뜨고 사라지는데, 통신 방향이 K8s→GitHub 단방향이라 내부망에서도 됩니다.
PVC Pending의 세 원인 — EBS CSI · IRSA · hostPort
EBS CSI 컨트롤러가 자격증명을 못 얻어 볼륨을 못 만들었습니다. IRSA를 붙였더니 이번엔 컨트롤러 자체가 Pending이 됐고, 원인은 hostPort였습니다.
Fluentd OOM — 메모리 버퍼 vs 파일 버퍼
버퍼가 메모리에 무제한으로 쌓이는 구성이었습니다. 파일 버퍼로 바꾸고 상한을 걸었는데, 배포 과정에서 다섯 번 막혔습니다.
IRSA 크로스 계정 인증 — EKS Pod의 다른 계정 Bedrock 접근
ServiceAccount에 붙이는 role-arn과 앱 설정에 쓰는 role은 서로 다른 계정의 다른 역할입니다. 액세스 키를 걷어내면서 이 구분이 필요했습니다.
kube-apiserver 복구 — Calico 순환 의존
kubelet이 API 서버를 localhost로 찾고 있었습니다. 거기서 시작해 Calico, CoreDNS, Ingress, FluxCD가 순서대로 무너진 복구 기록.
terraform-aws-ec2-instance v5→v6 — root_block_device 형식 변경
Provider는 최신인데 모듈 버전이 그대로라 Unsupported argument가 났습니다. 모듈을 올리니 이번엔 root_block_device 형식이 리스트에서 객체로 바뀌어 있었습니다.
eBPF 프로세스 exec 감시 — 실제로 걸린 것들
특정 프로그램이 실행되는 순간을 잡는 도구를 만들었습니다. eBPF가 뭔지부터 정리하고, 프로세스 이름이 15자에서 잘리는 것과 타임스탬프가 1970년으로 찍히는 것까지.
AWS 타겟 그룹 Unhealthy 알람 — EventBridge · Lambda
AWS 로드밸런서(ALB)의 타겟 그룹이 5분 이상 Unhealthy면 Slack으로 알리는 요구사항. CloudWatch Alarm은 타겟 그룹 개수만큼 알람이 생기는 구조라, EventBridge 폴링 + Lambda 하나로 갔습니다.
EKS Thanos 배포 트러블슈팅 — StorageClass·IRSA 연쇄 오류
StorageClass 하나 잘못 잡은 게 StatefulSet 재생성으로 번지고, IRSA Trust Policy에 ServiceAccount 하나 빠진 게 컴포넌트마다 Access Denied로 나왔습니다.
Traefik Gateway API — 포트 불일치
CrashLoopBackOff로 시작해 404, 그다음엔 Gateway가 Accepted로 안 넘어갔습니다. 증상은 셋 다 다른데 막힌 자리는 매번 포트였습니다.
Vault Secrets Operator refreshAfter — HMAC 기반 변경 감지
30초마다 Vault를 읽는데 부하가 문제되지 않았습니다. hmacSecretData가 값이 같으면 Kubernetes Secret을 건드리지 않아서, 진짜 비용이 드는 쪽이 안 돌아갑니다.
파드 Pending — volume node affinity 충돌
0/12 nodes are available이 떴는데 노드는 전부 살아 있는 상황. 원인은 EBS 볼륨이 묶여 있는 가용영역이었습니다.
브라우저 확장 보안 감사 — isTrusted · CSV 인젝션 · URL 스킴
확장이 AWS를 직접 부르다 보니 악성 페이지가 비용을 유발할 수 있었습니다. 사용자 입력 핸들러에 isTrusted 검사를 걸고, 단어장 CSV 인젝션과 출처 링크 스킴까지 훑었습니다.
번역 프롬프트 엔지니어링의 한계 — 모델 천장
작은 모델로 자막을 번역하니 전문용어를 직역하고 관용구를 문자 그대로 옮겼습니다. 프롬프트 규칙으로 상당 부분 잡혔지만, 고정 예시를 넣었더니 그게 그대로 출력에 새어 나왔습니다.
보안 그룹 규칙 부재 증명 — CloudTrail 교차 검증
개발 서버 20대에 Zabbix 에이전트를 일괄 배포하자 3대가 곧바로 host down이 됐습니다. 반사적으로 '오늘 누가 SG를 건드렸나'를 봤지만, 그 규칙은 처음부터 없었습니다. 두 개의 독립된 증거로 '한 번도 없었음'을 증명한 과정.
LLM 자막 번역 — 줄 밀림과 문장 그룹핑
여러 줄을 한 번에 번역시키면 LLM이 줄 경계를 흐려 번역이 한 줄씩 밀립니다. 그리고 한 문장이 자막 세 조각에 걸치면 조각별 번역은 원리적으로 불가능합니다.
Terraform ForceNew — 가용 영역(AZ) 추가에 클러스터 전체 재생성
클라우드 관리형 쿠버네티스에서 가용 영역(AZ)을 하나 늘리려고 네트워크 모듈에 추가했더니, terraform plan이 클러스터와 노드풀을 전부 replace했습니다. 모듈이 모든 서브넷을 클러스터에 넘겨서, 서브넷 목록 변경이 ForceNew를 건드렸습니다.
유튜브 자막 확보 — 플레이어 요청 가로채기
확장에서 유튜브 자막을 직접 부르니 빈 응답이 왔습니다. DOM 스크래핑은 느리고 화면이 움직였고, 결국 플레이어 자신의 요청을 가로채는 방식으로 갔습니다.
LiteTranslate — 브라우저 확장에서 Bedrock 직접 호출
유튜브 이중자막 번역·학습 확장을 만들면서 서버를 하나도 두지 않았습니다. 확장이 브라우저에서 직접 AWS를 부르고, 인증은 Cognito, 서명은 WebCrypto로 SigV4를 손수 짰습니다.
OpenStack 앱 크리덴셜 — 발급 시점 역할 고정
역할을 추가하고 키를 재발급해도 floating IP 생성이 계속 PolicyNotAuthorized였습니다. OpenStack 앱 크리덴셜은 발급 순간의 역할 목록을 키 안에 그대로 저장해서, 나중에 역할을 바꿔도 반영되지 않습니다.
S3 삭제 감사 로그 유실 — append 동시성과 FIFO SQS
S3 라이프사이클로 지워진 파일을 스프레드시트에 기록하는 감사 파이프라인이 32건 중 25건만 남겼습니다. 모든 단계 지표는 32로 깨끗했는데, Lambda 컨테이너 재사용으로 append가 겹쳐 서로 덮었습니다. FIFO SQS에 단일 MessageGroupId로 직렬화해 해결했습니다.
SigV4 canonical URI 이중 인코딩 — Bedrock 403
브라우저에서 SigV4를 직접 구현해 Bedrock을 부르는데 계속 403이 났습니다. 서명 로직은 맞는 것 같은데, canonical URI를 한 번만 인코딩한 게 문제였습니다.
Langfuse LLM 비용 집계 누락 — 워커 이미지에 포함된 모델 단가
새로 나온 모델의 트레이스는 잘 쌓이는데 비용이 $0로 집계됐습니다. Langfuse 워커가 기동 때 이미지에 번들된 기본 단가 JSON으로 DB를 시딩하는데, 배포된 이미지가 그 모델보다 오래돼 단가가 없었습니다.
Ubuntu 24.04 베이스 이미지 — UID 1000 충돌
보안 점검으로 베이스 이미지를 24.04로 올렸더니 useradd가 실패했습니다. ubuntu:24.04엔 UID/GID 1000을 이미 차지한 기본 ubuntu 유저가 있어서, 관습적인 useradd -u 1000이 충돌합니다.
과금 지표 vs 파드 런타임 상태
사용량 로그에 '컨테이너 실행 중'이 찍혀 있어 정상이라 봤는데, 실제 파드는 볼륨을 못 붙여 ContainerCreating에서 멈춰 있었습니다. 그 지표는 요청 시점에 쓰이는 값이지 파드가 떠 있다는 뜻이 아니었습니다.
EKS 서브넷 IP 고갈 — 파드 churn과 WARM_IP_TARGET
부하 테스트로 파드를 빠르게 churn하니 서브넷의 사설 IP가 절대적으로 부족해졌습니다. 삭제된 IP가 바로 안 돌아와서 처음엔 회수 시간을 줄이려 했지만, 진짜 문제는 노드가 안 쓰는 IP를 쥐고 있는 것이었습니다.
S3 CORS — 와일드카드 한 단계와 presigned POST
브라우저 업로드가 CORS로 막혔습니다. S3 CORS의 AllowedOrigins 와일드카드는 한 단계 라벨만 매칭해서 두 단계 서브도메인을 못 잡았고, 규칙이 PUT만 허용해서 presigned POST 업로드를 안 덮었습니다.
Docker 격리 규칙에 가려진 iptables ACCEPT — DOCKER-USER 체인
Docker를 최신으로 올린 뒤 컨테이너 포트가 외부에서 전부 타임아웃이었습니다. 직접 넣던 iptables ACCEPT 규칙이 Docker가 새로 추가한 격리 DROP 규칙에 가려져 있었고, DOCKER-USER 체인으로 옮겨 해결했습니다.
cgroup v1 경로 하드코딩 — v2에서 조용히 죽은 격리 로직
OS를 Ubuntu 22.04로 올린 뒤 컨테이너 생성이 몰릴 때 간헐적으로 실패했습니다. 코드는 그대로였는데, cgroup v1 경로를 하드코딩한 격리 로직이 cgroup v2 환경에서 매번 조용히 no-op이 되고 있었습니다.
Docker 29에서 사라진 inspect 최상위 IPAddress 필드
Docker를 29로 올린 뒤 컨테이너 생성이 매번 롤백됐습니다. inspect 응답의 최상위 NetworkSettings.IPAddress 필드가 29부터 제거됐는데, 구버전 클라이언트는 이걸 에러가 아니라 빈 문자열로 받고 있었습니다.
Postgres initdb 실패 — 볼륨 루트의 lost+found
Postgres 파드가 CrashLoopBackOff에 빠졌습니다. 이미지도 권한도 문제가 아니었고, EBS 볼륨을 데이터 디렉토리에 직접 마운트하면서 생긴 lost+found가 initdb의 빈 디렉토리 검사를 통과 못 시켰습니다.
RWO PVC 교착 — 삭제 순서와 Multi-Attach
야간 백업 Job과 사용자 재시작이 같은 RWO 볼륨을 두고 부딪혀 파드가 몇 시간씩 멈췄습니다. pvc-protection 파이널라이저, Multi-Attach, 그리고 볼륨은 지워졌는데 살아남은 Deployment까지 겹친 문제.
Kafka Schema Registry — 비즈니스 이벤트 계약
Schema Registry는 로그 파이프라인이 아니라 서비스 간 비즈니스 이벤트의 스키마를 지킵니다. 호환성 모드를 걸면 깨질 스키마를 배포 시점에 막아, 런타임 장애를 배포 실패로 바꿉니다.
CronJob JWKS 유실 — 이미지에 없는 도구와 set -e
게이트웨이에서 JWT 검증이 kid not found로 실패했습니다. JWKS를 갱신하는 CronJob이 python3으로 문자열을 가공했는데, kubectl 이미지엔 python3이 없었고 set -e가 없어서 스크립트가 빈 값으로 ConfigMap을 덮었습니다.
cgroup v1 vs v2 — PSI와 메모리 압력
쿠버네티스 requests/limits는 cgroup으로 구현됩니다. v1은 리소스별로 트리가 쪼개져 있고, v2는 하나로 통합되면서 PSI가 생겨 OOM으로 죽기 전에 메모리 압력을 감지할 수 있습니다.
Jenkins GIT_COMMIT — Git 플러그인의 덮어쓰기
빌드 대상 레포의 커밋 해시를 env.GIT_COMMIT에 넣었는데, 배포 매니페스트엔 늘 jenkinsfile 레포의 해시가 찍혔습니다. Git 플러그인이 GIT_COMMIT을 환경 조회 때마다 다시 계산해 주입하기 때문입니다.
package-lock의 resolved가 옛 레지스트리를 가리킴 — npm ci 인증 실패
.npmrc를 새 레지스트리로 바꿨는데도 npm ci가 401로 실패했습니다. package-lock.json의 resolved URL이 옛 GitHub Packages를 그대로 가리키고 있었고, npm ci는 그 lock을 따라가기 때문이었습니다.
호스티드 러너에서 내부 npm 레지스트리 접근 불가 — self-hosted 러너
CI에서 install 단계만 계속 타임아웃이었습니다. 내부망 전용 npm 레지스트리를 GitHub의 호스티드 러너가 외부 IP라 소켓 연결조차 못 했고, 그 Job 을 self-hosted 러너로 옮겨 해결했습니다.
WAF JA4 봇 차단의 한계 — 헤드리스 브라우저
TLS 지문(JA4)으로 봇넷을 자동 차단했는데, 봇이 헤드리스 크롬을 쓰기 시작하니 정상 사용자와 지문이 같아졌습니다. 단일 지문 의존을 버리고 복수 피처 + 이상 탐지로 옮긴 과정.
게이트웨이 JWT 검증 — exp 누락
만료된 토큰으로 호출했는데 401이 아니라 200이 왔습니다. 서명 검증은 정상인데 exp 클레임만 무시하고 있었습니다. 탈취된 토큰이 무기한 유효해지는 상황입니다.
Redis ENOTFOUND — 원래 항상 나던 에러
파드가 뜰 때마다 Redis ENOTFOUND가 로그에 찍혔습니다. 그런데 서비스는 멀쩡했습니다. 삭제된 지 몇 달 된 ElastiCache 엔드포인트를 앱이 초기화 때 먼저 찾고 있었습니다.
FailedAttachVolume — 삭제된 EBS 볼륨 복구
잘 쓰던 파드가 갑자기 안 떴습니다. PV가 가리키는 EBS 볼륨이 실제로는 삭제돼 있었고, 볼륨은 InvalidVolume.NotFound였습니다. 새 볼륨을 만들어 PV를 재생성해 복구했습니다.
Keycloak Authorization Code Flow — 왜 2단계인가
로그인하면 토큰을 바로 주지 않고, 짧은 code를 먼저 주고 서버가 그 code를 토큰으로 교환합니다. 토큰을 URL에 실으면 히스토리·로그·리퍼러로 새기 때문입니다.
Ubuntu 24.04 rootless Docker — nsenter와 netns
컨테이너로 작업을 돌리는 서버를 Ubuntu 24.04로 올린 뒤 컨테이너 접속이 깨졌습니다. apparmor의 userns 제한과 rootless docker의 detach-netns가 겹쳐서, nsenter가 엉뚱한 네임스페이스로 들어가고 있었습니다.
Prometheus 설정 리로드 실패 — 단일 파일 bind mount와 inode
prometheus.yml을 고치고 rsync로 올린 뒤 /-/reload를 불렀더니 200에 'reloaded successfully'까지 찍혔는데, 실제로는 옛날 설정이 그대로였습니다. 단일 파일 bind mount가 inode를 고정하고, rsync는 새 inode를 만들기 때문입니다.
WebSocket vs HTTP — 소켓으로 몰면 잃는 것
실시간이라고 전부 WebSocket으로 옮기면 상태를 갖게 되어 수평 확장이 막히고, CDN·캐시·경로 라우팅 같은 HTTP 인프라를 통째로 잃습니다. 서버가 먼저 말해야 할 때만 소켓을 씁니다.
L4 vs L7 로드밸런서 — 패킷을 어디까지 여는가
L4와 L7은 역할이 아니라 '결정을 내리려고 패킷을 얼마나 열어보느냐'의 차이입니다. 그 하나로 TLS passthrough 가능 여부, 클라이언트 IP가 어떻게 전달되는지가 전부 갈립니다.
Node.js 이벤트 루프 — 큐 우선순위
Node.js가 콜백을 처리하는 순서에는 우선순위가 있습니다. nextTick과 Promise가 setTimeout·setImmediate보다 먼저 비워지고, 이 큐는 OS가 아니라 libuv가 프로세스 안에서 관리합니다.
서버리스 실시간 매치메이킹 — Cloudflare Functions · Supabase
실시간 대결 게임인데 상시 서버를 두지 않았습니다. 2초마다 도는 함수가 대기열을 훑어 실력이 비슷한 사람을 붙이고, 대기가 길어지면 매칭 범위를 넓힙니다.
Glicko-2 레이팅 직접 구현
추론 대결 게임을 만들면서 순위 시스템이 필요했습니다. Elo 대신 Glicko-2를 골랐습니다. 실력뿐 아니라 '그 실력을 얼마나 믿을 수 있는지'까지 다루기 때문입니다.
useEffect cleanup과 타이머 — 의존성 배열이 지우는 예약
별이 순서대로 켜져야 하는데 안 켜졌습니다. 애니메이션 useEffect의 cleanup에서 타이머까지 지우고 있었고, 그 effect가 phase마다 다시 돌면서 타이머를 매번 죽였습니다.
SFTP vs NFS — 실시간 I/O엔 왜 NFS인가
SFTP는 SSH 위의 파일 전송 서브시스템이라 요청-응답마다 왕복이 생기고, NFS는 커널 파일시스템이라 앱이 로컬 디스크처럼 씁니다. 배치 전송엔 SFTP, 실시간 I/O엔 NFS인 이유를 정리합니다.
대용량 CSV 스트리밍 — StreamingHttpResponse · select_related
관리 데이터를 CSV로 내보내는 기능인데 데이터가 쌓이니 응답이 느려지고 메모리를 먹었습니다. 응답을 스트리밍으로 바꾸고, ORM의 N+1을 select_related로 잡았습니다.
검색 결과가 없습니다.