13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[카테고리:] cloud

  • OpenStack vs Proxmox — 홈랩 프라이빗 클라우드, 뭘 선택할까

    “홈랩에 프라이빗 클라우드를 올리고 싶은데, Proxmox랑 OpenStack 중 뭘 써야 하나요?” 자주 받는 질문이자 제가 직접 둘 다 굴려보며 내린 결론이 있습니다. 스포일러: 목적이 다릅니다. 운영이냐, 학습이냐에 따라 답이 갈립니다.

    1. 둘은 사실 같은 카테고리가 아니다

    많은 분이 Proxmox와 OpenStack을 “경쟁 제품”으로 보는데, 결이 다릅니다.

    • Proxmox VE: 가상화 플랫폼. VM·컨테이너를 손쉽게 돌리는 하이퍼바이저 관리 도구.
    • OpenStack: 클라우드 운영체제. 여러 서버를 묶어 “셀프서비스 클라우드”를 만드는 거대한 프레임워크.

    즉 Proxmox는 “내가 VM을 관리”하고, OpenStack은 “사용자들이 API로 알아서 VM을 뽑아 쓰게” 하는 물건입니다.

    2. 정면 비교

    기준 Proxmox VE OpenStack
    목적 가상화 운영 프라이빗 클라우드(IaaS)
    난이도 낮음(웹UI 즉시) 매우 높음
    최소 자원 미니 PC 한 대 노드 여러 대(무거움)
    멀티테넌시 제한적 핵심 기능
    API·셀프서비스 기본적 강력(클라우드급)
    확장성 중소 규모 수백~수천 노드
    학습·경력 가치 홈랩·중소기업 클라우드 엔지니어

    3. 상황별 선택

    Proxmox를 쓰세요, 만약 —

    • 홈랩·소규모 서버를 실제로 안정적으로 운영하고 싶다
    • VM·컨테이너를 빠르게 올리고 관리하는 게 목적이다
    • 미니 PC 한두 대로 굴린다

    OpenStack을 쓰세요(=공부하세요), 만약 —

    • 클라우드 엔지니어·프라이빗 클라우드 운영을 커리어로 삼는다
    • 멀티테넌시·API 기반 셀프서비스·수평 확장을 배워야 한다
    • 자격증(예: 프라이빗 클라우드 관련)을 준비한다

    4. 내 선택 — 둘 다, 역할을 나눠서

    제 결론은 “택일”이 아니었습니다.

    • 운영은 Proxmox: 홈랩의 실제 서비스(NAS·사진·블로그·홈오토메이션)는 전부 Proxmox 위에서 안정적으로 돌립니다.
    • 학습은 OpenStack: 그 Proxmox 위에 OpenStack 랩을 올려, 프라이빗 클라우드는 공부용으로 다룹니다.

    재미있는 건 Proxmox가 OpenStack 학습의 토대가 된다는 점입니다. 중첩 가상화를 켜면 Proxmox VM 안에서 OpenStack 컴퓨트 노드를 돌릴 수 있으니까요.

    5. 정리

    “운영하려면 Proxmox, 클라우드를 배우려면 OpenStack.” 홈랩의 실서비스는 Proxmox로 가볍고 안정적으로, 클라우드 엔지니어링 역량은 OpenStack으로 깊게 — 이 조합이 개인에게 가장 합리적이라고 생각합니다. 굳이 하나만 고를 필요가 없습니다.

  • [Cloud] Terraform State 파일 오류 디버깅: Lock·Drift 해결 전략

    [Cloud] Terraform State 파일 오류 디버깅: Lock·Drift 해결 전략

    Terraform State 파일 오류 디버깅: Lock·Drift 해결 전략

    Terraform State 파일 오류는 대개 Terraform이 기억하는 세계와 실제 클라우드에 존재하는 세계가 어긋날 때 시작됩니다. 에러 메시지는 lock, drift, import, backend처럼 흩어져 나오지만, 현장에서 보면 원인은 보통 세 가지입니다. 같은 state를 동시에 만졌거나, 콘솔·스크립트·다른 파이프라인이 리소스를 바꿨거나, 내가 생각한 backend가 아닌 다른 state를 보고 있는 경우입니다.

    저도 홈랩의 Proxmox VM과 AWS 테스트 계정을 Terraform으로 같이 관리하다가 state가 꼬인 적이 있습니다. plan은 말이 안 되는 삭제를 보여주고, apply는 lock에 막히고, 이미 콘솔에서 지운 인스턴스를 Terraform은 아직 관리 중이라고 믿고 있더라고요. 그때 배운 건 단순합니다. Terraform 디버깅은 명령어를 많이 아는 게임이 아니라, state, configuration, real infrastructure 중 어느 쪽이 틀렸는지 분리하는 작업입니다.

    이 글은 공식 문서 요약보다 운영 중 손을 멈춰야 할 지점, 바로 실행해도 되는 명령, 마지막까지 미뤄야 할 명령을 구분하는 실전 기준에 가깝습니다. 특히 state rm, import, state mv, force-unlock은 모두 강력하지만 잘못 쓰면 문제를 감추거나 더 키울 수 있습니다. 내부 위키에 IaC 배포 절차나 장애 복구 문서가 있다면 이 글과 함께 연결해 두면 리뷰 때 진짜 편합니다.

    Terraform State 파일 오류 이해를 위한 state 관리 흐름 다이어그램

    Terraform CLI, 원격 백엔드, 실제 클라우드 리소스, state lock이 어떻게 연결되는지 보여주는 개요 이미지입니다.

    Terraform State 파일 오류는 “지도”보다 “소유권 장부” 문제입니다

    Terraform state를 단순히 현재 인프라의 지도라고만 설명하면 절반만 맞습니다. 실무에서는 state를 Terraform이 어떤 실제 객체를 어느 리소스 주소로 관리하는지 기록한 소유권 장부로 보는 편이 더 정확합니다. 예를 들어 aws_instance.web이라는 주소가 실제 AWS 인스턴스 ID i-...에 묶여 있다면, Terraform은 그 주소를 기준으로 변경·삭제·재생성 판단을 합니다.

    문제는 이 장부가 코드와 자동으로 완벽히 동기화되지 않는다는 점입니다. Terraform은 .tf 파일, state, provider가 API로 읽어온 실제 리소스 정보를 비교해서 plan을 만듭니다. 그래서 “코드는 맞는데 plan이 이상하다”는 말은 충분히 가능합니다. 코드가 틀린 게 아니라 state가 다른 환경을 가리키거나, 실제 리소스가 콘솔에서 바뀌었거나, provider가 읽어온 값이 이전 실행과 달라졌을 수 있거든요.

    • Configuration: 우리가 원하는 선언입니다. .tf 파일, 변수, module 호출이 여기에 해당합니다.
    • State: Terraform이 이미 관리한다고 믿는 객체 목록과 속성입니다.
    • Real infrastructure: AWS, Azure, GCP, Kubernetes, vSphere 등에 실제로 존재하는 리소스입니다.
    • Provider: 실제 API를 조회하고 생성·수정·삭제를 수행하는 계층입니다. provider 버전 차이도 plan 결과를 바꿀 수 있습니다.

    제가 운영 환경에서 먼저 확인하는 건 코드가 아니라 내가 지금 올바른 backend와 workspace를 보고 있는지입니다. prod 작업이라고 믿고 있었는데 staging state를 보고 있으면, 이후의 모든 분석은 정교한 착각이 됩니다.

    흔한 Terraform State 파일 오류 유형과 근본 원인

    증상만 보고 바로 명령어를 치면 위험합니다. 같은 plan 이상이라도 원인이 drift인지, backend 오지정인지, 리팩터링 후 주소 변경인지에 따라 복구 방법이 달라집니다.

    오류 유형 대표 증상 근본 원인 먼저 볼 것 권장 대응
    State Lock Error acquiring the state lock 다른 Terraform 실행이 state 쓰기 권한을 잡았거나 이전 실행이 비정상 종료됨 CI 실행 상태, lock ID, 실행자 정보, backend lock 저장소 실행 중 작업을 확인한 뒤 고아 lock일 때만 force-unlock
    Drift 예상하지 못한 ~, -, -/+가 plan에 표시됨 콘솔 수동 변경, 외부 자동화, provider 기본값 변화 terraform plan -refresh-only, 클라우드 변경 이력 코드로 흡수할지, 실제 값을 되돌릴지 결정
    State와 실제 리소스 불일치 삭제된 리소스를 계속 추적하거나 기존 리소스를 새로 만들려 함 수동 삭제, import 누락, state rm 오남용, workspace 혼동 terraform state list, terraform state show, 실제 리소스 ID state rm, import, apply 중 선택
    Backend 설정 오류 init 실패, 전혀 다른 리소스가 plan에 표시됨 S3 key, workspace, profile, region, backend-config 불일치 .terraform/terraform.tfstate의 backend 메타정보, terraform workspace show terraform init -reconfigure 후 plan 재검증
    리팩터링 후 주소 변경 동일 리소스를 삭제 후 새로 만들려 함 resource 이름, module 경로, for_each key 변경 이전 주소와 새 주소, plan의 -/+ 여부 moved block 또는 terraform state mv

    Terraform 디버깅 루틴: apply 전에 보는 7개 체크포인트

    문제가 터졌을 때 저는 apply를 바로 다시 실행하지 않습니다. 첫 번째 실행이 실패한 이유를 모른 채 두 번째 실행을 하면 state가 더 헷갈려집니다. 아래 루틴은 로컬 backend, S3 backend, HCP Terraform/Terraform Enterprise 모두에서 큰 틀은 같습니다.

    1. Terraform 버전과 provider lock 파일을 확인합니다.
    2. 현재 workspace가 의도한 환경인지 확인합니다.
    3. backend가 어느 state 객체를 보고 있는지 확인합니다.
    4. state에 들어 있는 리소스 주소를 목록화합니다.
    5. 실제 리소스 조회와 refresh-only plan으로 drift를 분리합니다.
    6. 삭제 또는 재생성 액션이 있으면 영향 큰 리소스부터 읽습니다.
    7. state 변경 명령은 백업 또는 원격 backend 버전 복구 가능성을 확인한 뒤 실행합니다.
    # 0. 버전과 provider 잠금 파일 확인
    terraform version
    ls -la .terraform.lock.hcl
    
    # 1. 내가 보고 있는 workspace 확인
    terraform workspace show
    terraform workspace list
    
    # 2. backend 재초기화가 필요한 상황인지 확인
    terraform init -reconfigure
    
    # 3. state에 등록된 주소 확인
    terraform state list
    terraform state list 'module.network.*'
    
    # 4. 특정 리소스가 어떤 실제 ID에 연결되어 있는지 확인
    terraform state show aws_instance.web
    
    # 5. 실제 인프라를 읽어 state와의 차이만 확인
    terraform plan -refresh-only
    
    # 6. 일반 plan은 파일로 저장해서 리뷰 가능하게 남김
    terraform plan -out=tfplan
    terraform show tfplan

    terraform plan -refresh-only는 인프라를 바꾸겠다는 뜻이 아니라 실제 객체를 읽어서 state가 어떻게 갱신될지를 보여주는 모드입니다. 다만 terraform apply -refresh-only까지 실행하면 state가 실제 값에 맞게 업데이트될 수 있습니다. 운영에서는 plan과 apply의 차이를 분명히 나눠야 합니다. 조회만 하려면 plan -refresh-only에서 멈추고, state 동기화까지 의도한 경우에만 apply 단계로 갑니다.

    반대로 terraform plan -refresh=false는 실제 리소스 조회를 건너뜁니다. 빠를 수는 있지만 오래된 state를 기준으로 plan을 만들기 때문에 drift 디버깅에는 부적합합니다. 저는 대규모 환경에서 provider API 호출 비용이나 속도가 부담될 때 임시 비교용으로만 쓰고, 운영 적용 판단에는 거의 쓰지 않습니다.

    Terraform 디버깅 명령어로 state 상태를 점검하는 화면

    터미널에서 workspace, state list, refresh-only plan을 순서대로 확인하는 장면을 표현한 이미지입니다.

    IaC 상태 관리의 핵심: 원격 백엔드와 S3 key

    팀 작업에서는 로컬 state보다 원격 backend가 기본값에 가깝습니다. 로컬 파일은 간단하지만 동시 실행 제어, 접근 제어, 백업, 감사 추적이 약합니다. AWS 환경이라면 S3 backend를 많이 씁니다. 현재 Terraform S3 backend는 use_lockfile = true로 S3 기반 state locking을 사용할 수 있고, DynamoDB 기반 locking은 deprecated 상태라 신규 구성에서는 S3 lockfile을 우선 검토하는 편이 좋습니다.

    선택지 언제 적합한가 장점 주의점
    Local state 개인 실험, 폐기 가능한 샌드박스 구성이 단순하고 빠름 협업, 백업, lock, 접근 제어에 취약
    S3 backend + use_lockfile AWS에서 신규 원격 state를 단순하게 운영할 때 DynamoDB 테이블 없이 lock 구성 가능 Terraform 버전과 S3 lock 파일 권한을 함께 확인해야 함
    S3 backend + DynamoDB lock 기존 조직 표준이 이미 DynamoDB lock으로 굳어졌을 때 운영 사례가 많고 기존 자동화와 연동 쉬움 deprecated 방식이므로 마이그레이션 계획이 필요함
    HCP Terraform/Terraform Enterprise 정책, 승인, 실행 이력, 팀 권한을 한곳에서 관리할 때 협업 기능과 실행 관리가 강함 조직 계정·권한·네트워크 정책 설계가 필요
    terraform {
      backend "s3" {
        bucket       = "my-terraform-state-bucket"
        key          = "prod/network/terraform.tfstate"
        region       = "ap-northeast-2"
        encrypt      = true
        use_lockfile = true
      }
    }

    여기서 가장 자주 터지는 건 bucket보다 key입니다. 같은 bucket 안에 dev/network/terraform.tfstate, staging/network/terraform.tfstate, prod/network/terraform.tfstate를 넣는 구조는 흔합니다. 그래서 key 경로를 한 글자만 잘못 넣어도 전혀 다른 환경의 장부를 펼쳐놓고 작업하게 됩니다. 제 기준으로 prod backend는 코드 리뷰에서 bucket보다 key를 더 오래 봅니다.

    terraform init -reconfigure \
      -backend-config="bucket=my-terraform-state-bucket" \
      -backend-config="key=prod/network/terraform.tfstate" \
      -backend-config="region=ap-northeast-2" \
      -backend-config="encrypt=true" \
      -backend-config="use_lockfile=true"
    
    terraform init -reconfigure \
      -backend-config="bucket=my-terraform-state-bucket" \
      -backend-config="key=prod/network/terraform.tfstate" \
      -backend-config="region=ap-northeast-2" \
      -backend-config="dynamodb_table=terraform-locks" \
      -backend-config="encrypt=true"

    init -reconfigure는 현재 디렉터리의 backend 설정을 다시 읽게 합니다. state를 다른 위치로 옮기는 migration과는 다릅니다. Terraform이 state migration 여부를 묻는 상황에서는 메시지를 끝까지 읽어야 합니다. 운영 state를 잘못 옮기면 plan 이상보다 복구가 더 피곤해집니다.

    State Lock 해결: force-unlock은 마지막에 씁니다

    State Lock 해결을 검색하면 대부분 terraform force-unlock LOCK_ID가 먼저 보입니다. 하지만 이 명령은 lock을 푸는 도구이지, 현재 실행 중인 apply가 안전하게 끝났다는 증거가 아닙니다. 누군가 실제로 apply 중인데 강제로 풀면 두 개의 실행이 같은 state를 쓰려고 할 수 있습니다.

    ps aux | grep '[t]erraform'
    terraform workspace show
    terraform force-unlock LOCK_ID
    terraform force-unlock -force LOCK_ID

    제가 force-unlock을 허용하는 기준은 꽤 보수적입니다. 첫째, 현재 로컬·CI/CD·동료 작업 중 실행 중인 plan 또는 apply가 없어야 합니다. 둘째, lock 에러의 실행자·시간·operation 정보가 현재 살아 있는 작업과 맞지 않아야 합니다. 셋째, unlock 후 바로 apply하지 않고 plan부터 다시 봐야 합니다.

    상황 해야 할 일 하지 말아야 할 일
    CI job이 아직 실행 중 job 종료 또는 취소 결과를 기다림 lock ID가 보인다는 이유만으로 force-unlock
    노트북이 꺼져 apply가 중단됨 클라우드 리소스 생성 상태와 state 반영 여부를 확인 unlock 후 바로 apply 재시도
    오래된 고아 lock으로 확인됨 terraform force-unlock LOCK_ID 후 plan 검증 backend 저장소를 직접 수정하는 것으로 시작
    lock 저장소 권한 오류 S3 또는 DynamoDB 권한, profile, region을 확인 lock 기능을 꺼서 우회

    S3 lockfile을 쓴다면 lock 파일에 대한 s3:GetObject, s3:PutObject, s3:DeleteObject 권한을 확인해야 합니다. DynamoDB lock을 쓰는 기존 구성이라면 lock 테이블 권한과 region을 봐야 합니다. lock 문제를 Terraform 문제가 아니라 IAM 문제로 풀어야 하는 경우도 꽤 많습니다.

    State 불일치 복구: rm, import, mv를 섞어 쓰지 마세요

    Terraform State 파일 오류 중 가장 헷갈리는 구간입니다. state rm, import, state mv는 모두 state를 만지지만 목적이 다릅니다. 저는 이 셋을 “추적 끊기, 추적 시작하기, 주소 바꾸기”로 외웁니다. 이 구분만 잡아도 사고 확률이 확 줄더라고요.

    상황 사용 명령 의미 사용하면 안 되는 경우
    실제 리소스는 삭제됐는데 state에만 남음 terraform state rm Terraform의 추적만 제거 리소스를 실제로 삭제하려는 목적이면 부적합
    실제 리소스가 있는데 Terraform이 모름 terraform import 기존 객체를 state 주소에 연결 코드가 없거나 주소가 틀린 상태에서 성급히 실행
    리소스 이름·module 경로만 바뀜 terraform state mv 또는 moved block 같은 실제 객체를 새 Terraform 주소로 이동 실제 객체를 교체해야 하는 변경에는 부적합
    설정에서 더 이상 관리하지 않음 removed block 또는 state rm Terraform 관리 대상에서 제외 팀이 변경 이력을 코드로 남겨야 하는데 임시 명령만 실행
    terraform state list
    terraform state show aws_instance.web
    terraform state rm aws_instance.web
    terraform import aws_instance.web i-0123456789abcdef0
    terraform state mv aws_instance.web module.compute.aws_instance.web

    state rm은 실제 리소스를 삭제하지 않습니다. 이 사실은 단순하지만 사고를 많이 막아줍니다. 반대로 terraform destroy는 실제 리소스를 삭제할 수 있습니다. “state에서 빼고 싶다”와 “클라우드에서 없애고 싶다”는 완전히 다른 요구입니다.

    리팩터링이라면 가능하면 코드에 moved block을 남기는 방식을 선호합니다. 이유는 간단합니다. terraform state mv는 실행한 사람의 터미널 기록에만 맥락이 남지만, moved block은 저장소에 의도가 남습니다.

    moved {
      from = aws_instance.web
      to   = module.compute.aws_instance.web
    }
    Terraform State 파일 오류 복구를 위한 rm import mv 흐름도

    state에서 제거, 가져오기, 주소 이동이 각각 어떤 상황에 맞는지 정리한 흐름도 이미지입니다.

    재현 시나리오: 콘솔에서 삭제된 EC2를 다시 만들려는 경우

    가장 흔하고 교육용으로도 좋은 시나리오입니다. Terraform으로 EC2를 만들었는데 누군가 AWS 콘솔에서 직접 삭제했다고 가정하겠습니다. 이후 terraform plan을 실행하면 Terraform은 state에 남아 있는 인스턴스 ID를 기준으로 실제 객체를 조회합니다. provider는 “없다”고 답하고, Terraform은 설정 파일에 리소스가 여전히 있으니 다시 만들 계획을 세웁니다.

    1. Terraform으로 aws_instance.web를 생성합니다.
    2. AWS 콘솔 또는 외부 스크립트로 해당 인스턴스를 삭제합니다.
    3. terraform plan -refresh-only로 state와 현실의 차이를 봅니다.
    4. 그 인스턴스가 계속 필요한지, 이미 폐기한 리소스인지 결정합니다.
    5. 필요한 리소스면 terraform apply로 재생성을 검토하고, 폐기한 리소스면 코드와 state를 함께 정리합니다.
    terraform state show aws_instance.web
    terraform plan -refresh-only
    terraform plan -out=tfplan
    terraform show tfplan
    terraform apply tfplan
    terraform state rm aws_instance.web
    terraform plan

    여기서 자주 하는 실수는 state rm을 먼저 실행하는 겁니다. 설정 파일에 리소스 블록이 그대로 남아 있으면 다음 plan에서 Terraform은 “state에는 없지만 코드에는 있으니 새로 만들자”고 판단합니다. 그래서 정말 폐기하려면 코드 제거와 state 정리가 같이 가야 합니다. 필요한 리소스라면 반대로 state를 지우지 말고 Terraform이 재생성하도록 plan을 검토하는 편이 자연스럽습니다.

    plan 출력 해석: 기호보다 리소스 종류가 더 중요합니다

    Terraform plan의 기호는 기본 문법입니다. 하지만 운영 판단은 기호만으로 하지 않습니다. 같은 ~라도 태그 변경과 데이터베이스 엔진 옵션 변경은 무게가 다릅니다. 같은 -/+라도 무상태 인스턴스와 영구 디스크는 위험도가 다릅니다.

    • +: 새 리소스 생성입니다. 비용과 quota를 확인합니다.
    • ~: 기존 리소스 변경입니다. in-place 변경인지, provider가 어떤 필드를 바꾸는지 봅니다.
    • -: 리소스 삭제입니다. 운영에서는 승인 없이 진행하지 않습니다.
    • -/+: 삭제 후 재생성입니다. 네트워크, DB, 디스크, IAM, Kubernetes namespace에 보이면 멈춥니다.
    terraform plan -out=tfplan
    terraform show tfplan
    terraform show -json tfplan > tfplan.json
    jq '.resource_changes[] | select(.change.actions | index("delete")) | {address, actions: .change.actions}' tfplan.json

    저는 plan 리뷰 때 리소스를 세 그룹으로 나눕니다. 첫째, 재생성되면 장애가 될 수 있는 리소스입니다. VPC, subnet, route table, DB, disk, IAM role, cluster 같은 것들입니다. 둘째, 비용이 바로 붙는 리소스입니다. 인스턴스, NAT gateway, load balancer, managed database가 여기에 들어갑니다. 셋째, 변경되어도 영향이 제한적인 메타데이터입니다. 태그나 설명이 대표적입니다. 이 분류를 해두면 plan 리뷰가 감상이 아니라 판단이 됩니다.

    Terraform plan 결과로 State Lock 해결과 변경 위험을 해석하는 대시보드

    생성, 변경, 삭제, 재생성 항목을 색상별로 구분해 보여주는 Terraform plan 결과 해석 이미지입니다.

    성능·비용·안정성에 영향을 주는 결정 포인트

    State 디버깅은 단순 복구 작업처럼 보이지만, backend 설계와 운영 습관은 비용과 안정성에 영향을 줍니다. 숫자를 지어낼 필요는 없습니다. 어떤 결정이 어떤 방향의 비용을 만드는지만 알아도 충분히 실수를 줄일 수 있습니다.

    결정 안정성 영향 비용·성능 영향 추천 기준
    state를 환경별로 분리할지 여부 장애 범위를 줄임 state 수가 늘어 관리 포인트 증가 prod, staging, dev는 최소한 key 또는 workspace로 분리
    하나의 거대한 state 사용 한 번의 plan 영향 범위가 커짐 provider 조회가 많아져 plan이 느려질 수 있음 네트워크, 플랫폼, 앱 계층을 무리 없이 나눔
    -refresh=false 사용 drift를 놓칠 수 있음 조회가 줄어 빨라질 수 있음 운영 apply 판단에는 사용하지 않음
    state lock 비활성화 동시 apply 충돌 위험 증가 구성은 단순해짐 팀 환경에서는 lock 없는 운영을 피함
    원격 state 암호화·버전 관리 복구와 보안에 유리 스토리지 정책 관리 필요 민감 값 가능성을 전제로 암호화와 접근 제어 적용

    특히 state를 너무 크게 키우는 패턴은 나중에 발목을 잡습니다. 모든 리소스를 하나의 root module과 하나의 state에 넣으면 처음엔 편합니다. 하지만 plan이 느려지고, 작은 변경도 큰 영향 범위를 갖고, lock 대기 시간이 길어집니다. 그렇다고 너무 잘게 쪼개면 remote state 참조와 의존성 관리가 늘어납니다. 저는 “같이 생성되고 같이 롤백되어도 괜찮은 단위”를 state 분리 기준으로 잡습니다.

    자주 묻는 질문: Terraform State 파일 오류 FAQ

    Q1. terraform.tfstate 파일을 직접 수정해도 되나요?

    가능은 하지만 운영에서는 거의 마지막 수단입니다. JSON이라 열어볼 수는 있어도 Terraform 내부 구조, provider schema, 민감 값 처리, serial 값이 얽혀 있습니다. 먼저 terraform state 하위 명령을 쓰고, 원격 backend라면 버전 복구 가능성을 확인한 뒤 진행하세요.

    Q2. state 파일을 Git에 올려도 되나요?

    일반적으로 올리지 않는 편이 맞습니다. state에는 리소스 속성뿐 아니라 민감한 값이 평문에 가까운 형태로 남을 수 있습니다. 팀 환경에서는 원격 backend, 암호화, 접근 제어, 감사 가능한 실행 경로를 쓰는 쪽이 안전합니다.

    Q3. lock 오류가 나면 무조건 force-unlock 하면 되나요?

    아닙니다. lock은 귀찮은 장벽이 아니라 state 동시 수정을 막는 안전장치입니다. 실행 중인 작업이 없고 고아 lock이라고 판단될 때만 terraform force-unlock LOCK_ID를 사용하세요.

    Q4. import만 하면 코드도 자동으로 완성되나요?

    terraform import는 기존 리소스를 state에 연결하는 명령입니다. 코드 작성과 속성 정리는 별도 작업으로 보는 편이 안전합니다. import 후에는 반드시 terraform plan으로 코드와 실제 리소스 차이를 맞춰야 합니다.

    Q5. 리소스 이름만 바꿨는데 왜 삭제 후 생성이 뜨나요?

    Terraform 주소가 바뀌었기 때문입니다. aws_instance.web를 aws_instance.app으로 바꾸면 Terraform은 기본적으로 다른 리소스로 봅니다. 같은 실제 객체를 유지하려면 moved block이나 terraform state mv로 주소 이동을 알려줘야 합니다.

    운영에서 바로 쓰는 판단 기준

    Terraform State 파일 오류를 만나면 명령어보다 순서가 중요합니다. 먼저 workspace와 backend를 확인합니다. 그다음 state list, state show, plan -refresh-only로 state와 현실의 차이를 분리합니다. lock 에러라면 실행 중인 작업이 있는지 확인한 뒤 고아 lock일 때만 해제합니다.

    이럴 땐 이렇게 하시면 됩니다. 콘솔에서 지운 리소스를 Terraform이 다시 만들려 한다면, 계속 필요한 리소스인지 먼저 결정하세요. 필요하면 plan 검토 후 apply, 필요 없으면 코드 제거와 state 정리를 같이 합니다. 이름이나 module 경로만 바꿨다면 삭제·재생성을 허용하지 말고 moved block 또는 state mv를 씁니다. lock이 걸렸다면 Terraform이 나를 괴롭히는 게 아니라 state를 보호하는 중이라고 보고, 실행 중인 작업부터 찾습니다.

    제가 운영에서 절대 넘기지 않는 신호는 -와 -/+입니다. 특히 네트워크, 데이터베이스, 디스크, IAM, 클러스터 리소스에 보이면 손을 멈추고 이유를 설명할 수 있어야 합니다. 설명할 수 없는 apply는 복구 계획이 없는 변경과 비슷합니다.

    정확한 옵션과 backend 동작은 HashiCorp의 Terraform CLI 문서, S3 backend 문서, state 명령 문서를 기준으로 확인하는 습관을 권합니다. 문서는 명령 문법을 확인하는 곳이고, 운영 판단은 현재 state와 실제 인프라를 놓고 해야 합니다. 이 둘을 분리해서 보면 Terraform state 문제는 훨씬 덜 무섭습니다.

  • [Cloud] GitLab CI 마이그레이션 회고: 전환 결정 기준

    [Cloud] GitLab CI 마이그레이션 회고: 전환 결정 기준

    GitLab CI 마이그레이션 회고: 전환 결정 기준

    GitLab CI 마이그레이션, YAML 변환보다 먼저 바뀌는 것

    GitLab CI 마이그레이션을 몇 번 해보면 초반 착각이 거의 비슷합니다. 기존 Jenkinsfile이나 사내 CI 스크립트를 .gitlab-ci.yml로 옮기면 끝날 것 같지만, 실제로 흔들리는 지점은 YAML 문법이 아니라 실행 환경, 권한 경계, 캐시 수명, 배포 승인 습관입니다.

    제가 제일 경계하는 실패는 빨간 파이프라인이 아닙니다. 차라리 실패는 빨리 보이거든요. 더 위험한 건 성공처럼 보이는데 산출물이 예전과 다른 경우입니다. 예를 들어 기존 빌드 서버에는 전역 패키지, 로컬 캐시, SSH known_hosts, 사내 CA 인증서가 이미 깔려 있었는데 GitLab Runner의 Docker Executor에서는 전부 사라질 수 있습니다.

    그래서 저는 전환을 시작할 때 ‘CI 도구를 바꾼다’고 보지 않습니다. 빌드 지식을 어디에 둘 것인가, 운영 배포 권한을 누가 어떤 조건에서 행사할 것인가, 실패 로그를 누가 재현 가능한 방식으로 읽을 것인가를 다시 정하는 작업으로 봅니다. 결국 GitLab CI는 도구 선택보다 팀의 배포 습관을 저장소 중심으로 다시 쓸 준비가 되어 있는지의 문제에 가깝더라고요.

    GitLab CI 마이그레이션 전체 흐름 아키텍처 다이어그램

    기존 CI 도구에서 GitLab CI로 넘어갈 때 함께 이동하는 요소를 한눈에 보는 개요 다이어그램입니다.

    GitLab CI 마이그레이션이 바꾸는 운영 경계

    GitLab CI/CD의 핵심은 저장소 안의 .gitlab-ci.yml을 기준으로 Job, Stage, Pipeline을 선언하는 데 있습니다. 그런데 실무에서 중요한 변화는 정의 파일의 위치가 아니라 책임의 위치입니다. 예전에는 빌드 서버 안에 있던 지식이 저장소로 들어오고, 운영팀 개인 계정에 묶여 있던 배포 절차가 Job과 Environment로 드러납니다.

    저는 전환 전에 기존 CI를 다음 네 덩어리로 분해합니다. 이 과정을 생략하면 나중에 ‘왜 Jenkins에서는 됐는데 GitLab에서는 안 되죠?’라는 질문만 반복됩니다.

    • 실행 환경: OS 패키지, 런타임 버전, Docker-in-Docker 사용 여부, 사내 인증서, DNS, 프록시 설정입니다.
    • 상태 저장 지점: 캐시, 아티팩트, 빌드 번호, 릴리스 노트, 컨테이너 이미지 태그가 어디에 남는지입니다.
    • 권한 경계: 배포 토큰, 레지스트리 인증, 클라우드 IAM, SSH 키, protected branch/tag 조건입니다.
    • 사람의 개입: 운영 배포 승인, 장애 시 재실행 기준, 롤백 명령을 누가 실행하는지입니다.

    이 네 가지가 정리되어 있으면 GitLab CI 문법은 금방 따라옵니다. 반대로 이게 흐릿하면 문법을 아무리 예쁘게 써도 파이프라인은 오래 못 갑니다. 관련해서는 이전에 정리한 CI/CD 체크리스트 글과 함께 보면 결정 기준을 더 빨리 잡을 수 있습니다.

    마이그레이션 결정 요소: 저는 이 표를 먼저 채웁니다

    도구 비교표보다 먼저 보는 건 팀의 현재 배포 습관입니다. 같은 GitLab CI라도 저장소가 이미 GitLab에 있는 팀과, Jenkins 플러그인에 배포 지식이 잔뜩 들어 있는 팀의 난이도는 완전히 다릅니다.

    결정 요소 이럴 땐 전환 우선 이럴 땐 보류 또는 Shadow 운영 실패 모드
    저장소 위치 코드와 MR 리뷰가 이미 GitLab 중심입니다. 여러 SCM에 코드가 흩어져 있고 미러링 정책이 없습니다. 커밋 기준과 빌드 기준이 달라 추적성이 깨집니다.
    Runner 운영 Docker 또는 Kubernetes 기반 격리 실행을 운영할 사람이 있습니다. 빌드 서버가 한 대뿐이고 전역 설치 도구에 강하게 의존합니다. 특정 서버에서만 되는 빌드가 됩니다.
    Secret 관리 토큰 목록과 소유자가 파악되어 있고 변수 범위를 재설계할 수 있습니다. 개인 계정 SSH 키, 오래된 API 토큰, 스크립트 내 평문 값이 섞여 있습니다. Job은 성공하지만 과도한 권한으로 배포됩니다.
    배포 승인 운영 배포 전 승인자와 브랜치 정책이 명확합니다. main push가 곧 운영 반영인데 롤백 절차가 문서화되어 있지 않습니다. 마이그레이션 첫 주에 자동 배포 사고가 납니다.
    외부 연동 컨테이너 레지스트리, 패키지 저장소, 클라우드 계정 접근 방식이 표준화되어 있습니다. Jenkins 플러그인이나 사내 스크립트가 인증을 대신 처리합니다. YAML에는 문제가 없는데 인증 단계에서 계속 pending 또는 401이 납니다.

    제 기준은 단순합니다. GitLab CI로 옮겼을 때 배포 이력과 권한이 더 잘 보이면 전환 가치가 큽니다. 반대로 기존 CI가 레거시 시스템의 접착제 역할을 하고 있다면, 전환이 아니라 먼저 분해가 필요합니다.

    실전 구현: 기존 파이프라인을 작게 쪼개 옮기는 순서

    처음부터 운영 배포까지 한 번에 옮기면 원인 추적이 지옥이 됩니다. 저는 테스트 전용 파이프라인, 산출물 생성, 수동 배포, 기존 CI 제거 순서로 갑니다. 느려 보이지만 장애 복구 시간을 줄여줍니다.

    1. 기존 CI 작업을 빌드, 테스트, 패키징, 이미지 빌드, 배포, 알림으로 나눕니다.
    2. 각 단계가 읽는 파일, 쓰는 산출물, 필요한 환경 변수를 표로 만듭니다.
    3. Runner Executor를 정합니다. 서버 상태 의존을 줄이고 싶으면 Docker Executor를 먼저 봅니다.
    4. 배포 없는 테스트 Job부터 GitLab CI에 붙입니다.
    5. 아티팩트와 캐시를 분리합니다. 결과물은 artifacts, 재사용 의존성은 cache입니다.
    6. 운영 배포는 rules, when: manual, protected branch/tag를 함께 설계합니다.

    아래 예시는 Node.js 프로젝트를 전제로 한 출발점입니다. 의도적으로 only 대신 rules를 썼습니다. GitLab 문서에서도 새 조건 설계는 rules 사용을 권장하고, only/except는 deprecated 키워드로 안내합니다.

    stages:
      - test
      - build
      - deploy
    
    default:
      image: node:20
      interruptible: true
      before_script:
        - node --version
        - npm --version
    
    cache:
      key:
        files:
          - package-lock.json
      paths:
        - .npm/
      policy: pull-push
    
    test:
      stage: test
      script:
        - npm ci --cache .npm --prefer-offline
        - npm test
      artifacts:
        when: always
        paths:
          - junit.xml
        reports:
          junit: junit.xml
        expire_in: 1 week
    
    build:
      stage: build
      script:
        - npm ci --cache .npm --prefer-offline
        - npm run build
      artifacts:
        paths:
          - dist/
        expire_in: 1 week
      needs:
        - job: test
          artifacts: false
    
    deploy_production:
      stage: deploy
      image: alpine:latest
      needs:
        - job: build
          artifacts: true
      script:
        - echo 'deploy script goes here'
      environment:
        name: production
      rules:
        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
          when: manual
        - when: never

    여기서 needs는 단순한 보기 좋은 문법이 아닙니다. Stage 순서를 무작정 기다리지 않고 필요한 Job 관계를 명시합니다. 다만 남발하면 의존 그래프가 복잡해져 장애 때 읽기 어려워집니다. 빌드 시간이 길고 독립 테스트가 많은 저장소에는 적극적으로 쓰고, 작은 서비스에는 Stage만으로 단순하게 유지하는 편이 낫더라고요.

    GitLab CI 마이그레이션에서 Runner와 설정 파일 구성 관계도

    .gitlab-ci.yml, Runner, Job, Artifact, Cache가 어떻게 연결되는지 보여주는 구성 다이어그램입니다.

    GitLab CI Runner 등록과 검증: 토큰 방식부터 확인하세요

    Self-managed Runner를 쓴다면 등록 단계에서 예전 블로그 글을 그대로 따라 하면 막힐 수 있습니다. Runner registration token 방식은 deprecated 상태이며, 현재 GitLab Runner 등록 흐름은 GitLab UI나 API에서 Runner를 만든 뒤 발급되는 runner authentication token을 사용하는 쪽이 기준입니다. 명령어 예시도 --registration-token이 아니라 --token을 기준으로 잡는 게 안전합니다.

    sudo gitlab-runner register \
      --url https://gitlab.example.com \
      --token glrt-REPLACE_WITH_RUNNER_AUTH_TOKEN \
      --executor docker \
      --docker-image alpine:latest \
      --description docker-runner-01

    Runner 태그와 protected 설정은 GitLab UI에서 관리되는 경우가 많습니다. 등록 명령만 성공했다고 Job이 실행되는 건 아닙니다. Pipeline이 계속 pending이면 저는 아래 순서로 봅니다.

    sudo gitlab-runner status
    sudo gitlab-runner verify
    sudo gitlab-runner list
    sudo gitlab-runner --debug run

    verify가 통과하면 GitLab 서버와 Runner의 통신은 일단 됩니다. 그래도 Job이 안 잡히면 네트워크보다 태그 매칭, protected branch/tag, Runner scope, locked 설정을 먼저 봅니다. Job에 tags가 있는데 Runner에 같은 태그가 없으면 정상적으로 대기합니다. 이건 장애가 아니라 스케줄링 조건 불일치입니다.

    test_with_tagged_runner:
      stage: test
      tags:
        - docker
        - linux
      image: alpine:latest
      script:
        - echo 'runner tag matched'
      rules:
        - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

    Executor 선택도 비용과 안정성에 직접 영향을 줍니다. Shell Executor는 빠르고 단순하지만 호스트 오염 위험이 큽니다. Docker Executor는 격리가 좋아 마이그레이션 검증에 유리하지만 이미지 pull, 캐시, Docker 데몬 접근 정책을 설계해야 합니다. Kubernetes Executor는 확장성은 좋지만 클러스터 운영 역량이 없으면 CI 문제가 곧 플랫폼 문제가 됩니다.

    트러블슈팅: 겉으로는 CI 문제, 뿌리는 운영 습관 문제

    로컬에서는 되는데 CI에서 깨지는 상황은 대부분 암묵적 환경 때문입니다. 제가 실제로 본 문제들은 문법 오류보다 환경 차이, 권한 범위, 캐시 오염이 훨씬 많았습니다. 이거 진짜 한 번 겪으면, 다음 마이그레이션부터는 환경 목록부터 보게 됩니다.

    1. 변수 이름은 같지만 범위가 달랐습니다

    기존 CI에서 전역 토큰 하나를 모든 프로젝트가 공유하던 팀이 있었습니다. GitLab으로 옮기면서 프로젝트 변수에 넣었는데, 실제 배포 Job은 하위 템플릿 프로젝트에서 실행되고 있었습니다. 이름은 같은데 스코프가 달라 값이 비어 있었던 겁니다. 해결은 Group Variables로 올릴 값과 Project Variables로 제한할 값을 분리하고, 운영 배포용 변수는 protected branch/tag에서만 노출되도록 하는 것이었습니다.

    git grep -n -E 'DEPLOY|TOKEN|PASSWORD|SECRET|PRIVATE_KEY' -- . ':!node_modules' ':!dist'
    git grep -n -E 'ssh|scp|rsync|kubectl|helm|aws|gcloud|az' -- . ':!node_modules' ':!dist'

    이 명령은 마이그레이션 전에 꼭 돌려봅니다. 결과가 많이 나온다는 건 나쁜 게 아닙니다. 숨어 있던 배포 지식을 드러낸 겁니다. 다만 평문 Secret이 나오면 즉시 회수, 재발급, 변수화까지 같이 해야 합니다. 단순히 YAML에서 지우는 건 보안 조치가 아닙니다.

    2. 캐시가 실패를 숨겼습니다

    node_modules를 통째로 캐시하면 처음엔 빨라 보입니다. 그런데 lock file이 바뀌었는데도 오래된 의존성이 남거나, Runner OS/아키텍처가 바뀌며 네이티브 모듈이 깨지는 경우가 있습니다. 그래서 저는 패키지 매니저 캐시 디렉터리를 캐시하고, 설치 결과 디렉터리를 산출물처럼 다루지 않습니다. npm이면 .npm/, pip이면 wheel/cache 계열처럼 재다운로드 비용을 줄이는 쪽이 낫습니다.

    3. 아티팩트와 캐시를 섞었습니다

    캐시는 다음 파이프라인에도 재사용될 수 있는 가속 장치입니다. 아티팩트는 이번 파이프라인의 결과물입니다. 배포에 필요한 dist/, 패키지 파일, 테스트 리포트는 cache가 아니라 artifacts에 둬야 합니다. 반대로 의존성 다운로드 캐시는 artifacts에 넣으면 보관 비용과 다운로드 시간이 불필요하게 커집니다.

    4. 배포 자동화를 너무 빨리 켰습니다

    마이그레이션 첫 주에는 운영 배포를 when: manual로 둡니다. 저는 이 기간에 로그 포맷, 배포 산출물, 롤백 명령, 승인자 권한을 확인합니다. 자동화는 마지막에 켜도 늦지 않습니다. 검증 안 된 자동 배포는 속도가 아니라 부채입니다.

    GitLab CI 마이그레이션 검증 기준

    마이그레이션 완료 기준을 ‘성공 표시가 떴다’로 잡으면 위험합니다. 저는 같은 커밋에서 같은 산출물이 나오는지, 누가 배포를 눌렀는지, 실패 로그만 보고 원인을 좁힐 수 있는지를 봅니다.

    검증 항목 확인 방법 통과 기준
    재현성 같은 commit SHA로 기존 CI와 GitLab CI 산출물 파일 목록을 비교합니다. 파일 누락, 이름 규칙, 환경별 설정 차이가 설명 가능합니다.
    추적성 Pipeline, Job, Environment, Artifact 링크로 배포 이력을 따라갑니다. 운영 반영 커밋과 실행자를 역추적할 수 있습니다.
    권한 분리 protected branch/tag, protected variables, manual job 권한을 확인합니다. 일반 개발자가 운영 Secret을 읽거나 배포하지 못합니다.
    실패 해석 실패 명령 바로 위의 환경 출력, 버전, 인증 로그를 봅니다. 네트워크, 인증, 의존성, 스크립트 오류 중 하나로 빠르게 좁힙니다.

    로그를 볼 때는 마지막 줄만 보면 안 됩니다. npm ci가 실패했다면 registry 인증, lock file, 네트워크, Node 버전 차이를 봅니다. docker build가 실패했다면 Dockerfile 단계와 build context를 확인합니다. kubectl apply가 실패했다면 kubeconfig 권한, namespace, server-side validation을 분리해서 봅니다.

    git diff --name-only origin/main...HEAD
    find dist -type f | sort > dist-files.txt
    sha256sum package-lock.json dist-files.txt
    printenv | sort | grep -E '^CI_|^GITLAB_|^NODE_|^NPM_'

    로컬에서 Runner를 흉내 내는 방식은 빠른 확인에는 도움이 되지만, GitLab 서버의 모든 동작을 완전히 재현한다고 믿으면 안 됩니다. 특히 protected variables, merge request pipeline 조건, 환경 승인, Runner 스케줄링은 실제 GitLab Pipeline에서 확인해야 합니다.

    GitLab CI 파이프라인 검증과 로그 분석 화면

    파이프라인 성공 여부, 실패 로그, Artifact 확인 흐름을 보여주는 검증 화면 예시입니다.

    CI/CD 전환 전략: 병행 운영이 느린 게 아니라 싸게 먹힙니다

    제가 가장 효과를 본 방식은 기존 CI를 바로 끄지 않는 겁니다. GitLab CI를 먼저 읽기 전용으로 붙이고, 같은 커밋에 대해 테스트와 산출물을 비교합니다. 이 기간은 낭비가 아닙니다. 기존 자동화에 숨어 있던 전역 의존성, 권한 누수, 오래된 배포 스크립트를 찾는 보험입니다.

    1. Inventory 단계: 기존 Job, Secret, 산출물, 외부 연동을 목록화합니다.
    2. Read-only 단계: GitLab CI에서 테스트만 돌리고 배포는 하지 않습니다.
    3. Shadow 단계: 기존 CI와 GitLab CI를 같은 커밋에서 같이 돌립니다.
    4. Manual Deploy 단계: 운영 배포를 수동 승인 Job으로 연결합니다.
    5. Primary 단계: GitLab CI를 기본 파이프라인으로 바꾸고 기존 CI 트리거를 중지합니다.
    6. Cleanup 단계: 기존 토큰, 빌드 서버 권한, 스케줄러, 웹훅을 제거합니다.

    성능과 비용을 볼 때도 단순히 ‘GitLab CI가 빠른가’로 보면 답이 흐립니다. 비용을 좌우하는 건 Runner 대수보다 실패 재실행 횟수, 이미지 pull 정책, 캐시 설계, 병렬화된 Job의 대기 시간입니다. 작은 서비스는 단순한 Stage 기반 파이프라인이 더 안정적이고, 큰 모노레포는 rules:changes, needs, child pipeline을 검토할 가치가 있습니다. 다만 child pipeline은 관측 포인트가 늘어나므로 팀이 로그를 읽을 준비가 된 뒤에 넣는 편이 좋습니다.

    자주 묻는 질문

    GitLab CI 마이그레이션은 언제 하는 게 좋나요?

    저는 코드가 GitLab에 있고, Merge Request 중심으로 리뷰하며, 빌드와 배포 이력을 커밋 기준으로 추적하고 싶을 때 추천합니다. 특히 운영 배포 승인과 결과물을 GitLab 안에서 보고 싶은 팀은 전환 효과가 큽니다.

    Jenkins에서 바로 GitLab CI로 옮겨도 괜찮나요?

    가능하지만 Jenkinsfile을 줄 단위로 번역하면 실패하기 쉽습니다. 먼저 stage 구조, 플러그인 의존성, credential 사용 위치, workspace에 남는 파일을 분해해야 합니다. Jenkins 플러그인이 해주던 일을 GitLab CI에서는 YAML, Runner 설정, 외부 CLI, protected variables 조합으로 다시 설계하는 경우가 많습니다.

    가장 먼저 확인할 설정은 뭔가요?

    Runner 태그, protected branch/tag, CI/CD Variables 범위, artifacts 경로, cache key를 먼저 봅니다. 제 경험상 초반 문제의 상당수는 이 다섯 가지에서 나왔습니다.

    Shell Executor와 Docker Executor 중 무엇을 골라야 하나요?

    기존 빌드 서버의 상태를 그대로 써야 하고 격리 요구가 낮으면 Shell Executor가 빠른 출발점이 될 수 있습니다. 하지만 마이그레이션 품질을 높이고 싶다면 Docker Executor가 낫습니다. 매번 비슷한 컨테이너 환경에서 실행되기 때문에 ‘그 서버에만 깔린 무언가’를 빨리 찾아냅니다.

    GitLab CI 마이그레이션 결정 기준 요약 인포그래픽

    전환 여부를 판단하기 위한 저장소, Runner, 보안, 배포 승인 기준을 요약한 인포그래픽입니다.

    마무리: GitLab CI 마이그레이션은 언제 밀어붙일까

    GitLab CI 마이그레이션은 파이프라인 파일 하나를 만드는 작업이 아닙니다. 빌드 지식, 배포 권한, 장애 대응 루틴을 저장소 중심으로 다시 쓰는 일입니다. 그래서 저는 도구의 인기보다 운영 방식이 더 투명해지는지를 기준으로 봅니다.

    GitLab에 코드가 있고, MR 리뷰가 일상이고, 운영 배포 이력까지 한곳에서 보고 싶다면 GitLab CI로 옮기세요. 이 경우에는 테스트 전용 파이프라인부터 시작해 manual deploy까지 붙이는 방식이 가장 무난합니다. 반대로 Jenkins 플러그인, 개인 SSH 키, 사내 배포 서버의 전역 상태에 강하게 묶여 있다면 바로 전환하지 마세요. 먼저 Shadow Pipeline으로 기존 결과와 GitLab CI 결과를 비교하고, Secret과 배포 권한을 정리한 뒤에 주 파이프라인으로 승격하는 편이 안전합니다.

    제 경험상 성공적인 전환의 신호는 ‘파이프라인이 빨라졌다’보다 ‘실패했을 때 누구나 같은 방식으로 원인을 좁힐 수 있다’에 가깝습니다. 속도는 그다음에 옵니다. 파이프라인은 옮기는 게 아니라, 팀의 배포 습관을 코드로 다시 쓰는 일입니다.

    참고한 1차 문서: GitLab Runner 등록 문서, GitLab CI deprecated keywords, GitLab CI caching 문서

  • [Cloud] Datadog 비용 최적화 전략: 불필요한 지출 줄이기

    [Cloud] Datadog 비용 최적화 전략: 불필요한 지출 줄이기

    Datadog 비용 최적화 전략: 불필요한 지출을 줄이는 운영 방법론

    Datadog 비용 최적화는 모니터링을 약하게 만드는 일이 아닙니다. 장애를 설명하는 신호는 끝까지 남기고, 아무도 보지 않는 반복 신호는 수집 전이나 색인 단계에서 줄이는 작업에 가깝습니다. 이 선을 먼저 정해두면 비용도 줄고, 운영자가 불안해하지 않더라고요.

    Datadog 비용이 커지는 조직을 보면 공통점이 있습니다. 기능을 많이 써서라기보다 수집 정책의 기본값이 운영 정책처럼 굳어져 있습니다. Docker stdout은 전부 로그로 들어오고, Kubernetes의 짧게 뜨는 Job도 서비스처럼 취급되고, DEBUG 로그는 릴리스가 끝난 뒤에도 계속 색인됩니다.

    홈랩에서 Kubernetes와 VM을 섞어 굴릴 때도 비슷했습니다. 서비스 몇 개만 올렸는데 health check 로그, readiness probe 로그, 테스트 Job 출력, DEBUG 로그가 생각보다 빨리 쌓이더라고요. 그때 깨달은 건 단순했습니다. 비용을 줄이려면 UI에서 필터 몇 개 만지는 것보다 수집, 태깅, 색인, 보관, 알림의 경계를 먼저 정해야 합니다.

    Datadog 비용 최적화 전체 흐름을 보여주는 아키텍처 다이어그램

    Datadog 비용 최적화는 한 번의 정리 작업이 아니라 운영 루프입니다. 저는 보통 귀속 → 차단 → 샘플링 → 보관 → 검증 순서로 봅니다. 이 순서를 지키면 무엇을 줄였는지와 무엇을 아직 볼 수 있는지가 같이 남습니다.

    1. Datadog 비용 최적화는 제품명이 아니라 의사결정 단위로 보기

    Datadog 지출을 볼 때 “로그가 비싸다”, “APM이 비싸다”처럼 제품명으로만 보면 조치가 흐려집니다. 실무에서는 다음 질문으로 쪼개야 합니다.

    • 누가 만들었나: team, service, env 태그로 소유자를 찾을 수 있는가?
    • 어디에서 늘었나: host, container, custom metric, indexed log, ingested log, trace 중 어느 축인가?
    • 장애 대응에 쓰였나: 최근 인시던트에서 실제로 검색하거나 알림 조건에 사용했는가?
    • 줄이면 무엇을 잃나: 원인 분석 시간, 보안 감사, 규제 보관, SLO 계산에 영향이 있는가?

    제가 가장 먼저 확인하는 화면과 지표는 아래입니다.

    • Usage Attribution: 설정한 태그 키를 기준으로 사용량을 나눠 봅니다. 다만 모든 제품 사용량이 모든 태그 기준으로 완벽히 분해되지는 않습니다.
    • Estimated Usage Metrics: datadog.estimated_usage.* 메트릭으로 증가 추세를 봅니다. 실시간 추정치라 청구서와 1:1로 맞추는 용도보다는 변화 감지에 더 적합합니다.
    • Log Indexes: 어떤 로그가 검색 가능한 상태로 저장되는지, 어떤 exclusion filter를 통과하는지 확인합니다.
    • Retention: prod 오류 로그와 dev access log가 같은 기간 저장되고 있지 않은지 봅니다.

    비용 절감 작업에서 가장 위험한 말은 “일단 Agent 꺼보죠”입니다. Agent를 끄면 비용은 줄어든 것처럼 보이지만 장애 때 시스템이 침묵합니다. 먼저 소유권과 경로를 잡고, 그다음 수집량을 줄이는 편이 안전합니다.

    2. 태그 표준화: 비용 보고서가 아니라 운영 계약으로 다루기

    Datadog에서 태그는 검색 편의 기능을 넘어 비용 통제의 단위입니다. 태그가 흔들리면 Usage Attribution도 흔들리고, 팀별 비용 대화도 금방 흐려집니다. 저는 최소한 아래 4개를 기본 계약으로 둡니다.

    env:prod | env:staging | env:dev
    service:api | service:worker | service:nginx
    team:platform | team:billing | team:checkout
    cost_center:shared | cost_center:revenue | cost_center:internal

    중요한 건 태그 이름보다 태그가 붙는 위치입니다. 애플리케이션 로그에는 service가 붙었는데 컨테이너 메트릭에는 빠지고, APM trace에는 다른 서비스명이 들어가면 비용 분석이 갈라집니다. 가능하면 애플리케이션, 컨테이너 label, Helm values, Agent 설정에서 같은 네이밍을 유지하세요.

    Kubernetes에서는 Pod label과 Datadog 태그를 연결하는 규칙을 명확히 두는 편이 좋습니다. 예를 들어 Helm values에서 공통 태그를 관리하면 배포마다 태그가 빠지는 실수를 줄일 수 있습니다.

    # datadog-values.yaml 예시
    datadog:
      tags:
        - env:prod
        - team:platform
        - cost_center:shared
      logs:
        enabled: true
        containerCollectAll: false
      apm:
        portEnabled: true

    containerCollectAll: false는 모든 컨테이너 로그를 기본 수집하지 않겠다는 선택입니다. 이 값 하나로 비용이 마법처럼 줄지는 않지만, “로그를 보내려면 의도를 표시한다”는 문화를 만들 수 있습니다. 다만 초기 도입기나 장애가 잦은 시스템에서는 너무 빨리 닫으면 디버깅이 어려워집니다.

    3. Agent 단계에서 버릴 것과 Datadog 안에서 줄일 것 구분하기

    로그 비용 최적화의 첫 갈림길은 “Agent에서 보내지 않을 것인가, Datadog에는 보내되 색인하지 않을 것인가”입니다. 둘은 완전히 다른 결정입니다.

    선택지 언제 쓰나 장점 위험
    Agent 단계 제외 health check, readiness probe, 로컬 개발 Job처럼 분석 가치가 낮고 반복적인 로그 전송과 ingestion 자체를 줄입니다. 나중에 Datadog에서 복구할 수 없습니다.
    Index exclusion filter 들어오긴 해야 하지만 검색 저장은 줄이고 싶은 로그 수집 후 라우팅과 샘플링을 조정할 수 있습니다. ingestion은 계속 발생합니다.
    별도 Index 분리 보관 기간, 접근 권한, 검색 목적이 다른 로그 prod, audit, dev 로그의 보관 정책을 다르게 가져갑니다. 라우팅 쿼리와 순서를 문서화해야 합니다.
    애플리케이션 로그 레벨 수정 DEBUG/TRACE가 prod에서 계속 발생하는 경우 가장 근본적인 해결입니다. 릴리스 관찰에 필요한 신호까지 사라질 수 있습니다.

    제 기준은 간단합니다. 절대 다시 보지 않을 로그는 Agent에서 제외하고, 상황에 따라 필요할 수 있는 로그는 Datadog에 보낸 뒤 색인 정책으로 조절합니다. 보안, 감사, 결제, 인증 실패 로그는 비용만 보고 Agent 단계에서 버리면 안 됩니다.

    4. Datadog Agent 로그 수집을 실제로 줄이는 설정

    먼저 Agent가 무엇을 수집하는지 확인합니다. 비용 최적화 전에 이 명령어 결과를 남겨두면 변경 전후 비교가 쉽습니다.

    sudo datadog-agent status
    sudo datadog-agent configcheck
    sudo datadog-agent flare --local

    컨테이너로 Agent를 운영한다면 아래처럼 확인합니다.

    docker exec -it datadog-agent agent status
    docker exec -it datadog-agent agent configcheck

    NGINX access log에서 health check만 제외하려면 서비스별 conf에서 log_processing_rules를 씁니다. 패턴은 Go 정규식 문법을 따르므로, 운영 반영 전에 샘플 로그로 꼭 검증하세요.

    # /etc/datadog-agent/conf.d/nginx.d/conf.yaml
    logs:
      - type: file
        path: /var/log/nginx/access.log
        service: nginx
        source: nginx
        log_processing_rules:
          - type: exclude_at_match
            name: exclude_healthcheck
            pattern: 'GET /health(?:\s|\?)'

    GET /health처럼 단순 문자열만 쓰면 의도치 않은 경로까지 걸릴 수 있습니다. 비용 필터는 일부러 좁게 잡는 편이 낫습니다. 비용을 아끼려다 필요한 장애 로그를 버리면 복구 비용이 더 커지거든요.

    반대로 특정 로그만 보내고 싶다면 include_at_match를 쓸 수 있습니다. 다만 이 방식은 나머지를 모두 버리는 성격이라 dev나 특정 배치 Job처럼 목적이 좁은 곳에만 권합니다.

    # 특정 배치 Job에서 ERROR/WARN만 Datadog으로 전송
    logs:
      - type: file
        path: /var/log/batch/job.log
        service: settlement-batch
        source: java
        log_processing_rules:
          - type: include_at_match
            name: include_warn_error_only
            pattern: '"level":"(WARN|ERROR)"|\b(WARN|ERROR)\b'
    Datadog Agent 로그 필터링으로 옵저버빌리티 비용 절감하는 구성도

    Agent 단계 필터는 효과가 빠르지만 되돌릴 수 있는 데이터가 없습니다. 그래서 저는 health check, liveness probe, 성공한 정적 파일 요청처럼 장애 설명력이 낮고 재현 가능한 로그부터 제외합니다.

    5. Docker와 Kubernetes: 전체 수집보다 명시 수집을 기본값으로 만들기

    컨테이너 환경에서 비용이 흔들리는 이유는 서비스 수보다 수명 짧은 워크로드와 stdout 기본 수집 때문입니다. CronJob, migration Job, 임시 테스트 컨테이너가 모두 같은 규칙으로 수집되면 비용 그래프가 운영 트래픽과 무관하게 튑니다.

    Docker Compose에서는 Datadog Agent가 자기 자신이나 개발용 컨테이너 로그를 물고 들어가지 않게 제외 규칙을 둡니다. Compose의 environment list 문법에서는 값 전체를 불필요하게 따옴표로 감싸지 않는 편이 읽기도 좋습니다.

    services:
      datadog-agent:
        image: gcr.io/datadoghq/agent:latest
        environment:
          - DD_LOGS_ENABLED=true
          - DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL=true
          - DD_CONTAINER_EXCLUDE_LOGS=name:datadog-agent image:^local-dev-tool:.* name:^tmp-.*
          - DD_CONTAINER_EXCLUDE_METRICS=name:^tmp-.*
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock:ro
          - /var/lib/docker/containers:/var/lib/docker/containers:ro
          - /proc/:/host/proc/:ro
          - /sys/fs/cgroup/:/host/sys/fs/cgroup:ro

    DD_CONTAINER_EXCLUDE는 전역 제외에 가깝고, DD_CONTAINER_EXCLUDE_LOGS와 DD_CONTAINER_EXCLUDE_METRICS는 로그와 메트릭을 따로 다룰 때 씁니다. 저는 보통 로그부터 줄입니다. 메트릭은 장애 대응의 기본 뼈대라서, 임시 Job이 아닌 이상 너무 공격적으로 빼지 않습니다.

    Kubernetes에서는 namespace나 label 정책을 섞어야 합니다. 예를 들어 dev namespace 전체 로그를 줄이고, 꼭 필요한 Pod만 annotation으로 수집하는 방식이 관리하기 편합니다.

    # 로그를 수집할 Pod에만 명시적으로 Autodiscovery annotation 부여
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: api
      namespace: prod
    spec:
      template:
        metadata:
          labels:
            app: api
            tags.datadoghq.com/env: prod
            tags.datadoghq.com/service: api
            tags.datadoghq.com/team: checkout
          annotations:
            ad.datadoghq.com/api.logs: >-
              [{"source":"java","service":"api","log_processing_rules":[{"type":"exclude_at_match","name":"drop_health","pattern":"GET /health(?:\\s|\\?)"}]}]
        spec:
          containers:
            - name: api
              image: registry.example.com/api:stable

    여기서 흔한 실패 모드는 컨테이너 이름 불일치입니다. annotation의 ad.datadoghq.com/api.logs에서 api는 컨테이너 이름과 맞아야 합니다. Deployment 이름이나 Kubernetes Service 이름으로 착각하면 설정이 조용히 빗나갑니다.

    6. Log Index와 Exclusion Filter: 비용 최적화의 진짜 손잡이

    Datadog 로그는 ingestion과 indexing을 나눠 봐야 합니다. 들어온 로그 전체가 곧 검색 가능한 로그는 아닙니다. Index exclusion filter를 쓰면 Datadog에는 로그가 들어오지만, 특정 조건의 로그를 색인하지 않거나 샘플링할 수 있습니다.

    제가 자주 쓰는 판단은 이렇습니다.

    • 2xx access log: 전체 요청 추세는 메트릭으로 보고, 로그는 샘플링합니다.
    • 4xx log: 로그인, 결제, API gateway는 보존 쪽에 무게를 둡니다. 정적 리소스 404는 샘플링 후보입니다.
    • 5xx log: 기본 보존입니다. 줄이고 싶다면 먼저 중복 메시지와 애플리케이션 재시도 폭주를 고칩니다.
    • DEBUG/TRACE: prod 기본 색인은 피합니다. 릴리스 관찰 기간에는 만료 시간을 두고 켭니다.
    • audit/security: 비용 최적화의 첫 대상이 아닙니다. 보관 기간과 접근 권한을 별도 Index로 관리합니다.
    로그 유형 추천 정책 쓰면 좋은 도구 피해야 할 접근
    Health check, readiness probe Agent에서 제외 exclude_at_match, DD_CONTAINER_EXCLUDE_LOGS 검색 UI에서만 숨기고 ingestion을 계속 두기
    정상 2xx access log 샘플링 또는 낮은 보관 기간 Index exclusion filter, log-based metric 모든 2xx를 장기 색인
    오류 5xx, panic, exception 보존 별도 Index, Error Tracking, alert 비용 급증 원인이라는 이유만으로 일괄 제외
    개발/스테이징 로그 수집 범위 제한, 짧은 보관 namespace 정책, env 태그, index routing prod와 같은 Index/retention 적용
    보안/감사 로그 별도 정책으로 보호 전용 Index, RBAC, retention 정책 일반 access log와 같은 exclusion filter 공유

    Index filter에서 특히 조심할 점은 순서입니다. 넓은 라우팅 규칙이 앞에 있으면 뒤의 세밀한 규칙은 기대처럼 작동하지 않을 수 있습니다. prod 오류 로그는 먼저 분리하고, 제외 규칙은 더 좁게 두는 편이 안전합니다.

    7. Estimated Usage Metrics로 변화 감시하기

    비용 최적화는 적용보다 검증이 더 중요합니다. 저는 변경 전후에 아래 메트릭을 대시보드에 올려 둡니다.

    datadog.estimated_usage.logs.ingested_bytes
    datadog.estimated_usage.logs.ingested_events
    datadog.estimated_usage.logs.truncated_count
    datadog.estimated_usage.containers
    datadog.estimated_usage.hosts
    datadog.estimated_usage.metrics.custom

    최근 Metric Name Pricing을 쓰는 조직은 datadog.estimated_usage.metrics.custom 대신 datadog.estimated_usage.metrics.points.ingested, datadog.estimated_usage.metrics.points.indexed, datadog.estimated_usage.billable.metrics 같은 메트릭을 확인해야 할 수 있습니다. 계약과 과금 모델에 따라 보이는 지표가 달라질 수 있으니 Plan & Usage 화면과 함께 검증하세요.

    로그 사용량 메트릭은 service, status, datadog_index, datadog_is_excluded 같은 태그와 함께 볼 수 있습니다. 이 태그들이 비어 있거나 N/A로 보이면 라우팅이나 태깅이 기대대로 되지 않는다는 신호일 수 있습니다.

    모니터는 절대값보다 변화율에 먼저 둡니다. 계약과 트래픽이 조직마다 달라서 “몇 GB 이상이면 문제” 같은 숫자를 박아 넣는 건 오히려 위험합니다. 대신 운영 이벤트와 연결해서 봅니다.

    • 배포 직후 특정 service의 로그 이벤트가 급증했는가?
    • 주말이나 야간에도 env:dev 로그가 계속 색인되는가?
    • datadog_is_excluded:false 비율이 갑자기 늘었는가?
    • status:debug가 prod에서 계속 증가하는가?
    • truncated_count가 늘어 긴 로그가 잘리고 있지는 않은가?
    예시 모니터 쿼리 방향
    sum:datadog.estimated_usage.logs.ingested_events{service:api,env:prod,status:debug}.as_count()
    
    예시 대시보드 분해 기준
    sum:datadog.estimated_usage.logs.ingested_bytes{*} by {service,env,datadog_index,datadog_is_excluded}

    Estimated Usage Metrics는 청구 금액 계산기가 아니라 조기 경보 장치로 쓰는 게 맞습니다. 최종 비용은 계약, 제품별 과금 기준, 보관 정책에 따라 달라지므로 숫자를 단정하지 마세요.

    Datadog 사용량 대시보드에서 비용 최적화 결과를 확인하는 화면

    전후 비교는 총량보다 service, env, team, datadog_index별 변화로 보는 쪽이 실무적입니다. 총량만 보면 트래픽 증가와 낭비 증가를 구분하기 어렵습니다.

    8. 재현 가능한 시나리오: DEBUG 로그가 prod 비용을 밀어 올릴 때

    실제로 자주 보는 장면입니다. 신규 릴리스 후 service:api에서 DEBUG 로그가 켜진 채로 prod에 올라갑니다. 장애는 없는데 로그 색인량만 계속 늘고, 개발팀은 “문제 생기면 보려고 켜둔 것”이라고 말합니다. 이때 바로 DEBUG를 전부 버리면 또 다른 문제가 생깁니다.

    1. Log Explorer에서 env:prod service:api status:debug로 검색합니다.
    2. 상위 message pattern을 확인합니다. 같은 메시지가 반복되면 코드 경로를 찾습니다.
    3. trace_id, request_id, user_id 같은 고카디널리티 필드가 불필요하게 facet이나 태그로 승격됐는지 봅니다.
    4. 릴리스 관찰 목적이면 종료 시각을 정하고, 그 이후에는 INFO 이상으로 되돌립니다.
    5. 즉시 비용 방어가 필요하면 Index exclusion filter로 DEBUG 색인을 줄이되, ERROR/WARN과 특정 릴리스 태그는 보존합니다.
    6. 사후에는 배포 파이프라인에 prod 로그 레벨 검증을 넣습니다.

    애플리케이션 쪽에서 막을 수 있다면 그게 제일 좋습니다. 예를 들어 Kubernetes 배포에 로그 레벨 환경 변수를 명시하고, prod에서 DEBUG가 들어가지 않게 리뷰 규칙을 둡니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: api
    spec:
      template:
        metadata:
          labels:
            app.kubernetes.io/version: "1.2.3"
        spec:
          containers:
            - name: api
              image: registry.example.com/api:stable
              env:
                - name: LOG_LEVEL
                  value: INFO
                - name: DD_SERVICE
                  value: api
                - name: DD_ENV
                  value: prod
                - name: DD_VERSION
                  valueFrom:
                    fieldRef:
                      fieldPath: metadata.labels['app.kubernetes.io/version']

    운영에서 “DEBUG는 무조건 나쁘다”는 식으로 접근하면 팀이 우회합니다. 제가 선호하는 정책은 기본은 INFO, DEBUG는 티켓과 만료 시간을 가진 예외입니다. 이거 진짜 편하더라고요. 비용도 줄고, 필요한 디버그 관찰도 정당하게 할 수 있습니다.

    9. 커스텀 메트릭과 태그 카디널리티 관리

    Datadog 비용 최적화에서 로그만 보고 끝내면 절반만 본 겁니다. 커스텀 메트릭은 태그 조합이 늘어날수록 관리가 어려워집니다. 특히 user_id, request_id, session_id, pod_uid 같은 값이 태그로 들어가면 카디널리티가 급격히 커질 수 있습니다.

    제가 보는 기준은 단호합니다. 집계해서 의사결정하지 않을 값은 메트릭 태그로 올리지 않습니다. 로그 필드로 남기는 것과 메트릭 태그로 승격하는 것은 비용과 쿼리 성능 면에서 다른 선택입니다.

    필드 메트릭 태그 적합성 이유 대안
    env 높음 집계와 알림 분리에 계속 사용됩니다. 공통 태그로 유지
    service 높음 소유권과 장애 범위를 나누는 기본 축입니다. APM, 로그, 메트릭 모두 동일 명칭 사용
    team 중간~높음 비용 귀속과 운영 책임에 유용합니다. 조직 개편 시 매핑 관리
    user_id 낮음 고카디널리티가 되기 쉽습니다. 로그 필드 또는 샘플링 이벤트로 유지
    request_id 낮음 거의 모든 요청마다 값이 달라집니다. trace/log correlation에 사용

    StatsD나 DogStatsD를 쓰는 서비스라면 코드 리뷰에서 태그 추가를 가볍게 보지 마세요. 새 태그 하나가 대시보드 필터를 편하게 만들 수도 있지만, 반대로 비용과 쿼리 성능을 계속 압박할 수도 있습니다.

    # 피하고 싶은 예: request_id가 태그로 들어감
    statsd.increment('checkout.payment_attempt', tags=[
        'env:prod',
        'service:checkout',
        f'request_id:{request_id}'
    ])
    
    # 더 나은 예: 집계 가능한 축만 태그로 사용
    statsd.increment('checkout.payment_attempt', tags=[
        'env:prod',
        'service:checkout',
        f'payment_method:{payment_method}',
        f'result:{result}'
    ])

    10. APM과 Trace: 샘플링을 비용 절감 버튼처럼만 보지 않기

    APM 비용이 부담될 때 흔한 반응은 “샘플링 낮추자”입니다. 그런데 trace는 로그보다 더 조심해야 합니다. 낮은 샘플링은 비용을 줄이지만, 희귀한 장애 경로와 긴 꼬리 지연을 놓칠 수 있습니다.

    제가 쓰는 선택 기준은 이렇습니다.

    • 트래픽은 많고 정상 경로가 반복적: 샘플링을 낮추고 RED 지표는 메트릭으로 봅니다.
    • 오류율은 낮지만 비즈니스 영향이 큼: 오류 trace와 특정 service/resource는 더 오래 보존합니다.
    • 릴리스 직후: 일정 시간 샘플링을 넓히고, 안정화 뒤 기본값으로 되돌립니다.
    • 비용 급증: span tag에 고카디널리티 값이 붙었는지, 새 instrumentation이 과도한 span을 만들었는지 먼저 봅니다.

    즉 APM 최적화의 핵심은 “적게 남기기”가 아니라 희귀하지만 중요한 경로를 보존하고, 반복적인 정상 경로를 얇게 보는 것입니다.

    11. 트러블슈팅: 최적화 뒤 관측성이 깨지는 대표 원인

    비용 최적화 후 “로그가 안 보여요”가 나오면 대부분 아래 원인 중 하나입니다. 겉으로는 비슷해 보여도 근본 원인이 다르므로 확인 순서를 정해두는 게 좋습니다.

    증상 가능한 원인 확인 방법 조치
    특정 Pod 로그가 전혀 안 보임 container exclude 규칙 또는 annotation 이름 불일치 agent configcheck, Agent 로그, Pod container name 확인 annotation의 컨테이너명과 실제 container name을 맞춥니다.
    로그는 들어오는데 검색이 안 됨 Index routing 또는 exclusion filter에 걸림 datadog_index, datadog_is_excluded 태그 확인 Index 순서와 필터 쿼리를 좁힙니다.
    오류 로그까지 줄어듦 Agent exclude 패턴이 너무 넓음 변경된 regex와 샘플 로그 비교 ERROR/WARN 예외를 먼저 분리하거나 Agent 제외 대신 Index 정책으로 옮깁니다.
    비용은 줄었는데 대시보드가 비어 보임 로그 기반 메트릭 또는 facet 의존성 누락 대시보드 쿼리의 데이터 소스 확인 로그를 줄이기 전 필요한 메트릭을 별도로 생성합니다.
    변경했는데 효과가 없음 Agent 재시작 누락, DaemonSet 미롤아웃, 다른 설정 경로 우선 rollout status, agent status 롤아웃과 적용된 최종 설정을 확인합니다.

    Agent 설정을 바꾼 뒤에는 아래 순서로 확인합니다.

    sudo systemctl restart datadog-agent
    sudo datadog-agent configcheck
    sudo datadog-agent status
    sudo journalctl -u datadog-agent --since '10 minutes ago' --no-pager

    Kubernetes라면 DaemonSet 롤아웃과 Agent Pod 로그를 같이 봅니다.

    kubectl -n datadog rollout status daemonset/datadog-agent
    kubectl -n datadog get pods -l app=datadog-agent -o wide
    kubectl -n datadog logs daemonset/datadog-agent --tail=200

    12. 운영 체크리스트: 한 번 줄이고 끝내지 않기

    Datadog 비용은 한 번 정리해도 다시 자랍니다. 새 서비스, 새 로그 필드, 새 대시보드, 새 tracer가 계속 들어오기 때문입니다. 그래서 저는 월 1회 또는 큰 릴리스 뒤에 아래 체크리스트를 돌립니다.

    • env, service, team 태그가 Usage Attribution에 충분히 반영되는가?
    • 지난 7일 또는 지난 릴리스 이후 사용량이 급증한 service가 있는가?
    • prod에서 DEBUG/TRACE 로그가 계속 색인되는가?
    • dev/staging 로그가 prod와 같은 보관 정책을 쓰고 있지 않은가?
    • Index exclusion filter 순서가 문서와 일치하는가?
    • 커스텀 메트릭 태그에 고카디널리티 값이 새로 들어오지 않았는가?
    • APM trace 샘플링이 중요한 오류 경로를 놓치지 않는가?
    • 비용 절감 후 대시보드, SLO, 알림이 정상 동작하는가?
    Datadog 비용 최적화 체크리스트 요약 인포그래픽

    체크리스트는 문서로만 두면 잘 안 봅니다. 저는 대시보드에 “이번 달 사용량 증가 Top service”와 “prod DEBUG 로그”를 같이 올려둡니다. 팀이 자기 사용량을 직접 보게 만드는 편이 중앙 플랫폼팀이 계속 말하는 것보다 훨씬 오래 갑니다.

    FAQ: Datadog 비용 최적화에서 자주 막히는 지점

    로그를 줄이면 장애 대응이 느려지지 않나요?

    무작정 줄이면 느려집니다. 그래서 error, warn, security, audit 로그는 먼저 지키고, health check, 반복 2xx access log, prod DEBUG 같은 후보부터 줄입니다. 필요한 신호를 메트릭으로 승격한 뒤 로그 색인을 줄이는 방식도 좋습니다.

    가장 먼저 손볼 곳은 어디인가요?

    대부분은 로그 색인 정책과 컨테이너 로그 수집 범위부터 보는 게 빠릅니다. 그다음 커스텀 메트릭 카디널리티, APM sampling과 retention을 봅니다. host 수를 줄이는 건 인프라 구조와 연결되어 있어 단기 비용 최적화 카드로는 조심스럽습니다.

    Agent에서 제외하는 것과 Index에서 제외하는 것 중 무엇이 더 좋나요?

    다시 볼 일이 없는 반복 로그는 Agent에서 제외하세요. 나중에 검색할 가능성이 있거나 운영 정책이 바뀔 수 있는 로그는 Datadog에 보낸 뒤 Index exclusion filter나 retention으로 다루는 편이 안전합니다.

    Usage Attribution이 팀별로 깔끔하게 안 나뉩니다. 왜 그런가요?

    계측 단계에서 태그가 빠졌거나, 제품별 사용량이 모든 태그 기준으로 완벽히 분해되지 않을 수 있습니다. 그래서 비용 보고서를 만들기 전에 공통 태그가 로그, 메트릭, trace, 컨테이너에 일관되게 들어가는지 먼저 확인해야 합니다.

    마무리: 줄일 데이터와 지킬 신호를 분리하세요

    Datadog 비용 최적화의 핵심은 “덜 보기”가 아니라 장애를 설명하는 신호만 더 선명하게 남기는 것입니다. 운영 환경이라면 Usage Attribution + 태그 표준화 + Log Index 필터를 먼저 잡으세요. 개발/스테이징 비용이 문제라면 Agent 수집 범위와 컨테이너 제외 규칙부터 손보는 게 빠릅니다.

    이럴 땐 이렇게 가면 됩니다. health check와 반복 정상 로그는 Agent에서 제외하세요. prod 오류, 보안, 감사 로그는 보존하세요. 2xx access log는 샘플링하거나 짧게 보관하세요. DEBUG 로그는 기본 비활성화하고, 필요할 때만 만료 시간을 두고 켜세요. 커스텀 메트릭에는 집계 가능한 태그만 올리세요.

    마지막으로 변경 전후를 꼭 같은 대시보드로 보세요. 감으로 줄였다고 말하지 말고, service, env, team, datadog_index별 사용량 변화로 확인하는 겁니다. 관련 글로는 로그 레벨 운영 정책, Kubernetes 관측성 태그 표준화, 클라우드 모니터링 비용 관리 글을 함께 연결하면 독자가 다음 단계로 이동하기 좋습니다.

  • [Cloud] Podman vs Docker: 홈랩 환경에서의 성능 및 관리 비교

    [Cloud] Podman vs Docker: 홈랩 환경에서의 성능 및 관리 비교

    Podman vs Docker: 홈랩 환경에서의 성능 및 관리 비교

    안녕하세요, 13년차의 서버실 주인장입니다. 제 홈랩 이야기를 풀어놓는 곳이죠. 오늘은 많은 분들이 궁금해하실 만한 주제, 바로 Podman(팟맨)과 Docker(도커)에 대해 이야기해보려고 합니다. 저도 처음 인프라 엔지니어 일을 시작할 때부터 Docker를 줄곧 써왔고, 사실 컨테이너 하면 Docker가 거의 대명사처럼 쓰였잖아요? 근데 요즘 들어 Podman이 심상치 않다는 이야기를 많이 듣게 되더라고요. 특히나 개인적인 홈랩 환경에서는 어떤 선택이 더 좋을지 직접 비교해보고 싶어서, 제 홈랩 서버에 둘 다 설치해서 며칠간 굴려봤습니다. 삽질도 좀 했고요. ㅎㅎ

    컨테이너 기술은 이제 선택이 아닌 필수가 되었죠. 마이크로서비스 아키텍처(MSA)부터 CI/CD 파이프라인, 심지어 IoT 디바이스까지, 어디든 컨테이너가 쓰이지 않는 곳이 없습니다. 홈랩에서도 마찬가지예요. 웹서버, 데이터베이스, 각종 모니터링 툴까지 컨테이너로 띄워두면 관리도 편하고 자원도 효율적으로 쓸 수 있거든요. 하지만 두 가지 주요 컨테이너 런타임(Container Runtime), Podman과 Docker 중에서 어떤 것을 선택해야 할지 고민하는 분들이 많으실 겁니다. 저도 그랬거든요.

    Podman과 Docker의 핵심 아키텍처 차이점을 보여주는 다이어그램

    Podman과 Docker의 핵심 아키텍처 차이를 시각적으로 비교한 다이어그램입니다. 데몬 유무, rootless 모드 지원 여부 등에 초점을 맞춥니다.

    Podman과 Docker, 뭐가 다른가요?

    쉽게 말해, 둘 다 컨테이너를 만들고 실행하는 도구죠. 하지만 내부적으로 작동하는 방식에는 꽤 큰 차이가 있어요. 제가 직접 써보면서 느낀 핵심적인 차이점들을 짚어볼게요.

    • Docker (도커): 컨테이너 기술의 사실상 표준이 되었죠. 데몬(Daemon) 기반으로 작동합니다. 즉, dockerd라는 백그라운드 프로세스가 항상 떠 있어서 클라이언트의 명령을 받고 컨테이너를 관리하는 형태예요. 이 데몬이 모든 컨테이너 작업을 총괄하고, API를 통해 명령을 주고받는 클라이언트-서버 아키텍처(Client-Server Architecture)죠.
    • Podman (팟맨): Red Hat에서 주도적으로 개발한 컨테이너 런타임입니다. 가장 큰 특징은 데몬리스(Daemonless)라는 점이에요. 백그라운드 데몬 없이 필요한 시점에만 프로세스가 실행되고, 컨테이너를 직접 생성하고 관리합니다. 그리고 루트리스(Rootless) 모드를 기본적으로 지원해서, 일반 사용자 계정으로도 컨테이너를 실행할 수 있다는 게 강력한 장점이거든요.

    더 자세한 차이점을 표로 정리해볼게요.

    특징 Docker Podman
    데몬 유무 ✅ 데몬(dockerd) 필요 ❌ 데몬 없음 (필요 시 직접 실행)
    루트 권한 기본적으로 루트 권한 필요 (데몬) ✅ 기본 루트리스(Rootless) 지원
    Kubernetes(쿠버네티스) 호환성 Docker Swarm, Docker Compose ✅ Pods 지원 (podman play kube)
    CLI 명령어 docker ... podman ... (Docker와 유사)
    컨테이너 빌드 docker build podman build (Buildah 사용)
    멀티 컨테이너 오케스트레이션 docker-compose podman-compose (별도 설치), podman play kube

    홈랩에서 Podman과 Docker 직접 설치하고 써보기

    제 홈랩 서버는 Ubuntu 22.04 LTS를 사용하고 있습니다. 일반적으로 많이 쓰시는 환경이라 이 기준으로 설명드릴게요.

    1. Docker 설치 및 기본 사용

    Docker는 워낙 유명하니 설치는 비교적 간단합니다. 공식 문서에 따라 진행하면 되죠.

    
    # 기존 Docker 버전 제거 (선택 사항)
    sudo apt-get remove docker docker-engine docker.io containerd runc
    
    # 필수 패키지 설치
    sudo apt-get update
    sudo apt-get install ca-certificates curl gnupg lsb-release
    
    # Docker 공식 GPG 키 추가
    sudo mkdir -p /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    
    # Docker APT 저장소 설정
    echo \
      "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
      $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    
    # Docker Engine 설치
    sudo apt-get update
    sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
    
    # 사용자 계정을 docker 그룹에 추가 (sudo 없이 docker 명령어 사용)
    sudo usermod -aG docker $USER
    newgrp docker # 변경사항 즉시 적용
    
    # Docker 서비스 확인
    systemctl status docker
    
    # Nginx 컨테이너 실행 예시
    docker run -d --name my-nginx -p 8080:80 nginx:latest
    docker ps
    

    설치 후에는 docker run, docker ps 같은 명령어로 컨테이너를 쉽게 관리할 수 있어요. 익숙한 명령어들이죠? docker-compose도 플러그인 형태로 같이 설치되니 멀티 컨테이너 환경 구성에도 편리합니다.

    2. Podman 설치 및 기본 사용 (그리고 Rootless!)

    Podman도 Ubuntu에서는 APT 저장소를 통해 쉽게 설치할 수 있어요.

    
    # Podman 설치
    sudo apt-get update
    sudo apt-get install podman
    
    # Podman 버전 확인
    podman --version
    
    # Nginx 컨테이너 실행 예시 (루트리스 모드)
    podman run -d --name my-nginx-podman -p 8081:80 nginx:latest
    podman ps
    
    # Podman 서비스 활성화 (원격 API나 systemd 통합을 위해)
    # 이 명령은 rootless 모드에서 사용자별 systemd 서비스를 활성화합니다.
    systemctl --user enable --now podman.socket
    
    # Docker CLI처럼 원격으로 Podman 데몬에 연결하는 방법 (선택 사항)
    # DOCKER_HOST 환경 변수를 설정하면 docker CLI 명령어를 podman에 연결할 수 있습니다.
    # export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock
    # docker ps # 이제 podman 컨테이너를 보여줍니다 (docker CLI 사용)
    

    루트리스 모드(Rootless Mode)가 Podman의 가장 큰 매력 중 하나라고 했잖아요? 위 예시처럼 그냥 podman run을 실행하면 기본적으로 현재 사용자 계정의 권한으로 컨테이너가 실행돼요. 이게 진짜 편하더라고요. 보안 측면에서도 훨씬 안전하고요. 만약 컨테이너에서 어떤 취약점이 발견되더라도, 호스트 시스템의 루트 권한을 직접적으로 탈취하기가 훨씬 어려워지거든요.

    Podman rootless 모드로 컨테이너를 실행하고 확인하는 터미널 화면

    Podman으로 rootless 컨테이너를 실행하고 터미널에서 확인하는 화면입니다. user namespace가 잘 설정되었음을 보여줍니다.

    3. 멀티 컨테이너 오케스트레이션: Docker Compose vs Podman Play Kube

    여러 컨테이너를 함께 띄워야 할 때 Docker Compose(도커 컴포즈)는 정말 유용해요. 하나의 docker-compose.yaml 파일로 서비스들을 정의하고 한 번에 관리할 수 있죠. Podman에서는 비슷한 역할을 하는 podman-compose라는 툴도 있지만, 저는 podman play kube를 주로 사용해봤습니다. YAML 파일을 Kubernetes(쿠버네티스) 형식으로 작성해서 Podman으로 실행하는 방식인데, 미래를 생각하면 이쪽이 더 좋겠다 싶더라고요.

    
    # docker-compose.yaml 예시
    version: '3.8'
    services:
      web:
        image: nginx:latest
        ports:
          - "80:80"
        volumes:
          - ./nginx.conf:/etc/nginx/nginx.conf
      db:
        image: postgres:13
        environment:
          POSTGRES_DB: mydb
          POSTGRES_USER: user
          POSTGRES_PASSWORD: password
    
    
    # Docker Compose 실행
    docker-compose up -d
    
    # Podman Play Kube를 위한 YAML 파일 (Kubernetes Pod 정의)
    # web-pod.yaml 예시
    apiVersion: v1
    kind: Pod
    metadata:
      name: my-web-pod
    spec:
      containers:
        - name: nginx-container
          image: nginx:latest
          ports:
            - containerPort: 80
              hostPort: 8082 # Podman에서는 hostPort를 명시해야 외부 노출
        - name: postgres-container
          image: postgres:13
          env:
            - name: POSTGRES_DB
              value: mydb
            - name: POSTGRES_USER
              value: user
            - name: POSTGRES_PASSWORD
              value: password
    
    
    # Podman Play Kube 실행
    podman play kube web-pod.yaml
    podman ps -a
    

    podman play kube는 Kubernetes의 Pod(파드) 개념을 Podman 환경에서 그대로 사용할 수 있게 해줍니다. 나중에 Kubernetes로 전환할 계획이 있다면, 미리 익숙해질 수 있는 좋은 기회가 되죠.

    ⚠️ 삽질 경험: Podman rootless 모드와 포트 바인딩

    제가 Podman을 처음 쓰면서 가장 삽질했던 부분이 바로 루트리스 모드에서의 포트 바인딩(Port Binding)이었어요. podman run -p 80:80 nginx 처럼 80번 같은 특권 포트(Privileged Port, 1024번 이하)를 바인딩하려고 하면 에러가 나더라고요. 처음엔 이게 뭔가 싶었는데, 일반 사용자 계정은 1024번 이하 포트에 바인딩할 권한이 없기 때문이었습니다.

    해결책:

    1. 비특권 포트 사용: 가장 간단한 방법은 8080, 8000처럼 1024번 이상의 비특권 포트(Non-privileged Port)를 사용하는 거예요. podman run -p 8080:80 nginx 이렇게요. 홈랩에서는 보통 이 방법으로 충분합니다.
    2. rootful Podman 사용: 정말 특권 포트를 써야 한다면 sudo podman run -p 80:80 nginx 처럼 루트 권한으로 실행할 수도 있어요. 하지만 이는 루트리스 모드의 장점을 포기하는 것이 되니, 특별한 경우가 아니면 권장하지 않습니다.
    3. sysctl 설정 (주의 필요!): 극히 드물지만, 일반 사용자도 특권 포트를 사용할 수 있도록 시스템 설정을 변경하는 방법도 있어요. sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80 와 같은 명령어를 사용하는데, 보안상 매우 위험하니 절대 따라 하지 마세요. 제 삽질 경험을 공유하는 차원에서만 언급합니다.

    이 외에도 Podman이 systemd와 통합되는 방식이나, SELinux(에스이리눅스)가 활성화된 환경에서는 추가적인 설정이 필요할 수 있어요. 로그를 확인할 때는 journalctl --user -xeu podman.socket 처럼 사용자별 systemd 저널을 보는 것이 도움이 됩니다.

    성능 비교 및 관리 용이성

    자, 그럼 가장 중요한 부분이죠. 성능과 관리 용이성은 어땠을까요? 제가 직접 여러 컨테이너를 띄워놓고 htop, sar, iostat 같은 도구들로 모니터링해봤습니다.

    성능

    솔직히 말하면, 제 홈랩 환경(저사양 미니 PC)에서는 Podman과 Docker 간의 드라마틱한 성능 차이를 벤치마크 수치로 딱 잘라 말하기는 어려웠어요. 하지만 체감적으로 Podman이 약간 더 가볍게 느껴지는 부분은 있었습니다. 특히 시스템 리소스가 제한적인 홈랩 환경에서는 이 작은 차이가 중요할 수 있더라고요.

    • 메모리 사용량 (Memory Usage): Docker는 dockerd라는 데몬이 항상 일정량의 메모리를 점유하고 있어요. Podman은 데몬이 없으니 이 오버헤드(Overhead)가 없습니다. 여러 개의 컨테이너를 띄우지 않는다면 큰 차이가 아닐 수 있지만, 저처럼 컨테이너를 수십 개씩 띄우는 홈랩에서는 무시할 수 없는 부분입니다.
    • CPU 사용량 (CPU Usage): 마찬가지로 데몬의 존재 유무가 CPU 사용량에도 영향을 미칠 수 있어요. 특히 유휴 상태(Idle State)일 때 Podman이 미세하게 더 낮은 CPU 사용률을 보였습니다.
    • I/O 성능 (Disk I/O): 이 부분에서는 큰 차이를 느끼기 어려웠어요. 컨테이너 런타임보다는 기본 스토리지 드라이버나 실제 디스크 성능에 더 큰 영향을 받더라고요.

    결과 해석 기준: 만약 htop에서 `dockerd` 프로세스가 지속적으로 1% 이상의 CPU를 점유하거나, `top` 명령어로 확인했을 때 `VIRT` (Virtual Memory Size) 값이 수백 MB 이상으로 높게 나온다면, 데몬으로 인한 오버헤드를 고려해볼 만해요. 홈랩처럼 리소스가 제한된 환경에서는 이런 작은 수치들이 전체 시스템 안정성에 영향을 줄 수 있거든요.

    홈랩 환경에서 Podman과 Docker 컨테이너의 리소스 사용량을 비교하는 대시보드

    홈랩 환경에서 Podman과 Docker 컨테이너의 CPU, 메모리, 디스크 I/O 사용량을 비교하는 대시보드 스크린샷입니다. Prometheus, Grafana 같은 툴을 활용한 시각화를 보여줍니다.

    관리 용이성

    • 명령어 호환성: Podman은 Docker CLI 명령어와 거의 100% 호환돼요. docker run 대신 podman run이라고만 바꾸면 되니, 기존 Docker 사용자들이 쉽게 적응할 수 있습니다. 이게 진짜 편하더라고요.
    • systemd 통합: Podman은 systemd와 아주 잘 통합돼요. 컨테이너를 systemd 서비스로 등록해서 자동으로 시작하고 관리할 수 있죠. podman generate systemd --name my-nginx-podman > ~/.config/systemd/user/my-nginx-podman.service 이런 식으로 쉽게 서비스 파일을 만들 수 있어서, 서버 재부팅 시 컨테이너를 수동으로 다시 띄울 필요가 없어집니다.
    • Kubernetes 친화적: podman play kube 기능을 통해 Kubernetes YAML 파일을 직접 실행할 수 있다는 점은 향후 클라우드 환경이나 복잡한 오케스트레이션으로 넘어갈 때 큰 강점이 돼요.

    그래서, 홈랩에는 어떤 컨테이너 런타임이 좋을까요?

    저의 13년차 인프라 엔지니어이자 홈랩 운영자로서의 경험을 바탕으로 명확한 결론과 추천을 드려볼게요.

    • Docker를 추천하는 경우:
      • 이미 Docker에 익숙하고, 복잡한 설정 변경을 원치 않는다면: 기존 Docker Compose 파일들이 많고, 커뮤니티 자료나 플러그인 생태계를 그대로 활용하고 싶다면 Docker가 여전히 좋은 선택이에요.
      • Docker Swarm(도커 스웜)을 활용한 간단한 클러스터가 필요하다면: Podman은 Swarm을 직접 지원하지 않아요.
      • 상대적으로 고사양의 서버를 사용하고 데몬 오버헤드가 크게 문제 되지 않는다면: 데몬의 안정성과 광범위한 사용 사례를 그대로 가져갈 수 있습니다.
    • Podman을 추천하는 경우:
      • 보안을 최우선으로 생각하고, 루트리스 컨테이너를 선호한다면: 홈랩 환경에서 보안은 아무리 강조해도 지나치지 않아요. 루트리스 모드는 잠재적인 보안 위협을 크게 줄여줍니다.
      • 시스템 리소스가 제한적인 저사양 홈랩 서버를 운영한다면: 데몬 오버헤드가 없기 때문에 미세하게라도 자원을 더 효율적으로 사용할 수 있어요.
      • 향후 Kubernetes 환경으로 확장할 계획이 있다면: podman play kube를 통해 Kubernetes YAML 파일에 익숙해질 수 있고, Pod 개념을 미리 체험해볼 수 있습니다.
      • systemd와의 긴밀한 통합을 통해 컨테이너 자동화를 하고 싶다면: Podman의 systemd 통합은 컨테이너 관리를 한층 더 편리하게 만들어줍니다.
    Podman과 Docker의 장단점과 홈랩에서의 선택 기준을 요약한 인포그래픽

    Podman과 Docker의 주요 장단점을 비교하고, 홈랩 환경에 적합한 선택 기준을 시각적으로 요약한 인포그래픽입니다.

    개인적으로는 요즘 Podman에 손이 더 많이 갑니다. 특히 루트리스 모드와 systemd 통합이 저에게는 굉장히 매력적이더라고요. 제 홈랩 서버가 그렇게 고사양은 아니라서 리소스 효율성도 무시할 수 없고요. 물론 Docker가 가진 강력한 생태계와 안정성은 여전히 큰 장점이지만, 새로운 기술을 시도하고 실험해보는 홈랩의 재미를 생각하면 Podman은 충분히 도전해볼 가치가 있는 툴입니다.

    여러분도 이 글을 통해 자신의 홈랩 환경에 맞는 컨테이너 런타임을 선택하는 데 도움이 되셨으면 좋겠어요. 다음 글에서는 Podman으로 Nginx와 Let’s Encrypt를 연동해서 HTTPS 웹서버를 구축하는 방법에 대해 더 자세히 다뤄볼 예정이니 기대해주세요! 그때 또 다른 삽질 경험담과 해결책을 공유해드리겠습니다. 🎉

  • [Cloud] OIDC와 SSO를 활용한 사내 인증 시스템 통합 전략 분석

    [Cloud] OIDC와 SSO를 활용한 사내 인증 시스템 통합 전략 분석

    안녕하세요, 13년차의 서버실 주인장입니다. 오늘은 인프라 엔지니어라면 한 번쯤은 고민하고, 또 삽질하게 되는 주제, 바로 OIDC(OpenID Connect)와 SSO(Single Sign-On)를 활용한 사내 인증 시스템 통합 전략에 대해 이야기해보려고 합니다. 사내에 시스템이 한두 개가 아닐 텐데, 매번 다른 아이디와 비밀번호로 로그인하는 게 여간 귀찮은 일이 아니죠. 저도 처음엔 ‘그냥 다 개별적으로 관리하면 되지 뭐’라고 안일하게 생각했었는데, 시스템이 늘어날수록 사용자들의 불만이 폭주하더라고요. ‘로그인만 수십 번 하는 것 같다’는 피드백을 들었을 때는 정말 등골이 오싹했었습니다. 😱

    이런 불편함을 해소하고 보안 강화는 물론, 사용자 경험 개선까지 동시에 잡을 수 있는 방법이 바로 SSO, 그리고 그 핵심에 있는 OIDC입니다. 저도 저희 회사 시스템들을 OIDC 기반으로 통합하면서 정말 많은 삽질을 했거든요. 오늘은 그 경험을 바탕으로 OIDC와 SSO가 무엇인지, 어떻게 통합해야 하는지, 그리고 어떤 문제들을 만날 수 있는지 솔직하게 풀어보겠습니다.

    OIDC SSO 사내 인증 시스템 통합 아키텍처 다이어그램

    OIDC와 SSO를 활용한 사내 인증 시스템 통합의 전체 아키텍처 다이어그램입니다. 사용자가 SSO 포털을 통해 OIDC Provider에 인증하고, 여러 서비스에 접근하는 흐름을 한눈에 볼 수 있습니다.

    SSO와 OIDC, 이 둘은 대체 뭘까요?

    먼저 핵심 개념들을 쉽고 명확하게 정리해 볼게요. 저도 처음엔 OAuth랑 OIDC가 뭐가 다른지 헷갈려서 엄청 찾아봤거든요. 간단히 정리해볼게요.

    SSO (Single Sign-On): 한 번의 로그인으로 만사형통!

    SSO(Single Sign-On, 싱글 사인온)는 말 그대로 한 번의 인증(로그인)으로 여러 개의 서로 다른 애플리케이션이나 서비스에 접근할 수 있도록 해주는 인증 방식입니다. 예를 들어, 회사 포털에 한 번 로그인하면 그룹웨어, 메신저, 인사 시스템 등 모든 사내 시스템에 추가 로그인 없이 바로 접근할 수 있는 거죠. 사용자 입장에서는 정말 편리하고, 관리자 입장에서는 사용자 계정 관리가 훨씬 수월해집니다. 💡

    OAuth 2.0 (Open Authorization): 권한 위임의 대명사

    OAuth 2.0(Open Authorization)은 인증(Authentication) 프로토콜이 아닙니다. 정확히는 권한 위임(Authorization Delegation)을 위한 프레임워크라고 보시면 됩니다. ‘나’라는 사용자가 ‘서비스 A’에게 ‘서비스 B’에 있는 내 정보에 접근할 수 있는 권한을 위임할 때 사용됩니다. “카카오톡으로 로그인”이나 “구글로 로그인” 버튼을 눌렀을 때, 서비스 제공자가 내 개인 정보에 접근할 권한을 얻는 과정이 바로 OAuth 2.0을 통해 이루어집니다. 중요한 건, OAuth 2.0은 ‘누가 로그인했는지(인증)’보다는 ‘무엇에 접근할 수 있는지(권한)’에 초점을 맞춘다는 거죠.

    OIDC (OpenID Connect): OAuth 2.0 위에서 동작하는 인증 프로토콜

    드디어 오늘의 주인공, OIDC(OpenID Connect)입니다. OIDC는 OAuth 2.0 프레임워크 위에서 동작하는 인증(Authentication) 계층입니다. OAuth 2.0이 권한 위임에 집중했다면, OIDC는 ‘로그인한 사용자가 누구인지’를 확인하는 데 집중합니다. 사용자 정보를 담은 ID Token(아이디 토큰)을 발행하여, 클라이언트 애플리케이션이 사용자를 식별하고 인증할 수 있도록 해줍니다. 즉, OAuth 2.0은 “문 열어줄 권한”을 주고, OIDC는 “문 열고 들어온 사람이 누구인지 신분증 확인”까지 해준다고 생각하시면 쉽습니다.

    OAuth 2.0 vs OIDC 비교

    특징 OAuth 2.0 OIDC (OpenID Connect)
    목적 권한 위임 (Authorization) 사용자 인증 (Authentication)
    기반 기술 인증/인가를 위한 프레임워크 OAuth 2.0 위에 구축된 프로토콜
    주요 토큰 Access Token (접근 토큰) ID Token (아이디 토큰), Access Token
    제공 정보 자원 접근 권한 사용자 신원 정보 (이름, 이메일 등)
    활용 예시 타사 앱에서 내 Google Drive 접근 Google, Kakao로 웹사이트 로그인

    OIDC는 OAuth 2.0의 유연한 구조를 활용하면서도, 표준화된 방식으로 사용자 인증 정보를 제공한다는 점에서 큰 장점이 있습니다.

    OIDC 기반 SSO, 실전 구현 삽질기

    자, 이제 개념은 잡혔으니 실제로 어떻게 사내 시스템에 OIDC SSO 통합을 적용했는지 이야기해 볼까요? 저는 주로 Keycloak 같은 오픈소스 OIDC Provider를 활용하는 편입니다. 홈랩에서도 자주 써봤거든요. 여기서는 OIDC Provider(Keycloak 등)가 이미 구축되어 있고, 우리가 만든 서비스(Client Application)에 연동하는 과정을 중심으로 설명하겠습니다.

    1단계: OIDC Provider에 클라이언트 등록

    가장 먼저 할 일은 OIDC Provider에 우리 서비스(Client Application)를 클라이언트(Client)로 등록하는 겁니다. 이때 몇 가지 중요한 정보를 입력해야 해요.

    • Client ID (클라이언트 ID): 우리 서비스를 식별하는 고유한 ID입니다.
    • Client Secret (클라이언트 시크릿): 클라이언트의 비밀 키로, 보안상 매우 중요합니다. 절대 외부에 노출되면 안 돼요!
    • Redirect URI (리다이렉트 URI): 인증이 성공했을 때 OIDC Provider가 사용자 에이전트(브라우저)를 다시 보낼 URL입니다. 이 URL은 정확해야 합니다. 제가 초반에 제일 삽질 많이 했던 부분이 이 리다이렉트 URI 오타나 불일치였어요. ⚠️
    • Scope (스코프): 클라이언트가 사용자 정보 중 어떤 항목에 접근할 권한을 요청할지 정의합니다. 최소한 openid 스코프는 필수입니다. 보통 profile, email 등도 함께 요청하죠.
    OIDC Provider 클라이언트 등록 설정 화면

    OIDC Provider(예: Keycloak)에 새로운 클라이언트를 등록하는 설정 화면입니다. Client ID, Client Secret, Redirect URI, Scope 등을 정확히 입력해야 합니다. 특히 Redirect URI는 오타 없이 정확하게 입력하는 것이 중요합니다.

    2단계: 서비스 애플리케이션에 OIDC 클라이언트 연동

    이제 우리 서비스 애플리케이션에서 OIDC Provider와 통신할 차례입니다. 저는 웹 서비스 앞단에 Nginx를 두고 OIDC 모듈을 연동하거나, 직접 애플리케이션 코드에 OIDC 클라이언트 라이브러리를 사용하는 방식을 선호합니다. 여기서는 Nginx를 이용한 간단한 연동 예시와 함께, OIDC 플로우의 핵심을 보여드릴게요.

    Nginx OIDC 모듈을 사용한 연동 예시

    Nginx를 리버스 프록시(Reverse Proxy)로 사용하고, nginx-auth-oidc 같은 모듈을 활용하면 애플리케이션 코드 수정 없이도 OIDC 인증을 붙일 수 있습니다. 정말 편리하더라고요.

    
    # nginx.conf 예시 (http 블록 내 server 블록)
    
    server {
        listen 80;
        server_name your.service.com;
    
        # OIDC 설정 블록
        location / {
            # OIDC Provider의 Well-Known Configuration 엔드포인트
            # 예: https://idp.example.com/auth/realms/your-realm/.well-known/openid-configuration
            auth_request /_oauth2_proxy_auth; # 이 경로는 실제 인증 프록시 엔드포인트와 맞춰야 합니다.
            error_page 401 = /_oauth2_proxy_auth; # 401 에러 발생 시 인증 프록시로 리다이렉트
    
            # 인증이 성공하면 애플리케이션으로 트래픽 전달
            proxy_pass http://your_backend_service;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    
        # OIDC 인증 프록시 설정 (이 부분은 실제 OIDC 모듈 설정에 따라 달라질 수 있습니다)
        location = /_oauth2_proxy_auth {
            internal; # 외부에서 직접 접근 불가
            proxy_pass http://localhost:PORT/auth; # 실제 OIDC 인증 프록시 서버 주소
            proxy_pass_request_body off;
            proxy_set_header Content-Length "";
            proxy_set_header X-Original-URI $request_uri;
            # 여기에 OIDC Client ID, Secret, Redirect URI 등의 환경 변수나 설정 파일 경로 지정
            # 예: proxy_set_header X-OIDC-Client-ID "your_client_id";
            # proxy_set_header X-OIDC-Client-Secret "your_client_secret";
            # proxy_set_header X-OIDC-Redirect-URI "https://your.service.com/oauth2/callback";
        }
    
        # OIDC 콜백 엔드포인트
        location = /oauth2/callback {
            proxy_pass http://localhost:PORT/callback; # OIDC 인증 프록시 콜백 엔드포인트
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    

    위 Nginx 설정은 개념적인 예시이며, 실제 nginx-auth-oidc 모듈이나 oauth2_proxy 같은 솔루션을 사용할 경우 설정 방식이 조금 다를 수 있습니다. 핵심은 사용자가 보호된 리소스에 접근하려 할 때, Nginx가 이를 가로채 OIDC Provider로 인증을 위임하고, 인증이 완료되면 원래 요청을 백엔드 서비스로 전달하는 흐름입니다.

    OIDC 플로우 간략 설명 (Authorization Code Flow 기준)

    1. 사용자가 보호된 리소스(예: your.service.com/dashboard)에 접근합니다.
    2. 서비스 애플리케이션(또는 Nginx 프록시)은 사용자가 인증되지 않았음을 감지하고, OIDC Provider의 Authorization Endpoint(인가 엔드포인트)로 사용자를 리다이렉트합니다. 이때 Client ID, Redirect URI, Scope 등을 쿼리 파라미터로 넘깁니다.
    3. 사용자는 OIDC Provider 로그인 페이지에서 아이디/비밀번호를 입력하여 인증합니다.
    4. 인증 성공 시, OIDC Provider는 미리 등록된 Redirect URI로 Authorization Code(인가 코드)를 포함하여 사용자를 다시 우리 서비스로 리다이렉트합니다.
    5. 우리 서비스는 받은 Authorization Code를 이용해 OIDC Provider의 Token Endpoint(토큰 엔드포인트)로 Access Token과 ID Token을 요청합니다. 이때 Client Secret도 함께 전송하여 클라이언트의 신원을 증명합니다.
    6. OIDC Provider는 Access Token과 ID Token을 발급해줍니다.
    7. 우리 서비스는 ID Token을 검증하여 사용자 신원을 확인하고, Access Token을 사용하여 필요한 경우 사용자 정보(Userinfo Endpoint)를 추가로 조회하거나, 다른 리소스에 접근할 수 있습니다.
    8. 사용자는 이제 서비스에 로그인된 상태가 됩니다. 🎉

    주의사항과 트러블슈팅: 삽질은 나의 힘!

    이런 시스템 통합 작업은 늘 예상치 못한 문제의 연속이더라고요. 제가 겪었던 대표적인 삽질 경험과 해결책을 공유합니다.

    • ⚠️ Redirect URI 불일치: OIDC Provider에 등록된 Redirect URI와 실제 요청하는 URI가 단 한 글자라도 다르면 인증 플로우가 진행되지 않습니다. 심지어 http와 https, 마지막 슬래시 / 유무까지 정확히 일치해야 합니다.

      해결책: OIDC Provider 설정 화면과 애플리케이션 코드/설정 파일을 꼼꼼히 대조하고, 브라우저 개발자 도구(F12)의 네트워크 탭에서 실제 리다이렉트되는 URL을 확인하며 디버깅하세요.

      
              # curl 명령어로 .well-known/openid-configuration 확인하여 정확한 endpoint 정보 파악
              curl -s https://idp.example.com/auth/realms/your-realm/.well-known/openid-configuration | jq .
              

      이 명령어를 통해 OIDC Provider가 제공하는 정확한 엔드포인트와 정보들을 확인할 수 있습니다. 특히 authorization_endpoint, token_endpoint, jwks_uri 등을 잘 봐야 합니다.

    • ⚠️ Scope/Claim 누락: 필요한 사용자 정보(예: 이메일, 이름)가 ID Token이나 Userinfo Endpoint에서 넘어오지 않을 때가 있습니다. 이는 보통 클라이언트 등록 시 요청하는 Scope(스코프)가 부족하거나, OIDC Provider에서 해당 Claim(클레임)을 발행하도록 설정되지 않았기 때문입니다.

      해결책: 클라이언트 등록 시 openid profile email 등 필요한 스코프를 모두 요청했는지 확인하고, OIDC Provider의 사용자 속성(User Attributes) 또는 클레임 매핑 설정을 확인하세요.

    • ⚠️ Token 검증 실패: ID Token의 Signature(서명) 검증에 실패하거나, 유효 기간(exp 클레임)이 만료된 토큰을 사용하는 경우가 있습니다. 이는 주로 OIDC Provider의 JWKS(JSON Web Key Set) 엔드포인트 설정 오류, 서버 시간 동기화 문제, 또는 토큰 캐싱 문제에서 발생합니다.

      해결책: OIDC Provider의 JWKS URI가 정확한지 확인하고, 애플리케이션 서버의 시간이 NTP 등으로 정확하게 동기화되어 있는지 확인하세요. 토큰을 캐싱한다면 만료 시간을 고려해야 합니다.

    • ⚠️ Client Secret 노출 위험: Client Secret은 정말 중요합니다. 절대 하드코딩하거나 Git 저장소에 올리면 안 돼요.

      해결책: 환경 변수(Environment Variable), HashiCorp Vault 같은 Secrets Management 솔루션, 또는 Kubernetes Secret 등을 활용하여 안전하게 관리해야 합니다.

      
              # 환경 변수로 Client Secret 설정 예시 (애플리케이션 실행 시)
              export OIDC_CLIENT_SECRET="your_very_secret_key"
              python your_app.py
              

      이런 방식으로 서버 시작 시에만 접근하도록 하는 것이 안전합니다.

    검증 및 결과 확인: 드디어 로그인 성공!

    위의 삽질들을 거쳐서 드디어 OIDC 기반 SSO가 정상적으로 동작하는 것을 확인했습니다. 보통 다음과 같은 방법으로 검증합니다.

    1. 서비스 접근 및 로그인 시도: 웹 브라우저로 서비스 URL에 접속했을 때, OIDC Provider의 로그인 페이지로 정확히 리다이렉트되는지 확인합니다.
    2. 로그인 후 서비스 접근: 로그인 성공 후, 서비스의 메인 페이지나 보호된 리소스에 정상적으로 접근되는지 확인합니다.
    3. ID Token 및 Access Token 내용 확인: 개발자 도구 또는 애플리케이션 로그를 통해 발급된 ID Token(JWT 형식)을 jwt.io 같은 사이트에서 디코딩하여, 사용자 정보(sub, name, email 등)가 올바르게 포함되어 있는지 확인합니다. Access Token은 불투명할 수 있지만, OIDC Provider의 Userinfo Endpoint를 통해 사용자 정보를 가져올 때 사용됩니다.
    4. 세션 관리 확인: 한 번 로그인하면 다른 사내 시스템(OIDC Client로 등록된)에 추가 로그인 없이 접근되는지 확인합니다.
    OIDC SSO 로그인 성공 및 JWT 토큰 디코딩 결과 화면

    OIDC 기반 SSO 로그인 성공 후, 사용자 정보가 표시되는 웹 애플리케이션 대시보드와 JWT 토큰 디코딩 결과 화면입니다. 토큰에 담긴 사용자 정보가 정확한지 확인하는 과정이 중요합니다.

    마무리: OIDC SSO, 선택이 아닌 필수

    OIDC와 SSO를 사내 시스템에 통합하는 과정은 결코 쉽지 않습니다. 저도 수많은 시행착오와 삽질을 거쳤고, 밤샘 디버깅도 여러 번 했거든요. 하지만 고생 끝에 낙이 온다고, 제대로 구축된 OIDC 기반 SSO는 사용자 경험 개선, 보안 강화, 그리고 중앙 집중식 계정 관리라는 세 마리 토끼를 동시에 잡을 수 있게 해줍니다.

    OIDC 기반 SSO 통합의 주요 장점

    장점 설명
    사용자 경험 개선 한 번 로그인으로 여러 시스템 접근, 로그인 피로도 감소
    보안 강화 중앙 집중식 인증으로 보안 정책 일관성 유지, 2FA(다단계 인증) 적용 용이
    관리 효율성 증대 계정 생성/삭제/관리의 단일화, 감사(Audit) 용이
    표준화된 프로토콜 다양한 서비스 및 솔루션과의 호환성 보장

    이러한 장점들은 OIDC 기반 SSO가 단순한 편의 기능을 넘어, 현대 IT 인프라에서 필수적인 요소가 되었음을 보여줍니다.

    OIDC SSO 통합 주요 장점 인포그래픽

    OIDC 기반 SSO의 주요 장점을 요약한 인포그래픽입니다. 사용자 경험 개선부터 보안 강화, 관리 효율성 증대까지 다양한 이점을 명확하게 보여줍니다.

    만약 여러분의 사내 시스템이 아직도 각자도생의 길을 걷고 있다면, 지금 바로 OIDC 기반 SSO 통합을 진지하게 고민해 보시길 강력히 권해드립니다. 처음엔 어렵겠지만, 장기적으로는 훨씬 더 견고하고 효율적인 인프라를 만들 수 있을 거예요. 다음번에는 OIDC와 연동하여 RBAC(Role-Based Access Control, 역할 기반 접근 제어)를 어떻게 구현하는지에 대한 경험담을 풀어볼까 합니다. 궁금하신 점이 있다면 언제든 댓글 남겨주세요! 😉

  • [Cloud] Cosmos DB 마이그레이션 가이드: 기존 DB에서 NoSQL로 안전하게 전환하기

    [Cloud] Cosmos DB 마이그레이션 가이드: 기존 DB에서 NoSQL로 안전하게 전환하기

    안녕하세요, 13년차 인프라 엔지니어입니다. 오늘은 저처럼 관계형 데이터베이스(Relational Database) 환경에 익숙한 분들이 NoSQL로 전환할 때 겪는 Cosmos DB 마이그레이션 경험담을 풀어볼까 합니다. 특히 기존 DB에서 NoSQL로 넘어갈 때 마주치는 문제들과 제가 삽질 끝에 찾아낸 해결 방안들을 여과 없이 공유하려고 해요.

    최근 서비스가 빠르게 성장하면서 기존 RDB로는 감당하기 어려운 트래픽과 데이터 규모에 직면하는 경우가 많아졌습니다. 저도 직접 운영하는 서비스에서 비슷한 고민을 했거든요. 이럴 때 확장성과 유연성이 뛰어난 NoSQL 데이터베이스, 특히 Azure Cosmos DB 같은 글로벌 분산 데이터베이스가 매력적인 대안으로 떠오릅니다. 하지만 익숙한 환경을 떠나 새로운 패러다임으로 넘어가는 건 결코 쉽지 않죠. 😅

    기존 관계형 데이터베이스에서 Azure Cosmos DB로 마이그레이션하는 전체 아키텍처 개요도

    Cosmos DB와 NoSQL 마이그레이션, 왜 필요한가?

    Cosmos DB는 Microsoft Azure에서 제공하는 완전 관리형(fully managed) NoSQL 데이터베이스 서비스입니다. 전 세계 어디서든 낮은 지연 시간(low-latency)으로 데이터를 읽고 쓸 수 있으며, 필요에 따라 자동으로 확장(auto-scale)되는 것이 가장 큰 장점이죠. 특히 여러 NoSQL API를 지원해서(SQL API, MongoDB API, Cassandra API, Gremlin API 등) 기존에 사용하던 NoSQL 시스템과 호환성을 유지하면서 Cosmos DB 마이그레이션을 진행할 수 있다는 점이 매력적입니다.

    그렇다면 왜 굳이 NoSQL 마이그레이션을 해야 할까요? 제가 직접 경험해보니 몇 가지 확실한 이유가 있더라고요.

    • 확장성 (Scalability): 폭발적으로 증가하는 데이터와 트래픽을 RDB의 수직 확장(vertical scaling)만으로는 감당하기 어렵습니다. NoSQL은 수평 확장(horizontal scaling)에 유리하죠.
    • 유연한 스키마 (Flexible Schema): RDB는 엄격한 스키마를 요구해서 데이터 모델 변경 시 복잡한 마이그레이션 작업이 필요합니다. NoSQL은 스키마가 유연해서 빠르게 변화하는 비즈니스 요구사항에 대응하기 좋습니다.
    • 글로벌 분산 (Global Distribution): 전 세계 사용자에게 서비스를 제공해야 할 때, Cosmos DB는 몇 번의 클릭만으로 전 세계 리전에 데이터를 복제하고 동기화할 수 있습니다.

    이런 장점들 때문에 데이터베이스 전환을 고민하게 되지만, 막상 시작하면 만만치 않은 벽에 부딪힙니다. 가장 큰 벽은 바로 데이터 모델링과 마이그레이션 전략 수립이더군요.

    마이그레이션 준비: 데이터 모델링이 반이다

    관계형 데이터베이스는 정규화(Normalization)를 통해 데이터 중복을 최소화하고 데이터 무결성을 유지합니다. 하지만 NoSQL은 읽기 성능(read performance)을 최적화하기 위해 비정규화(Denormalization)를 적극적으로 활용하는 경우가 많습니다. 이게 처음엔 정말 낯설더라고요.

    Cosmos DB에서는 파티션 키 (Partition Key) 선정이 정말 중요합니다. 파티션 키는 데이터를 논리적 파티션으로 나누는 기준이 되는데, 이 키를 어떻게 정하느냐에 따라 성능과 비용이 천차만별로 달라지거든요. 잘못 정하면 ‘핫 파티션(Hot Partition)’이 생겨서 특정 파티션에만 부하가 몰려 성능 저하를 겪게 됩니다. 제가 이 부분에서 정말 크게 삽질했어요. ㅎㅎ

    💡 팁: 파티션 키는 카디널리티(Cardinality, 고유한 값의 수)가 높고, 읽기/쓰기 작업이 고르게 분산될 수 있는 필드를 선택하는 것이 좋습니다. 예를 들어, 사용자 ID나 주문 ID 같은 필드가 좋은 후보가 될 수 있죠.

    실전 마이그레이션: 데이터 이동 전략

    이제 실제로 데이터를 옮겨볼 차례입니다. Cosmos DB로 데이터를 마이그레이션하는 방법은 여러 가지가 있습니다.

    1. Azure Cosmos DB Data Migration Tool: GUI 기반의 도구로, CSV, JSON 파일, MongoDB, SQL Server 등 다양한 소스에서 데이터를 가져올 수 있습니다. 간단한 초기 마이그레이션에 유용합니다.
    2. Azure Data Factory (ADF): 대규모 데이터 마이그레이션, ETL(Extract, Transform, Load) 파이프라인 구축에 적합합니다. 다양한 데이터 소스와 싱크를 지원하며, 데이터 변환 로직을 추가할 수 있습니다.
    3. 커스텀 스크립트: Python, .NET 등 SDK를 이용해 직접 스크립트를 작성하여 데이터를 마이그레이션하는 방식입니다. 복잡한 변환 로직이나 실시간 동기화가 필요할 때 유용하죠.

    저는 초기 마이그레이션에는 Data Migration Tool을 사용했지만, 복잡한 데이터 변환이 필요해서 결국 Python 스크립트를 직접 작성했습니다. 기존 관계형 DB에서 데이터를 읽어와 NoSQL 모델에 맞게 변환한 뒤 Cosmos DB에 삽입하는 방식이었거든요.

    먼저 Azure CLI를 이용해 Cosmos DB 계정과 데이터베이스, 컨테이너를 생성하는 기본적인 명령어부터 살펴보겠습니다.

    # Azure 리소스 그룹 생성 (이미 있다면 생략)
    az group create --name myCosmosResourceGroup --location koreacentral
    
    # Cosmos DB 계정 생성 (API 종류는 필요에 따라 선택, 여기서는 SQL API)
    az cosmosdb create \
      --name mycosmosdbaccount13year \
      --resource-group myCosmosResourceGroup \
      --default-consistency-level Session \
      --locations regionName=koreacentral failoverPriority=0 \
      --kind GlobalDocumentDB
    
    # 데이터베이스 생성
    az cosmosdb sql database create \
      --account-name mycosmosdbaccount13year \
      --name myNoSQLDatabase \
      --resource-group myCosmosResourceGroup
    
    # 컨테이너 생성 (파티션 키는 /userId 로 예시)
    az cosmosdb sql container create \
      --account-name mycosmosdbaccount13year \
      --database-name myNoSQLDatabase \
      --name myItemsContainer \
      --partition-key-path /userId \
      --throughput 400 \
      --resource-group myCosmosResourceGroup
    

    그리고 Python으로 데이터를 읽어와 삽입하는 예시 코드입니다. 실제 환경에서는 데이터 변환 로직이 훨씬 복잡하겠죠.

    import os
    import json
    from azure.cosmos import CosmosClient
    
    # Cosmos DB 설정
    COSMOS_DB_ENDPOINT = os.environ.get("COSMOS_DB_ENDPOINT")
    COSMOS_DB_KEY = os.environ.get("COSMOS_DB_KEY")
    DATABASE_NAME = "myNoSQLDatabase"
    CONTAINER_NAME = "myItemsContainer"
    
    # Cosmos DB 클라이언트 초기화
    client = CosmosClient(COSMOS_DB_ENDPOINT, COSMOS_DB_KEY)
    database = client.get_database_client(DATABASE_NAME)
    container = database.get_container_client(CONTAINER_NAME)
    
    def migrate_data(data_list):
        print(f"Migrating {len(data_list)} items...")
        for item in data_list:
            try:
                # 기존 RDB 데이터를 NoSQL 모델에 맞게 변환 (예시)
                # item_transformed = {"id": str(item["old_id"]), "userId": item["user_id"], "productName": item["name"], ...}
                # 실제 데이터 변환 로직은 여기에 구현
                item_transformed = item # 단순 복사 (예시)
                item_transformed["id"] = str(item_transformed["id"]) # Cosmos DB item id는 문자열이어야 함
    
                container.upsert_item(body=item_transformed)
                # print(f"Inserted/Updated item: {item_transformed['id']}")
            except Exception as e:
                print(f"Error processing item {item.get('id', 'N/A')}: {e}")
    
    if __name__ == "__main__":
        # 예시 데이터 (실제로는 RDB에서 조회)
        sample_data = [
            {"id": 1, "userId": "userA", "productName": "Laptop", "price": 1200},
            {"id": 2, "userId": "userB", "productName": "Mouse", "price": 50},
            {"id": 3, "userId": "userA", "productName": "Keyboard", "price": 100},
            {"id": 4, "userId": "userC", "productName": "Monitor", "price": 300},
        ]
        migrate_data(sample_data)
        print("Migration complete!")
    
    Python 스크립트를 활용한 데이터 마이그레이션 과정 개념도

    Python 스크립트를 활용한 데이터 마이그레이션 과정 개념도

    삽질 경험: 파티션 키 잘못 설계했다가 고생했던 이야기 ⚠️

    제가 가장 크게 삽질했던 부분이 바로 파티션 키 (Partition Key) 설계였습니다. 처음에는 무심코 productId를 파티션 키로 잡았거든요. 저희 서비스는 특정 인기 상품에 대한 조회수가 압도적으로 높았는데, 아니나 다를까 얼마 지나지 않아 productId가 높은 특정 파티션에만 요청이 몰려서 Latency(지연 시간)가 급증하고 Request Unit(RU) 소모가 비정상적으로 치솟는 문제가 발생했습니다. 이게 바로 핫 파티션 (Hot Partition) 문제였죠. 😱

    Cosmos DB는 RU라는 개념으로 처리량을 측정하고 비용을 부과하는데, 핫 파티션이 발생하면 특정 파티션의 RU가 한계치에 도달하여 요청이 제한(throttling)될 수 있습니다. 전체 컨테이너의 RU가 여유 있어도 특정 파티션만 과부하가 걸리는 상황이 발생하더라고요. 이때 정말 당황했었습니다.

    해결책은 파티션 키를 userId와 같이 고르게 분산될 수 있는 값으로 변경하는 것이었습니다. 기존 데이터를 다시 마이그레이션해야 하는 대규모 작업이었지만, 장기적인 안정성을 위해서는 필수적인 선택이었죠. 이 경험을 통해 파티션 키 설계가 얼마나 중요한지 뼈저리게 느꼈습니다.

    RU (Request Unit)는 Cosmos DB에서 모든 데이터베이스 작업(읽기, 쓰기, 쿼리 등)에 대한 성능 단위입니다. 1 RU는 1KB 크기의 항목을 읽는 비용과 같습니다. 복잡한 쿼리나 큰 항목을 쓰면 더 많은 RU가 소모되죠. RU 사용량을 모니터링하면서 핫 파티션이 있는지 주기적으로 확인하는 것이 중요합니다.

    마이그레이션 후 검증 및 최적화

    데이터 마이그레이션이 끝났다고 끝이 아닙니다. 오히려 이때부터가 진짜 시작이죠! 마이그레이션 후에는 반드시 다음 사항들을 꼼꼼하게 검증하고 최적화해야 합니다.

    1. 데이터 일관성 (Data Consistency): 원본 데이터와 Cosmos DB의 데이터가 일치하는지 확인해야 합니다. 간단한 스크립트를 통해 레코드 수, 일부 필드의 합계 등을 비교하는 방법이 효과적입니다.
    2. 성능 벤치마킹 (Performance Benchmarking): 예상했던 성능이 나오는지 부하 테스트를 진행해야 합니다. 특히 핵심 비즈니스 로직에 대한 읽기/쓰기 Latency와 RU 소모량을 면밀히 관찰해야 합니다.
    3. 비용 모니터링 (Cost Monitoring): Cosmos DB는 사용량 기반 과금(pay-as-you-go)이므로, 예상치 못한 RU 소모로 비용 폭탄을 맞지 않도록 모니터링 대시보드를 설정하는 것이 중요합니다. Azure Portal에서 RU 사용량을 실시간으로 확인할 수 있습니다.

    Azure Portal에서 Cosmos DB 계정의 ‘메트릭 (Metrics)’ 섹션을 보면 RU 사용량, 지연 시간, 요청 수 등을 그래프로 볼 수 있습니다. 여기서 특정 파티션 키에 대한 요청이 급증하는 패턴이 보인다면 핫 파티션을 의심해봐야 합니다.

    Azure Portal의 Cosmos DB RU 사용량 및 성능 메트릭 대시보드 예시

    Azure Portal의 Cosmos DB RU 사용량 및 성능 메트릭 대시보드 예시

    Cosmos DB 마이그레이션 전략 선택 가이드

    다양한 상황에 따라 마이그레이션 전략도 달라질 수 있습니다. 제가 경험했던 것들을 바탕으로 몇 가지 시나리오별 추천 전략을 정리해봤습니다.

    시나리오 고려 사항 추천 마이그레이션 전략 주의사항
    소규모 초기 마이그레이션
    (데이터 양 < 100GB)
    빠른 전환, 데이터 모델 단순 Azure Cosmos DB Data Migration Tool 대규모 데이터에는 부적합, 복잡한 변환 어려움
    대규모 데이터 마이그레이션
    (데이터 양 > 100GB, 복잡한 ETL 필요)
    데이터 변환, 증분 동기화 Azure Data Factory (ADF) 활용 ADF 파이프라인 설계 및 운영 복잡성 증가
    실시간/Near Real-time 동기화
    (서비스 중단 최소화)
    무중단 전환, 이중 쓰기(Dual-write) 전략 커스텀 스크립트 + Change Feed 활용 이중 쓰기 로직 구현 및 동기화 일관성 관리 복잡
    기존 NoSQL (예: MongoDB) 에서 전환 API 호환성, 스키마 유사성 MongoDB API for Cosmos DB + Mongo Shell 또는 Data Migration Tool 일부 기능/성능 차이 고려
    Cosmos DB 마이그레이션 전략 선택 가이드 인포그래픽

    Cosmos DB 마이그레이션 전략 선택 가이드 인포그래픽

    마무리: 새로운 패러다임으로의 전환

    Cosmos DB 마이그레이션은 단순한 데이터 이동을 넘어, 데이터베이스 패러다임을 전환하는 일입니다. 제가 직접 겪어보니, 기존 관계형 DB의 사고방식에서 벗어나 NoSQL의 특성과 장점을 이해하는 것이 가장 중요하더라고요. 특히 파티션 키 설계나 RU 최적화 같은 부분은 충분한 학습과 테스트 없이는 큰 시행착오를 겪을 수밖에 없습니다.

    하지만 제대로만 설계하고 구현한다면, Cosmos DB는 여러분의 서비스에 강력한 확장성과 유연성을 제공할 것입니다. 이 글이 기존 DB에서 NoSQL로의 전환을 고민하는 분들께 작은 도움이 되었으면 좋겠네요. 다음에 기회가 된다면 Cosmos DB의 Change Feed를 활용한 실시간 데이터 처리 아키텍처에 대해서도 한번 다뤄보겠습니다. 궁금한 점이 있다면 언제든 댓글로 남겨주세요! 😊

  • [Cloud] Zero Trust 아키텍처 도입 시 고려사항 및 보안 강화 전략

    [Cloud] Zero Trust 아키텍처 도입 시 고려사항 및 보안 강화 전략

    [인프라] Zero Trust 아키텍처 도입 시 고려사항 및 보안 강화 전략

    안녕하세요, 13년차 서버실 지킴이 ’13년차의 서버실’입니다. 오늘은 인프라 보안의 새로운 패러다임, Zero Trust 아키텍처에 대한 이야기를 해볼까 합니다.

    제가 처음 인프라 업무를 시작했을 때만 해도 보안은 흔히 ‘성벽과 해자(Moat and Castle)’ 모델이 대세였죠. 회사 내부망은 안전하고, 외부 위협만 잘 막으면 된다는 생각이었거든요. 그런데 클라우드 도입이 가속화되고, 원격 근무가 일상화되면서 이 경계가 무너져 버렸죠. 더 이상 ‘내부’라는 개념 자체가 모호해진 겁니다. 이로 인해 클라우드 보안의 새로운 패러다임이 필요해졌거든요. 저도 처음엔 ‘이게 뭔가’ 싶었는데, 직접 홈랩에 이것저것 적용해보면서 결국 Zero Trust가 답이라는 걸 깨달았습니다.

    혹시 아직도 ‘우리 회사 내부망은 안전할 거야’라고 막연히 믿고 계신가요? 그렇다면 지금이 바로 Zero Trust(제로 트러스트)를 고민해볼 때입니다. 저와 함께 Zero Trust 아키텍처의 핵심과 실전 도입 전략, 그리고 제가 겪었던 삽질 경험까지 솔직하게 공유해볼게요.

    Zero Trust 아키텍처의 핵심 원칙과 구성 요소를 보여주는 개요 다이어그램

    Zero Trust 아키텍처의 핵심 개념을 시각적으로 보여주는 다이어그램입니다. 모든 요소가 신뢰 없이 검증되는 과정을 나타냅니다.

    Zero Trust, 도대체 뭘까요? (쉽게 말해!)

    Zero Trust(제로 트러스트)는 말 그대로 ‘아무것도 신뢰하지 않는다(Never Trust)’는 원칙에서 시작합니다. 기존 보안 모델이 ‘내부는 안전하다’고 가정한 것과 달리, Zero Trust는 사용자, 디바이스, 애플리케이션 등 모든 접근 요청을 잠재적 위협으로 간주하고 항상 검증(Always Verify)하는 것이 핵심입니다. 쉽게 말해, ‘너 누구니? 뭘 하려 하니? 정말 해도 되는 거니?’ 하고 매번 꼼꼼히 물어보고 확인하는 방식이라고 보시면 되죠.

    Zero Trust의 세 가지 핵심 원칙은 다음과 같습니다.

    • 모든 트래픽은 신뢰할 수 없다 (Never Trust, Always Verify): 내부망이든 외부망이든 모든 트래픽을 의심하고 검증합니다.
    • 최소 권한 원칙 (Least Privilege): 사용자나 시스템에 필요한 최소한의 권한만 부여하고, 권한이 필요한 순간에만 잠시 허용합니다.
    • 침해는 항상 발생한다고 가정 (Assume Breach): 아무리 잘 막아도 언젠가는 뚫릴 수 있다는 전제하에, 침해 발생 시 피해를 최소화하고 빠르게 탐지/대응하는 데 집중합니다.

    실전 구현: Zero Trust 핵심 요소와 적용 전략

    Zero Trust를 도입하려면 여러 요소들을 유기적으로 결합해야 합니다. 제가 홈랩이나 회사에서 고민하고 적용했던 방법들을 바탕으로 설명해 드릴게요.

    1. 강력한 ID 및 접근 관리 (IAM, Identity and Access Management)

    가장 기본 중의 기본입니다. 모든 접근의 주체인 사용자와 디바이스를 확실하게 식별하고 인증해야 하거든요. 특히 MFA (Multi-Factor Authentication, 다단계 인증)는 이제 선택이 아닌 필수입니다. 비밀번호 하나만으로는 너무 취약하잖아요?

    저는 오픈소스 Keycloak (키클록)을 활용해서 MFA를 강제한 경험이 있습니다. 처음엔 사용자들의 반발도 좀 있었는데, 보안 사고 한 번 겪고 나니 다들 군말 없이 따르더라고요. Keycloak에서 사용자가 로그인할 때 OTP 앱 같은 두 번째 인증 요소를 요구하도록 설정할 수 있어요.

    예를 들어, 특정 클라이언트에 대해 MFA를 강제하려면 Keycloak 관리 콘솔에서 Realm(영역) 설정의 Authentication(인증) 탭에서 Flow를 조정하거나, 클라이언트별로 Required Actions(필수 작업)을 설정할 수 있습니다. CLI로도 가능하죠.

    
    # Keycloak Admin CLI를 사용하여 특정 Realm의 Required Action 확인 및 추가
    # (Keycloak 버전 및 설정에 따라 명령어가 다를 수 있습니다. 예시입니다.)
    
    # realm-name을 실제 Realm 이름으로 변경하세요.
    REALM_NAME="my-zero-trust-realm"
    
    # 현재 Realm의 모든 Required Actions 목록 확인
    ./kcadm.sh get realms/$REALM_NAME -r --fields requiredActions
    
    # 'configure OTP'를 필수 작업으로 추가 (이미 있다면 건너뜜)
    # 실제 운영 환경에서는 단계별 플로우를 더 정교하게 구성해야 합니다.
    ./kcadm.sh update realms/$REALM_NAME -s 'requiredActions=[{"alias":"configure_otp","name":"Configure OTP","providerId":"kc-otp-setup","enabled":true,"defaultAction":true,"priority":10,"config":{}}]'
    
    echo "MFA(OTP) 설정이 필수 작업으로 추가되었습니다. 사용자는 다음 로그인 시 OTP 설정을 요구받을 것입니다."
    

    이렇게 설정하면 사용자는 로그인 후 OTP 설정을 완료해야만 서비스에 접근할 수 있게 됩니다. 처음에 좀 번거로워도 보안에 훨씬 유리하죠. SSO (Single Sign-On, 단일 로그인)를 함께 구축하면 사용자 경험도 크게 해치지 않으면서 보안을 강화할 수 있죠.

    2. 마이크로 세그멘테이션 (Micro-segmentation)

    네트워크를 아주 작게 쪼개서 각 워크로드나 서비스 간의 통신까지도 제어하는 방식입니다. 기존에는 ‘사내망’이라는 큰 덩어리로 관리했다면, 마이크로 세그멘테이션은 ‘웹 서버는 DB 서버랑만 통신하고, 개발 서버는 운영 DB에 접근 못하게’ 같은 아주 세밀한 정책을 적용하는 거죠. 침해 사고 발생 시 피해 확산을 막는 데 아주 효과적입니다.

    특히 Kubernetes (쿠버네티스) 환경에서는 Network Policy (네트워크 정책)를 활용해서 쉽게 마이크로 세그멘테이션을 구현할 수 있습니다. 처음엔 좀 헷갈렸는데, 몇 번 해보니 그리 어렵지 않더라고요.

    Kubernetes 환경에서 마이크로 세그멘테이션이 어떻게 동작하는지 보여주는 다이어그램입니다. 각 네임스페이스 간의 트래픽 흐름을 제어하는 모습을 나타냅니다.

    
    # Kubernetes Network Policy 예시: frontend 파드가 backend 파드로만 접근 허용
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: allow-frontend-to-backend
      namespace: default # 정책을 적용할 네임스페이스
    spec:
      podSelector:
        matchLabels:
          app: backend # 이 정책이 적용될 파드 (backend 앱을 가진 파드)
      policyTypes:
        - Ingress # Ingress(인그레스, 외부 -> 내부) 트래픽에 대한 정책
      ingress:
        - from:
            - podSelector:
                matchLabels:
                  app: frontend # frontend 앱을 가진 파드로부터의 트래픽만 허용
          ports:
            - protocol: TCP
              port: 8080 # backend 파드의 8080 포트로만 접근 허용
    
    ---
    
    # Kubernetes Network Policy 예시: 모든 outbound 트래픽 기본 차단 (default deny egress)
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: default-deny-egress
      namespace: default
    spec:
      podSelector: {}
      policyTypes:
        - Egress # Egress(이그레스, 내부 -> 외부) 트래픽에 대한 정책
      egress: [] # 아무런 egress 룰도 없으므로, 모든 outbound 트래픽이 차단됩니다.
    

    위 예시처럼 `NetworkPolicy`를 사용하면 `frontend` 파드만이 `backend` 파드의 특정 포트로 접근하게 하고, 다른 파드들은 접근하지 못하게 할 수 있습니다. 두 번째 정책은 해당 네임스페이스의 모든 파드가 외부로 나가는 트래픽을 기본적으로 차단하는 설정입니다. 필요한 아웃바운드(Outbound) 통신만 명시적으로 허용해야겠죠. 이러한 정책을 통해 불필요한 통신 경로를 차단하고 공격 표면(Attack Surface)을 최소화할 수 있습니다.

    3. 엔드포인트 보안 및 가시성 (Endpoint Security & Visibility)

    사용자가 사용하는 노트북, 모바일 기기 같은 엔드포인트(Endpoint) 역시 Zero Trust의 중요한 요소입니다. 이 디바이스들이 안전한지, 최신 보안 패치가 적용되어 있는지, 악성코드는 없는지 지속적으로 확인해야 합니다. EDR (Endpoint Detection and Response) 솔루션이나 디바이스 Posture Check (자세 확인) 기능이 필수적이죠. 저는 홈랩에서 오픈소스 솔루션들을 조합해서 대략적인 Posture Check를 구현해본 적이 있는데, 상용 솔루션만큼 강력하진 않아도 기본적인 보안 수준을 높이는 데는 도움이 되더라고요.

    4. SDP (Software-Defined Perimeter, 소프트웨어 정의 경계)

    SDP는 사용자가 접근하려는 애플리케이션에 직접 연결시켜주고, 불필요한 네트워크 노출을 최소화하는 개념입니다. 마치 보이지 않는 네트워크 터널을 만들어서 인가된 사용자만 접근하게 하는 방식이죠. VPN(Virtual Private Network)과 비슷하지만, 더 세밀한 접근 제어가 가능하고, ‘연결 전 인증’을 통해 네트워크 자체를 숨기는 효과가 있습니다. 클라우드 환경에서 특히 유용합니다.

    ⚠️ 주의사항 및 트러블슈팅: 제가 겪은 ‘삽질’ 경험

    Zero Trust를 도입하면서 마냥 좋기만 했던 건 아닙니다. 저도 꽤 많은 삽질을 했거든요. 몇 가지 대표적인 경험을 공유해 드릴게요.

    1. 너무 강한 초기 정책 설정으로 인한 서비스 장애

    처음엔 ‘모든 것을 차단하고 필요한 것만 열자!’라는 생각에 너무 빡빡하게 정책을 잡았습니다. 그랬더니 개발자들이 ‘왜 우리 서비스가 통신이 안 되냐’며 난리가 났던 적이 있어요. 특정 마이크로서비스 간의 통신이 막혀서 장애가 발생한 거죠. 🤦‍♂️

    해결 과정: 급하게 정책을 풀고, 시스템 로그를 꼼꼼히 분석하기 시작했습니다. 특히 네트워크 보안 장비의 `audit log`나 `syslog`를 확인해서 어떤 트래픽이 차단되었는지, 소스와 목적지가 어디인지 파악하는 게 중요하죠. Kubernetes 환경에서는 `kubectl logs`로 파드 로그를 보고, `NetworkPolicy` 관련 이벤트를 확인했죠. 만약 로그에서 `DROP`이나 `DENY` 관련 메시지가 특정 IP 주소나 포트에서 지속적으로 보인다면, 해당 통신이 정책에 의해 차단되고 있다는 의미이니, 서비스 요구사항에 맞춰 정책을 조정해야 합니다. 너무 급하게 하지 마시고, 테스트 환경에서 충분히 검증하는 시간을 가져야 합니다.

    2. 레거시 시스템과의 연동 문제

    Zero Trust를 도입하려다 보니, 오래된 사내 시스템 중에는 MFA나 SSO를 지원하지 않는 경우가 많더라고요. 이런 시스템들은 IDP와 직접 연동하기가 어려워서 골머리를 앓았죠. 😩

    해결 과정: 이런 경우에는 프록시(Proxy)나 API 게이트웨이(Gateway)를 도입해서 인증 과정을 중간에서 처리하는 방식으로 우회했습니다. 예를 들어, Nginx의 `auth_request` 모듈을 활용해서 Keycloak과 같은 IDP로 인증 요청을 보낸 후, 인증이 성공하면 실제 백엔드 레거시 시스템으로 트래픽을 포워딩하는 방식이죠. 초기 설정이 좀 복잡하지만, 한 번 구축해두면 레거시 시스템도 Zero Trust 환경에 포함시킬 수 있습니다.

    3. 성능 저하 및 관리 복잡도 증가

    모든 트래픽을 검증하고, 세밀한 정책을 적용하다 보니 초기에는 네트워크 지연 시간(Latency)이 늘어나거나, 정책 관리 자체가 너무 복잡해지는 문제가 발생하기도 했습니다. ‘이러다 배보다 배꼽이 더 커지는 거 아니야?’ 하는 걱정도 들었죠. 😥

    해결 과정: 성능 문제는 정책 최적화와 함께 전용 보안 솔루션을 도입하면서 해결했습니다. 하드웨어 기반의 보안 장비나 클라우드 네이티브 보안 서비스를 활용하면 정책 검증에 드는 오버헤드를 줄일 수 있습니다. 관리 복잡도는 정책 자동화 도구나 중앙 집중식 정책 관리 플랫폼을 도입해서 해결해나갔죠. 처음부터 완벽하게 하려기보다, 핵심 시스템부터 점진적으로 적용하고 경험을 쌓아가는 것이 중요합니다.

    ✅ 검증 및 결과: Zero Trust 도입 후 달라진 점

    이런 삽질을 거쳐 Zero Trust 아키텍처를 도입하고 나니, 확실히 보안 수준이 한 단계 높아졌다는 걸 체감할 수 있었습니다. 가장 크게 체감한 건 공격 표면(Attack Surface) 감소와 위협 탐지율 증가였어요. 예전에는 내부망에 들어오면 끝이었지만, 이제는 모든 접근이 검증되니 마음이 한결 놓이더라고요.

    저희 팀에서는 Zero Trust 도입 후 다음과 같은 지표들을 꾸준히 모니터링하면서 관리하고 있습니다.

    • MFA 적용률: 전체 사용자 중 MFA를 사용하는 비율. (높을수록 좋음)
    • 정책 위반(Policy Violation) 로그 수: 허용되지 않은 접근 시도 및 차단 횟수. (낮을수록 좋지만, 너무 낮으면 정책이 느슨한 것일 수도 있어 분석 필요)
    • 엔드포인트 보안 점수: 디바이스의 보안 패치 상태, 악성코드 유무 등을 종합한 점수. (높을수록 좋음)
    • 세션당 평균 권한 지속 시간: 최소 권한 원칙이 잘 지켜지는지 확인. (짧을수록 좋음)
    Zero Trust 도입 후 주요 보안 지표를 모니터링하는 대시보드

    Zero Trust 도입 후 보안 수준을 측정하고 모니터링하는 대시보드 예시입니다. 주요 지표들을 한눈에 확인할 수 있습니다.

    마무리: Zero Trust는 여정입니다

    Zero Trust 아키텍처는 한 번에 뚝딱 완성되는 것이 아니라, 지속적인 개선과 노력이 필요한 ‘여정(Journey)’이라고 생각합니다. 기술적인 도입뿐만 아니라, 조직의 보안 문화와 프로세스를 함께 변화시켜야 성공할 수 있거든요.

    어떤 솔루션을 선택해야 할지 고민이 많으실 텐데요, 우리 조직의 규모와 예산, 그리고 기존 인프라 환경에 따라 최적의 선택은 달라질 수 있습니다. 제가 간단한 비교표를 준비해봤어요.

    구분 오픈소스/클라우드 네이티브 상용 솔루션
    장점
    • 비용 효율적
    • 높은 유연성과 커스터마이징 가능
    • 클라우드 환경에 최적화
    • 통합된 기능과 관리 용이성
    • 전문적인 기술 지원
    • 빠른 구축 및 안정성
    단점
    • 구축 및 관리에 기술적 역량 요구
    • 기능 조합 시 복잡도 증가
    • 레거시 시스템 연동 어려움
    • 높은 도입 비용
    • 벤더 종속성 발생 가능
    • 커스터마이징의 한계
    추천 대상
    • 작은 규모의 조직/스타트업
    • 클라우드 중심의 인프라
    • 내부 기술 역량 충분한 경우
    • 대규모 조직 또는 규제 산업
    • 빠르고 안정적인 구축 필요한 경우
    • 기술 지원이 필수적인 경우

    만약 작은 규모의 조직이거나 클라우드 환경에 익숙하다면, Keycloak 같은 오픈소스 IDP와 각 클라우드 벤더(AWS, Azure, GCP 등)의 보안 서비스(예: AWS IAM, Security Groups, Network ACLs, Azure Active Directory, Google Cloud Identity)를 조합하여 시작하는 것을 추천합니다. 반면, 규제가 엄격하거나 인프라 규모가 큰 경우에는 통합적인 기능을 제공하는 상용 Zero Trust 솔루션이 더 유리할 수 있습니다.

    어떤 선택이든 중요한 것은 점진적인 접근입니다. 모든 것을 한 번에 바꾸려 하지 말고, 가장 핵심적인 시스템부터 Zero Trust 원칙을 적용하고, 점차 범위를 넓혀 나가는 전략이 성공 확률을 높일 수 있을 거예요. 저도 그렇게 해왔고요.

    Zero Trust 솔루션 선택을 위한 오픈소스, 클라우드 네이티브, 상용 솔루션 비교표

    Zero Trust 솔루션 선택 시 고려할 수 있는 다양한 옵션들을 비교한 개념표입니다.

    다음번에는 특정 Zero Trust 솔루션을 저희 홈랩에서 직접 구축하고 연동하는 과정에 대해 더 자세히 다뤄볼게요. 그때까지 여러분의 서버실도 항상 안전하길 바랍니다! 궁금한 점이 있다면 언제든지 댓글로 남겨주세요.

  • [Cloud] 객체 스토리지 성능 측정: S3/R2/GCS 벤치마크 방법론

    [Cloud] 객체 스토리지 성능 측정: S3/R2/GCS 벤치마크 방법론

    객체 스토리지 성능 측정 방법론: S3/R2/GCS 벤치마크

    객체 스토리지 성능 측정은 숫자 하나 뽑는 일보다 비교 조건을 설계하는 일에 더 가깝습니다. 업로드 한 번 돌려서 나온 시간에는 그 순간의 네트워크 상태, TLS 세션 재사용 여부, 클라이언트 CPU 상태, 리전 거리까지 한꺼번에 섞이거든요. 저도 처음엔 단일 파일 복사 시간만 보고 판단했다가, 운영에 올린 뒤 작은 파일 다건 업로드와 메타데이터 조회가 병목이 되면서 완전히 다른 그림을 봤습니다. 그래서 이 글은 “누가 더 빠르다”를 단정하려는 글이 아니라, 객체 스토리지 성능 측정을 실무에서 재현 가능하게 만드는 방법론을 정리한 글입니다.

    특히 S3, R2, GCS는 API 표면은 비슷해 보여도 측정 도구, 병렬화 방식, 재시도 전략, 멀티파트 또는 분할 업로드 동작이 꽤 다릅니다. 이 차이를 통제하지 않으면 저장소를 비교하는 게 아니라 각 벤치마크 도구의 기본값을 비교하게 됩니다. 현업에서는 이 실수가 진짜 치명적이더라고요. 비슷한 인프라 비교 글을 함께 정리해 두면, 나중에 CDN 캐시나 전송 비용 이슈까지 한 번에 판단하기도 훨씬 편합니다.

    객체 스토리지 성능 측정 아키텍처 다이어그램

    단일 벤치마크 클라이언트에서 S3, R2, GCS로 동일한 워크로드를 보내고 지표를 수집하는 구성 예시입니다.

    왜 단순 업로드 시간만 보면 자꾸 판단이 틀어질까

    객체 스토리지는 로컬 디스크 벤치마크처럼 순차 읽기·쓰기 하나로 설명되지 않습니다. 요청 하나마다 DNS, TCP, TLS, HTTP 연결 재사용, 인증, 서버 측 큐잉이 섞입니다. 같은 100GB 업로드라도 1GB 파일 100개와 256KB 파일 수십만 개는 완전히 다른 테스트예요. 전자는 스트림 처리량 문제고, 후자는 요청 수와 꼬리 지연 문제입니다.

    • 대용량 백업이면 단일 스트림 처리량, 멀티파트 분할 기준, 재시도 시 복구 비용이 핵심입니다.
    • 썸네일, 로그 조각, 문서 스니펫처럼 작은 객체가 많으면 평균 속도보다 요청당 지연 시간과 실패율이 더 중요합니다.
    • 애플리케이션이 HEAD, LIST, GET 또는 메타데이터 조회를 자주 쓰면 PUT 성능보다 p95·p99 응답 시간이 체감에 더 크게 작용합니다.

    제가 실무에서 제일 많이 보는 오판은 이것입니다. 큰 파일 하나는 빨랐는데 서비스는 느린 경우요. 이유는 단순합니다. 서비스는 큰 파일 한 번보다 작은 객체 수천 번을 더 자주 만지기 때문입니다. 그래서 벤치마크는 항상 워크로드를 먼저 정의해야 합니다. 저장소 이름보다 접근 패턴이 먼저예요.

    객체 스토리지 성능 측정에서 꼭 봐야 하는 지표

    표를 화려하게 만들 필요는 없습니다. 대신 운영 판단에 실제로 쓰이는 지표만 남겨야 합니다. 저는 아래 여섯 개를 기본 세트로 잡습니다.

    1. Throughput: 초당 전송 바이트 수입니다. 큰 파일 백업, 미디어 아카이브, 데이터 레이크 적재처럼 스트리밍 성격이 강한 워크로드에서 중요합니다.
    2. Latency: 요청 하나가 끝나는 시간입니다. 작은 객체 서비스는 이 값이 곧 체감 성능입니다.
    3. p95/p99 Tail Latency: 평균값이 멀쩡해도 느린 구간이 튀면 운영 품질은 나빠집니다. 사용자 불만은 거의 여기서 나옵니다.
    4. Error Rate / Retry Count: 4xx, 5xx, 타임아웃, 재시도 횟수입니다. 처리량이 조금 높아도 재시도가 많으면 운영 점수는 낮게 봐야 합니다.
    5. Concurrency Scaling: 동시성을 올렸을 때 얼마나 비례해서 늘어나는지 봅니다. 이 곡선이 평평해지는 지점이 병목 후보입니다.
    6. Client Resource Usage: CPU, NIC, 로컬 디스크 사용률입니다. 저장소를 재는 줄 알았는데 테스트 클라이언트의 암호화 비용이나 디스크 읽기 속도를 재는 경우가 생각보다 흔합니다.

    여기서 하나 더 붙이자면 변동성입니다. 같은 조건에서 5번 돌렸을 때 편차가 큰 서비스는 평균값이 좋아도 운영 예측 가능성이 낮습니다. 백업 창이 촘촘하거나 배치 종료 시간이 중요한 팀이라면 최고 속도보다 흔들림이 작은 쪽이 더 낫습니다. 이거, 막상 운영 붙이면 차이가 꽤 크게 느껴집니다.

    S3/R2/GCS 객체 스토리지 성능 측정을 공정하게 만드는 설계

    툴보다 중요한 건 통제 변수입니다. S3 성능 벤치마크, R2 속도 측정, GCS 처리량 비교를 따로따로 보면 쉬워 보여도 실제로는 한 줄만 달라도 해석이 깨집니다. 아래 항목은 최소한 맞춰두셔야 합니다.

    항목 왜 맞춰야 하나 권장 방식
    클라이언트 위치 리전 거리와 네트워크 경로가 바뀌면 저장소 자체 차이가 가려집니다 같은 VM 또는 같은 베어메탈 호스트에서 모든 테스트 수행
    파일 크기 분포 작은 파일과 큰 파일은 병목이 다릅니다 작은 파일, 중간 파일, 큰 파일, 혼합 패턴을 분리 측정
    동시성 단일 연결 수치만으로는 확장 특성을 보기 어렵습니다 1, 4, 8, 16처럼 단계적으로 올리고 증가 곡선을 기록
    재시도/타임아웃 도구 기본값이 다르면 실패율과 시간 값이 왜곡됩니다 가능하면 동일한 재시도 횟수와 타임아웃 정책을 명시
    멀티파트/병렬 업로드 기준 큰 파일 성능에 직접 영향을 줍니다 threshold, chunk size, 병렬 프로세스 수를 문서화해서 고정
    워밍업 첫 실행은 DNS, TLS, 캐시 영향으로 튀는 경우가 많습니다 워밍업 1회 후 본 측정 5회 이상

    제 기준으로는 한 서비스가 더 빨라 보일 때 바로 결론을 내리지 않습니다. 먼저 묻는 건 세 가지입니다. 같은 크기 분포였는지, 같은 동시성이었는지, 같은 재시도 정책이었는지. 이 셋이 다르면 사실상 다른 시험입니다.

    객체 스토리지 성능 측정용 워크로드 매트릭스 이미지

    큰 파일, 작은 파일, 혼합 워크로드와 동시성 단계별 측정 매트릭스를 한눈에 정리한 구성입니다.

    S3 성능 벤치마크를 위한 실전 준비

    1. 테스트 파일 세트 만들기

    데이터 세트는 재현 가능해야 합니다. 운영 덤프를 그대로 쓰면 현실성은 좋지만, 개인정보 처리와 반복성 문제가 생깁니다. 그래서 저는 보통 크기 분포만 닮은 합성 데이터 세트를 따로 만듭니다. 빈 파일이나 <code>truncate로 만든 희소 파일은 피하시는 편이 낫습니다. 압축, 중복 제거, 페이지 캐시 영향 때문에 실제 업로드 비용과 동떨어질 수 있어서요.

    mkdir -p ~/objbench/data/{small,medium,large}
    cd ~/objbench/data
    
    # 작은 파일 1000개: 요청 수와 지연 시간 측정용
    for i in $(seq -w 1 1000); do
      dd if=/dev/urandom of=small/small-${i}.bin bs=256K count=1 status=none
     done
    
    # 중간 파일 100개: 일반적인 애플리케이션 업로드 패턴 근사
    for i in $(seq -w 1 100); do
      dd if=/dev/urandom of=medium/medium-${i}.bin bs=8M count=8 status=none
     done
    
    # 큰 파일 4개: 멀티파트 처리량과 재시도 비용 확인용
    for i in $(seq -w 1 4); do
      dd if=/dev/urandom of=large/large-${i}.bin bs=64M count=16 status=progress
     done
    
    # 디렉터리별 총 용량 확인
    find ~/objbench/data -type f -printf '%h\n' | sort | uniq -c
    find ~/objbench/data -type f -exec du -ch {} + | tail -n 1

    이렇게 나누는 이유는 단순합니다. 큰 파일은 스트리밍 경로를 재고, 작은 파일은 요청 처리 경로를 잽니다. 같은 저장소라도 두 결과가 정반대로 나올 수 있습니다.

    2. AWS CLI로 S3와 R2 조건 맞추기

    S3와 R2는 같은 S3 호환 API 축에 있으니, 비교용 도구는 하나로 맞추는 편이 낫습니다. 여기서 핵심은 기본값을 믿지 않는 겁니다. AWS CLI는 max_concurrent_requests, multipart_threshold, multipart_chunksize, addressing_style 같은 값이 결과에 직접 영향을 줍니다. 큰 파일 테스트에서는 이 네 개를 반드시 기록해 두세요.

    # 기본 프로파일에 S3 전송 관련 설정 고정
    aws configure set default.s3.max_concurrent_requests 16
    aws configure set default.s3.multipart_threshold 64MB
    aws configure set default.s3.multipart_chunksize 16MB
    aws configure set default.s3.addressing_style path
    
    # 선택: 대역폭 제한이 필요한 경우만 사용
    # aws configure set default.s3.max_bandwidth 200MB/s
    
    # 설정 확인
    aws configure get default.s3.max_concurrent_requests
    aws configure get default.s3.multipart_threshold
    aws configure get default.s3.multipart_chunksize
    aws configure get default.s3.addressing_style
    
    # S3 업로드 측정
    /usr/bin/time -f 'elapsed=%E cpu=%P mem=%MKB' \
      aws s3 cp ~/objbench/data/large/large-01.bin s3://my-s3-bucket/bench/large-01.bin \
      --only-show-errors
    
    # R2 업로드 측정
    /usr/bin/time -f 'elapsed=%E cpu=%P mem=%MKB' \
      aws --endpoint-url https://ACCOUNT_ID.r2.cloudflarestorage.com \
      s3 cp ~/objbench/data/large/large-01.bin s3://my-r2-bucket/bench/large-01.bin \
      --only-show-errors

    여기서 자주 놓치는 부분이 두 가지입니다. 하나는 경로 방식(path style)과 가상 호스트 방식 차이, 다른 하나는 도구 내부 기본 동작 차이입니다. R2는 현재 path style과 virtual-hosted style을 모두 지원하지만, 비교 목적이라면 주소 지정 방식까지 한 번 정해 고정하는 편이 해석이 깔끔합니다. 조건이 자꾸 바뀌면 저장소 비교가 흐려지거든요.

    그리고 멀티파트를 너무 공격적으로 키우는 것도 능사는 아닙니다. 파트 수를 늘리면 처리량은 올라갈 수 있지만, 요청 수와 클라이언트 메모리 사용량도 같이 늘어납니다. 백업 창을 줄이는 게 목표라면 유효할 수 있지만, 공유 VM이나 제한된 NAT 환경에서는 오히려 타임아웃과 재시도가 늘 수 있습니다.

    3. GCS 처리량 측정은 gsutil 병렬 옵션을 분리해서 보기

    GCS 쪽은 여기서 많이 헷갈립니다. gsutil -m은 단순한 장식 옵션이 아니라 결과 해석을 바꾸는 변수입니다. 공식 문서 기준으로도 -m은 병렬 작업을 켜며, 여러 객체 작업에서 동작 특성이 달라집니다. 그래서 단일 실행 결과와 병렬 실행 결과를 같은 줄에 섞으면 안 됩니다.

    또 한 가지는 최신 상태입니다. 2026년 8월 30일 기준으로 Google Cloud는 새 자동화에는 gcloud storage 사용을 권장하고 있고, gsutil은 권장 CLI가 아닙니다. 게다가 공식 문서에는 2027년 3월 이후 gsutil이 Google Cloud CLI 설치 패키지에 더 이상 포함되지 않고 독립 도구로 배포된다고 안내돼 있습니다. 기존 스크립트 검증이라면 gsutil로 재도 되지만, 새 벤치마크 체계를 만든다면 gsutil 결과와 gcloud storage 결과를 혼용하지 않는 것이 좋습니다. 도구가 다르면 기본 병렬화와 출력 형식이 다르기 때문입니다.

    # gsutil 단일 실행
    /usr/bin/time -f 'elapsed=%E cpu=%P mem=%MKB' \
      gsutil cp ~/objbench/data/large/large-01.bin gs://my-gcs-bucket/bench/
    
    # gsutil 병렬 실행: 작은/중간 파일 다건 처리 패턴 확인용
    /usr/bin/time -f 'elapsed=%E cpu=%P mem=%MKB' \
      gsutil -m cp ~/objbench/data/medium/*.bin gs://my-gcs-bucket/bench/
    
    # 병렬 관련 boto 설정 예시
    cat > ~/.boto <<'EOF'
    [GSUtil]
    parallel_process_count = 4
    parallel_thread_count = 8
    resumable_threshold = 64M
    EOF
    
    # 객체 메타데이터 확인
    gsutil stat gs://my-gcs-bucket/bench/large-01.bin
    
    # 새 자동화 체계 예시: gcloud storage 기준으로 별도 측정
    /usr/bin/time -f 'elapsed=%E cpu=%P mem=%MKB' \
      gcloud storage cp ~/objbench/data/large/large-01.bin gs://my-gcs-bucket/bench/

    제가 권하는 방식은 간단합니다. GCS는 최소 두 시나리오로 나누세요. 1) 단일 객체 업로드 성능 2) 다건 병렬 처리 성능. 이 둘을 분리해야 S3/R2와 비교할 때 어디서 차이가 나는지 보입니다.

    클라이언트 병목을 같이 보지 않으면 숫자가 자꾸 거짓말합니다

    저장소 성능 테스트에서 가장 억울한 순간은, 나중에 보니 저장소가 아니라 내 테스트 머신이 병목이었던 경우입니다. 특히 암호화 오프로딩이 약한 VM, 공유 네트워크 인터페이스, 느린 로컬 디스크 위에서는 이런 일이 자주 납니다. 그래서 벤치마크 로그에는 저장소 지표와 함께 클라이언트 지표를 남겨야 합니다.

    sar, mpstat, iostat는 보통 Linux의 sysstat 패키지에 포함되니, 없는 환경이라면 먼저 설치 여부부터 확인해 두세요. 이런 사전 점검이 생각보다 시간을 많이 아껴줍니다.

    # 1초 간격으로 네트워크, CPU, 디스크 상태 확인
    sar -n DEV 1
    mpstat -P ALL 1
    iostat -x 1
    
    # DNS와 TCP/TLS가 병목인지 분리해서 보기 위한 curl 예시
    curl -s -o /dev/null \
      -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} starttransfer=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' \
      https://storage.googleapis.com/

    저는 해석을 이렇게 합니다.

    • 동시성을 1, 4, 8, 16으로 올렸는데 처리량이 거의 그대로면 저장소보다 클라이언트 NIC, CPU, TCP 윈도우, NAT 세션 한도를 먼저 의심합니다.
    • CPU 한 코어만 유독 높으면 CLI의 단일 스레드 구간이나 TLS 처리 비용이 병목일 수 있습니다.
    • await나 %util이 높은 디스크에서 업로드 테스트를 하면, 사실은 원본 파일 읽기 성능을 재고 있을 가능성이 큽니다.
    • 평균은 괜찮은데 체감이 느리면 p95/p99를 따로 적어야 합니다. 서비스 품질은 평균보다 꼬리 지연에 훨씬 민감합니다.

    여기서 하나 더. 공인 인터넷을 거치는 테스트라면 시간대 편차도 로그에 남기세요. 같은 조건인데 낮과 밤이 다르면 저장소보다 네트워크 경로 혼잡도가 결과를 더 크게 흔들고 있다는 뜻일 수 있습니다.

    객체 스토리지 성능 측정 중 병목 분석 화면

    저장소 자체 성능과 클라이언트 CPU·디스크·네트워크 병목을 함께 확인하는 실전 점검 화면 예시입니다.

    실제 트러블슈팅: 멀티파트 조건이 달라서 비교가 깨졌던 사례

    실무에서 가장 자주 보는 실패 모드는 화려하지 않습니다. 대개는 비교 조건이 숨은 곳에서 달라져 있는 문제입니다. 제가 크게 데인 사례도 그랬습니다. S3와 R2는 AWS CLI로, GCS는 별도 CLI로 측정했는데, 한쪽은 큰 파일을 여러 파트로 나눠 병렬 전송하고 있었고 다른 쪽은 사실상 단일 흐름에 가까웠습니다. 표만 보면 특정 서비스가 느려 보이는데, 저장소가 느린 게 아니라 전송 방식이 달랐던 거죠.

    증상은 이랬습니다.

    1. 큰 파일 업로드에서 한 서비스만 유독 시간이 길었습니다.
    2. 작은 파일 묶음 테스트에서는 차이가 크지 않았습니다.
    3. 클라이언트 CPU와 NIC는 한계에 닿지 않았고, 재시도 로그도 많지 않았습니다.

    이럴 때는 저장소를 탓하기 전에 먼저 전송 단위를 확인하셔야 합니다. 멀티파트 threshold, part size, 병렬 프로세스 수, 재시도 정책이 같지 않으면 결과 해석이 불가능합니다. 저는 그 뒤로 아예 아래 체크리스트를 벤치마크 시트 첫 줄에 적어둡니다.

    • 도구와 버전이 동일한가
    • 멀티파트 또는 병렬 업로드 기준이 동일한가
    • 동시성과 재시도 정책이 동일한가
    • 첫 실행을 버리고 워밍업 뒤 본 측정을 했는가
    • 클라이언트 CPU, NIC, 디스크 병목 여부를 함께 기록했는가

    이 체크리스트를 통과하면 그제야 숫자가 설명되기 시작합니다. 벤치마크는 예쁜 수치보다 왜 그런 결과가 나왔는지 설명 가능한 상태가 먼저입니다.

    결과는 어떻게 읽어야 하나: 숫자보다 패턴을 봅니다

    같은 결과표라도 읽는 기준이 없으면 결론이 흔들립니다. 저는 아래처럼 해석합니다.

    시나리오 기록할 값 읽는 방법
    큰 파일 업로드 경과 시간, 평균 처리량, 재시도 횟수, 멀티파트 설정 동시성 증가에 따라 처리량이 오르는지, 특정 지점에서 평평해지는지 봅니다
    작은 파일 다건 업로드 총 소요 시간, 파일당 평균 시간, 오류 수, p95/p99 평균보다 편차와 실패율을 더 무겁게 봅니다
    메타데이터 요청 HEAD/LIST 응답 시간 또는 메타데이터 조회 시간, 타임아웃 여부 애플리케이션 체감과 직접 연결되므로 꼬리 지연을 우선 봅니다
    반복 실행 안정성 실행별 분포, 중앙값, 최댓값/최솟값 차이 가장 빠른 값보다 흔들림이 작은 쪽이 운영 예측 가능성이 높습니다

    운영 관점에서 읽으면 기준이 더 선명해집니다.

    • 동시성을 올려도 처리량이 안 늘면 그 지점이 현재 병목입니다. 저장소가 아니라 클라이언트나 네트워크일 가능성도 큽니다.
    • 큰 파일은 빠른데 작은 파일이 느리면, 그 저장소가 나쁜 게 아니라 현재 워크로드와 안 맞는 겁니다.
    • 반복 5회 중 1회만 튄다면 최고 기록보다 안정성 리스크를 먼저 봐야 합니다.
    • 오류나 재시도가 보이면 평균 처리량이 좋아도 운영 점수는 낮게 잡는 편이 안전합니다.
    S3 R2 GCS 처리량과 지연 시간 비교 대시보드

    처리량, p95 지연 시간, 재시도 횟수를 한 화면에서 비교해 패턴을 읽는 결과 대시보드 예시입니다.

    자주 받는 질문 정리

    벤치마크는 몇 번 반복하면 좋을까요?

    워밍업 1회 후 본 측정 5회 이상은 잡으시는 게 좋습니다. 그보다 적으면 편차를 해석하기 어렵습니다. 가능하면 시간대도 나눠 보세요. 저장소보다 네트워크가 흔들리는지 구분하는 데 도움이 됩니다.

    다운로드 테스트도 꼭 해야 하나요?

    네. 업로드와 다운로드는 병목이 다를 수 있습니다. 서비스 제공용이면 GET 패턴이 더 중요할 때도 많고, 다운로드 경로에서 CDN이나 캐시 전략을 같이 볼 수도 있습니다.

    한 도구로 통일해야 하나요?

    가능하면 좋습니다. 다만 서비스별 공식 도구 차이가 있으니 현실적으로 완전 통일이 어려울 수는 있습니다. 그럴수록 비교 문서에 도구, 버전, 병렬화 옵션, 재시도 설정을 더 자세히 남겨야 합니다.

    벤치마크 비용은 어떻게 줄이나요?

    저는 먼저 작은 파일 세트와 중간 파일 세트로 병목 구간을 찾고, 마지막에만 큰 파일 반복 측정을 합니다. 큰 파일 다회 측정은 전송 비용과 시간 비용이 커서, 초반 탐색 단계에서는 효율이 좋지 않습니다.

    현업 기준으로 이렇게 추천드립니다

    판단 기준은 생각보다 명확합니다. 백업, 로그 아카이브, 대용량 적재처럼 큰 파일 중심이면 S3/R2/GCS 중 무엇이든 먼저 볼 것은 단일 업로드 성공률, 멀티파트 설정, 동시성 증가 시 처리량 곡선입니다. 여기서는 최고 속도 한 줄보다 재시도와 안정성이 더 중요합니다. 처리량이 조금 낮아도 흔들림이 작고 재시도가 적은 쪽이 운영에선 더 낫습니다.

    반대로 이미지, 썸네일, 문서 조각, 메타데이터 조회가 많은 서비스라면 p95 지연 시간, LIST/HEAD 응답, 반복 간 편차를 우선 보셔야 합니다. 이 경우 대역폭보다 요청당 지연이 더 아프게 들어옵니다. 큰 파일 업로드 수치가 좋아도 작은 객체에서 꼬리 지연이 크면 실제 서비스 만족도는 떨어집니다.

    도구 선택도 이렇게 가져가시면 됩니다. S3와 R2를 비교할 때는 AWS CLI로 최대한 조건을 맞추세요. GCS는 기존 스크립트 검증이면 gsutil을 쓰되, 새 자동화 체계라면 gcloud storage로 넘어갈 계획까지 같이 잡는 편이 낫습니다. 다만 한 번의 비교 세트에서는 두 도구를 섞지 않는 게 안전합니다.

    제가 현업에서 내리는 결론은 늘 비슷했습니다. “어느 서비스가 더 빠른가”보다 내 워크로드가 큰 파일 중심인지, 작은 객체 중심인지, 메타데이터 요청 중심인지부터 자르는 것. 그다음 같은 조건으로 재고, 평균보다 꼬리 지연과 변동성을 보고, 마지막에 비용과 운영 복잡도를 얹어 판단합니다. 벤치마크에서 진짜 가치가 있는 숫자는 가장 높은 숫자가 아니라, 운영에서 다시 재현되는 숫자입니다.

    워크로드별로 어떤 지표를 우선 봐야 하는지 정리한 요약 인포그래픽입니다.

  • [Cloud] Cloudflare R2 vs AWS S3/GCS: 객체 스토리지 선택 기준 비교 분석

    [Cloud] Cloudflare R2 vs AWS S3/GCS: 객체 스토리지 선택 기준 비교 분석

    Cloudflare R2 vs AWS S3/GCS: 객체 스토리지 선택 기준 비교

    Cloudflare R2를 검토할 때 많은 분이 먼저 보는 건 저장 단가인데, 실제 운영에서는 그보다 데이터가 어디로 흘러가느냐가 더 크게 작용하더라고요. 같은 파일 하나를 올려도 사용자 다운로드가 많은지, 같은 클라우드 내부 서비스가 읽는지, 브라우저가 직접 접근하는지에 따라 비용 구조와 운영 난이도가 확 달라집니다. 저도 백업 산출물, 정적 자산, 로그 아카이브를 R2, S3, GCS에 각각 붙여보면서 결국 같은 결론으로 돌아왔습니다. 객체 스토리지는 GB당 저장 단가보다 네트워크 경로와 권한 모델이 더 비싸게 굴 때가 많다는 점입니다.

    이번 글은 Cloudflare R2, AWS S3, Google Cloud Storage(GCS)를 제품 소개가 아니라 선택 기준으로 비교합니다. 어느 서비스가 더 좋다고 단정하기보다, 어떤 워크로드에 붙이면 덜 후회하는지, 반대로 어떤 경우엔 일부러 피하는 게 맞는지까지 적어보겠습니다. 가격은 수시로 바뀌니 숫자 나열은 줄이고, 대신 공식 문서로 확인 가능한 동작과 실무에서 바로 부딪히는 파라미터, CLI 문법, 실패 모드를 중심으로 봤습니다.

    특히 이미 S3를 쓰고 있는데 egress 비용이 거슬리거나, GCP 분석 파이프라인과 스토리지를 더 단순하게 묶고 싶거나, S3 호환이라는 말만 믿고 R2로 옮기려는 분이라면 이 글이 판단 시간을 꽤 줄여줄 겁니다. 비슷한 인프라 비교 글이 더 필요하다면 블로그 안의 스토리지·CDN 관련 글도 함께 보시면 흐름이 더 잘 잡힙니다.

    Cloudflare R2, AWS S3, GCS의 연결 구조와 비용 관점을 한눈에 보는 개요 이미지입니다.

    Cloudflare R2 비교에서 먼저 봐야 하는 것

    비교 순서를 잘못 잡으면 스토리지 가격표만 오래 보게 됩니다. 저는 실제로 아래 순서로 먼저 봅니다.

    • 데이터가 누구에게 나가느냐: 사용자 인터넷 다운로드인지, 같은 클라우드 내부 서비스 호출인지
    • 누가 쓰느냐: 백엔드 서버, 배치 작업, 브라우저, 데이터 파이프라인 중 어디가 주체인지
    • 권한을 어디서 통제하느냐: AWS IAM, GCP IAM, 별도 키 관리, 임시 서명 URL 중 무엇이 주력인지
    • 현재 도구를 얼마나 재사용하느냐: AWS CLI/SDK를 그대로 쓸지, gcloud 중심으로 갈지, 운영 문서를 새로 써야 하는지

    저장 단가, 요청 비용, 전송 비용이라는 세 축은 여전히 중요합니다. 다만 실무에서는 이 세 축이 따로 움직이지 않더라고요. 예를 들어 다운로드 서비스는 전송 정책이 핵심이고, 배치·분석 워크로드는 같은 클라우드 안에서 붙는 서비스들과의 연동 비용이 더 중요합니다. 반대로 브라우저 직접 업로드는 CORS와 presigned URL 설계가 더 중요하고요. 결국 객체 스토리지는 저장 상품이 아니라 애플리케이션 경로의 일부로 봐야 비교가 맞습니다.

    Cloudflare R2 vs AWS S3/GCS: 한 번에 보는 선택 기준

    항목 Cloudflare R2 AWS S3 GCS
    핵심 포인트 S3 호환 API와 인터넷 egress 무료 정책이 강점 가장 넓은 생태계, IAM·이벤트·주변 서비스 연동이 매우 촘촘함 GCP 애플리케이션·분석 워크로드와의 결합이 자연스럽고 운영 경험이 일관적
    언제 유리한가 외부 다운로드가 많고 S3 도구 체인을 버리고 싶지 않을 때 이미 AWS 중심 아키텍처이고 저장소를 다른 서비스와 깊게 묶어야 할 때 Cloud Run, GKE, BigQuery, 데이터 적재 파이프라인을 GCP 안에서 굴릴 때
    언제 불리한가 S3와 완전 동일한 부가 기능, 세부 API 동작, 특정 AWS 연동을 기대할 때 인터넷 방향 전송량이 커서 egress가 월 비용을 흔들 때 AWS 도구 체인에 이미 깊게 묶였고 팀이 gcloud·IAM 모델에 익숙하지 않을 때
    운영 난이도 포인트 endpoint 고정, 호환성 문서 확인, 서명·버킷 동작 차이 점검이 핵심 리전, 버킷 정책, 소유권 제어, 서비스별 권한 위임 설계가 핵심 uniform bucket-level access, 서비스 계정 권한, CORS·공개 접근 정책 정리가 핵심
    비용 판단 포인트 인터넷 방향 다운로드가 많을수록 매력적 AWS 내부 서비스와 같은 리전에 묶일수록 자연스럽고 예측 가능 GCP 내부 데이터 처리와 함께 갈 때 전체 운영비 계산이 쉬움
    먼저 권하는 상황 정적 파일 배포, 다운로드 아카이브, 사용자 파일 제공 서비스 AWS 기반 SaaS, 이벤트 파이프라인, 권한 통합이 중요한 시스템 로그 적재, 분석용 원본 보관, GCP 중심 애플리케이션 백엔드

    공식 문서 기준으로 보면 R2는 인터넷 방향 egress에 과금하지 않고, S3와 GCS는 저장, 요청, 전송을 분리해서 봐야 합니다. R2는 현재 S3 호환 API를 제공하지만 구현 범위가 AWS S3와 완전히 같지는 않습니다. 그래서 핵심은 단순히 “R2가 싸다”가 아니라, 트래픽이 인터넷으로 빠지는 구조면 R2가 비용 변동성을 줄이기 쉽다는 점입니다. 반대로 애플리케이션, 배치, 분석, 권한 체계가 이미 AWS/GCP에 깊게 묶여 있으면 저장소만 떼어내는 순간 운영 복잡성이 생깁니다.

    Cloudflare R2를 포함해 제가 실제로 쓰는 결정 프레임

    1. 다운로드 트래픽이 핵심이면 Cloudflare R2

    R2의 가장 눈에 띄는 장점은 저장 기능 자체보다 인터넷 방향 전송 비용을 계산하기 쉬운 구조입니다. 사용자에게 원본 파일, 이미지, 백업 산출물, 미디어 자산을 자주 내려줘야 한다면 이 장점이 꽤 직접적으로 보입니다. 특히 기존 스크립트가 aws s3 cp, aws s3 sync, SDK 기반 업로드 로직으로 이미 짜여 있다면 endpoint만 바꿔서 마이그레이션 테스트를 시작할 수 있다는 점도 큽니다. 이거 진짜 편하더라고요.

    다만 여기서 많이 착각하는 부분이 있습니다. S3-compatible는 S3와 동일가 아닙니다. 호환 API라는 건 마이그레이션 진입장벽을 낮춰준다는 뜻이지, 모든 기능과 부가 동작이 완전히 같다는 뜻은 아니거든요. 그래서 저는 R2를 검토할 때 기능 비교보다 먼저 S3 API compatibility 문서를 열어놓고 봅니다. 이 단계 없이 들어가면 나중에 ACL, 이벤트, 특정 헤더 처리, SDK 기본값에서 시간을 꽤 씁니다.

    2. AWS 서비스와 촘촘히 묶여 있으면 AWS S3

    S3는 여전히 기준점입니다. 이유는 단순합니다. 저장소 혼자 강한 게 아니라 AWS 내부 연결성이 압도적으로 넓기 때문입니다. EC2, Lambda, CloudFront, IAM, 이벤트 기반 처리, 백업 정책, 수명주기 관리까지 이미 AWS 안에서 굴러가고 있다면 S3는 기술적으로도, 조직적으로도 마찰이 적습니다.

    실무에서는 저장 단가 몇 퍼센트보다 운영 문서와 권한 체계를 다시 쓰는 비용이 더 큽니다. 누가 어느 버킷을 읽고 쓰는지, 어떤 배치가 어떤 role을 assume 하는지, 특정 배포 파이프라인이 어떤 리전에 올리는지 다 이미 굴러가고 있다면, 스토리지만 따로 빼서 최적화하는 작업이 생각보다 깔끔하지 않습니다. AWS 중심 서비스라면 S3를 기본값으로 두고, 정말로 인터넷 방향 전송비가 반복적으로 부담이 되는 워크로드만 별도로 떼어내는 접근이 더 현실적이었습니다.

    3. GCP 앱과 데이터 파이프라인이 중심이면 GCS

    GCS는 기능 설명보다 운영 흐름의 일관성으로 평가하는 게 맞습니다. Cloud Run, GKE, 데이터 적재 파이프라인, 서비스 계정, GCP IAM 모델에 이미 익숙한 팀이라면 GCS가 훨씬 덜 거슬립니다. 저장소 하나 때문에 별도의 계정 체계나 SDK 관용구를 늘리지 않아도 되니까요.

    특히 저는 로그 보관, 분석용 원본 적재, 주기적 배치 산출물 보관 같은 용도는 GCP 안에서 끝내는 편이 실수가 적었습니다. 브라우저 접근, 서비스 계정, 권한 상속, 배치 계정 권한 분리가 한 체계 안에 있으니 운영 문서가 짧아집니다. 사소해 보여도 팀이 커질수록 체감 차이가 꽤 큽니다.

    실전 구현: 같은 파일을 R2, S3, GCS에 올려보며 보는 차이

    말로만 비교하면 결국 감이 흐려집니다. 아래는 backup.tar.gz 파일 하나를 각각 올리는 최소 흐름입니다. 저는 이런 테스트를 할 때 세 가지만 같이 봅니다. 인증이 어디서 결정되는지, 리전 또는 endpoint를 누가 결정하는지, 업로드 직후 메타데이터를 어떻게 검증하는지입니다. 여기까지 확인해야 나중에 자동화 스크립트로 확장할 때 덜 꼬입니다.

    1. 버킷을 생성합니다.
    2. 샘플 파일을 업로드합니다.
    3. head-object 또는 상세 조회로 메타데이터를 확인합니다.
    4. 브라우저 접근이 있으면 CORS와 공개·비공개 경계를 분리합니다.

    Cloudflare R2: AWS CLI를 재활용하되 endpoint를 고정해야 합니다

    R2는 공식 문서 기준으로 https://<ACCOUNT_ID>.r2.cloudflarestorage.com 엔드포인트를 사용하고, S3 API에서 bucket region은 auto를 씁니다. 이 조합이 중요합니다. 인증 정보만 바꾸고 endpoint를 빼먹으면 AWS CLI는 자연스럽게 AWS S3 쪽으로 요청을 보냅니다. 여기서 생기는 실패는 얼핏 보면 권한 문제처럼 보여서 더 헷갈립니다. 원인은 단순한데 증상은 복잡하게 보여요. 초반에 한 번쯤은 꼭 삽질하게 되는 포인트입니다.

    aws configure --profile r2
    # AWS Access Key ID: <R2_ACCESS_KEY_ID>
    # AWS Secret Access Key: <R2_SECRET_ACCESS_KEY>
    # Default region name: auto
    # Default output format: json
    
    export R2_ENDPOINT="https://<ACCOUNT_ID>.r2.cloudflarestorage.com"
    
    aws s3api create-bucket \
      --bucket my-r2-bucket \
      --endpoint-url "$R2_ENDPOINT" \
      --profile r2
    
    aws s3 cp ./backup.tar.gz s3://my-r2-bucket/backup.tar.gz \
      --endpoint-url "$R2_ENDPOINT" \
      --profile r2
    
    aws s3api head-object \
      --bucket my-r2-bucket \
      --key backup.tar.gz \
      --endpoint-url "$R2_ENDPOINT" \
      --profile r2

    실무 팁은 간단합니다. R2용 profile과 endpoint 환경변수를 아예 분리해두세요. 매 명령마다 직접 치는 습관은 결국 한 번 빠뜨리게 됩니다. 그리고 테스트 초반에는 aws s3 ls보다 aws s3api head-object를 먼저 보시는 걸 권합니다. 존재 여부뿐 아니라 객체 키, 크기, 수정 시각, ETag를 바로 확인할 수 있어서 자동화 검증에 더 잘 맞습니다.

    Cloudflare R2 버킷 생성과 업로드 설정 흐름을 보여주는 이미지

    R2를 AWS CLI로 다루는 실제 흐름을 보여주는 이미지입니다. endpoint 설정 위치가 핵심입니다.

    AWS S3: 리전과 버킷 소유권 모델을 같이 정리하는 게 낫습니다

    S3는 익숙해서 오히려 대충 넘어가기 쉽습니다. 그런데 버킷 생성 단계에서 리전과 소유권 제어를 같이 잡아두면 뒤가 편합니다. 특히 us-east-1 이외 리전에서는 LocationConstraint를 명시해야 하고, ACL 기반 접근을 굳이 유지할 이유가 없다면 BucketOwnerEnforced로 가는 편이 팀 운영상 깔끔합니다.

    aws s3api create-bucket \
      --bucket my-s3-bucket-example \
      --region ap-northeast-2 \
      --create-bucket-configuration LocationConstraint=ap-northeast-2 \
      --object-ownership BucketOwnerEnforced
    
    aws s3 cp ./backup.tar.gz s3://my-s3-bucket-example/backup.tar.gz
    
    aws s3api head-object \
      --bucket my-s3-bucket-example \
      --key backup.tar.gz

    여기서 핵심은 단순 업로드 성공이 아닙니다. head-object 결과를 보고 올바른 버킷·키에 올라갔는지, 메타데이터가 비어 있거나 잘못 덮이지 않았는지, 자동화가 예상한 리전에 들어갔는지를 확인해야 합니다. S3에서 흔한 문제는 인증 실패보다도 잘못된 기본값으로도 성공해 버리는 문제입니다. 예를 들어 잘못된 키 경로, 다른 계정, 다른 버킷 접두사로 업로드해도 명령 자체는 끝나기 때문에 검증 단계를 생략하면 나중에 배포 파이프라인에서 터집니다.

    GCS: 권한 모델을 먼저 단순화하면 뒤가 덜 꼬입니다

    GCS는 최근 gcloud storage 계열 명령이 잘 정리돼 있어서 CLI 사용감이 괜찮은 편입니다. 제가 권하는 시작점은 버킷을 만들 때부터 --uniform-bucket-level-access를 켜는 것입니다. 객체마다 ACL을 따로 만지는 모델은 초기엔 유연해 보여도, 운영자가 늘어나고 서비스 계정이 늘어나면 사고 지점이 늘어나더라고요.

    gcloud storage buckets create gs://my-gcs-bucket-example \
      --location=ASIA-NORTHEAST3 \
      --uniform-bucket-level-access
    
    gcloud storage cp ./backup.tar.gz gs://my-gcs-bucket-example/backup.tar.gz
    
    gcloud storage ls -L gs://my-gcs-bucket-example/backup.tar.gz

    이 흐름에서는 ls보다 ls -L 쪽을 선호합니다. 단순 존재 여부만 보지 말고 저장 클래스, 메타데이터, 업데이트 시각까지 같이 보는 게 좋거든요. GCS는 특히 브라우저 직접 접근과 서비스 계정 접근이 섞일 때 권한 구조가 복잡해지기 쉬워서, 시작부터 버킷 단위 접근을 기본값으로 잡는 편이 훨씬 관리가 쉽습니다.

    브라우저 접근이 있으면 CORS를 뒤로 미루지 마세요

    이 부분은 세 서비스 모두에서 자주 놓칩니다. 백엔드 업로드만 테스트하면 멀쩡한데, 프론트엔드에서 이미지 fetch나 파일 다운로드를 붙이는 순간 막히는 경우가 많습니다. 특히 GCS는 콘솔이나 JSON API 예시와 gcloud storage buckets update --cors-file가 기대하는 파일 구조가 달라서 한 번씩 틀리더라고요.

    [
      {
        "origin": ["https://app.example.com"],
        "method": ["GET", "HEAD"],
        "responseHeader": ["Content-Type", "ETag"],
        "maxAgeSeconds": 3600
      }
    ]
    gcloud storage buckets update gs://my-gcs-bucket-example \
      --cors-file=./cors.json

    근본 원인은 단순합니다. 문서 예시를 섞어 읽으면 JSON API용 구조와 gcloud CLI 입력 구조를 같은 것으로 착각하기 쉽습니다. 실제로 CLI는 최상위 {"cors": [...]}가 아니라 규칙 배열만 있는 파일을 기대합니다. 브라우저에서만 깨지는 문제는 로그가 빈약해서 더 오래 잡히니, CORS는 마지막에 보는 항목이 아니라 초기에 붙여보는 편이 시간을 아낍니다.

    어디서 삽질이 나는지: 흔한 실패 모드와 근본 원인

    여기부터가 제품 소개 글과 실무 글이 갈리는 지점입니다. 표면 증상보다 원인을 알아야 다음에도 덜 틀립니다.

    • R2에서 endpoint 누락: 증상은 인증 실패, 버킷 없음, 예상 밖 계정 리소스 조회처럼 보입니다. 근본 원인은 CLI가 R2가 아니라 AWS S3 기본 엔드포인트로 서명 요청을 보내기 때문입니다. 해결은 --endpoint-url를 강제하거나 R2 전용 profile·환경변수 래퍼 스크립트를 두는 것입니다.
    • S3에서 리전 생성 옵션 누락: 증상은 버킷 생성 실패 또는 리다이렉트성 오류입니다. 근본 원인은 us-east-1 외 리전에서 LocationConstraint가 필요하기 때문입니다. 해결은 리전 값을 단일 변수로 관리하고 생성 시점에 함께 주는 것입니다.
    • GCS에서 CORS 파일 형식 오류: 증상은 명령은 실행됐는데 브라우저에서 preflight나 응답 헤더가 기대와 다르게 보입니다. 근본 원인은 콘솔·JSON API 구조와 gcloud CLI 입력 구조를 혼동한 것입니다.
    • S3 호환이면 기능도 동일하겠지라는 가정: 증상은 특정 SDK 옵션, 부가 API, 권한 동작에서 예상과 다른 결과가 납니다. 근본 원인은 호환 API와 원본 구현을 동일시한 설계 가정입니다. 해결은 사전에 호환성 문서를 대조하고, 사용하는 API 집합을 실제 호출 기준으로 목록화하는 것입니다.
    • 메타데이터 검증 생략: 증상은 업로드는 성공했는데 캐시, 콘텐츠 타입, 브라우저 다운로드 동작이 뒤늦게 꼬입니다. 근본 원인은 존재 확인만 하고 Content-Type, 캐시 헤더, 키 경로, ETag를 안 본 것입니다.

    재현 가능한 시나리오 하나를 적어보겠습니다. 기존 백업 스크립트가 매일 aws s3 cp ./dist/report.tar.gz s3://archive-bucket/daily/만 실행하던 환경이라고 해보죠. 이걸 R2로 옮기면서 액세스 키만 바꾸고 endpoint를 안 넣으면, 스크립트는 여전히 AWS S3 문법으로 실행됩니다. 여기서 운영자는 흔히 IAM 키 문제부터 의심합니다. 그런데 실제 원인은 권한이 아니라 목적지 자체가 바뀌지 않은 것입니다. 이 차이는 로그를 자세히 보기 전까지 잘 안 보입니다. 그래서 저는 마이그레이션 첫 주엔 업로드 직후 head-object를 붙여 결과를 강제로 검증합니다.

    검증과 결과 해석: 무엇을 보면 배포 가능한 상태인지 알 수 있나

    업로드가 한 번 성공했다고 운영 준비가 끝난 건 아닙니다. 최소한 아래 검증은 해야 합니다.

    aws s3api head-object \
      --bucket my-r2-bucket \
      --key backup.tar.gz \
      --endpoint-url "$R2_ENDPOINT" \
      --profile r2
    
    gcloud storage ls -L gs://my-gcs-bucket-example/backup.tar.gz
    • 객체 존재 확인: 키 경로가 정확한지 봅니다. 특히 자동화에서 날짜 접두사나 디렉터리 흉내 키를 많이 틀립니다.
    • 크기 확인: 로컬 파일과 원격 객체 크기가 다르면 중간 실패, 잘못된 파일 참조, 압축 단계 혼선을 의심해야 합니다.
    • 수정 시각 확인: 배포 직후인데 예전 객체를 보고 있으면 잘못된 경로에 올렸을 가능성이 큽니다.
    • 메타데이터 확인: Content-Type, 캐시 관련 헤더, 커스텀 메타데이터가 의도대로 들어갔는지 봅니다.
    • 브라우저 응답 확인: CORS가 필요한 구조라면 Access-Control-Allow-Origin와 preflight 응답을 실제 브라우저에서 확인합니다.

    제가 보는 해석 기준은 단순합니다. GET이 된다와 서비스에 올릴 수 있다는 다른 이야기입니다. 후자는 권한, 메타데이터, 캐시 정책, CORS, 자동화 재실행 시 동작까지 포함합니다. 다운로드 서비스나 이미지 서빙은 특히 브라우저에서만 드러나는 문제가 많아서, curl 테스트만 통과했다고 안심하면 꼭 한 번 더 일하게 됩니다.

    Cloudflare R2와 GCS 업로드 검증 포인트를 보여주는 대시보드 이미지

    업로드 이후 무엇을 확인해야 하는지, 메타데이터와 권한 검증 포인트를 시각화한 이미지입니다.

    비용은 단가보다 경로가 갈라놓습니다

    비용 비교에서 제일 흔한 실수는 저장 단가만 비교하는 것입니다. 실제로는 아래처럼 워크로드를 나눠 봐야 판단이 빨라집니다.

    워크로드 유형 주요 비용 민감도 우선 검토 피해야 할 실수
    백업 보관 위주 저장 기간, 요청 수, 복구 빈도 S3 또는 GCS 기본 구조, R2는 외부 복구 빈도 높을 때 검토 복구 때만 발생하는 전송·검색 패턴을 무시하고 저장 단가만 보는 것
    정적 자산·다운로드 배포 인터넷 방향 전송량, 캐시 전략 Cloudflare R2 SDK 호환만 보고 브라우저·CORS·캐시 정책 검증을 뒤로 미루는 것
    AWS 내부 서비스 연동 권한 통합, 이벤트 처리, 같은 리전 데이터 이동 AWS S3 end-to-end 운영 비용 대신 저장 단가만 보고 외부 스토리지를 섞는 것
    GCP 분석·앱 파이프라인 서비스 계정 권한, GCP 도구 일관성, 데이터 적재 흐름 GCS 팀이 이미 익숙한 IAM·CLI 체계를 버리고 저장소만 따로 최적화하는 것

    여기서 제가 특히 중요하게 보는 건 비용의 예측 가능성입니다. 월말에 갑자기 튀는 비용은 대부분 저장이 아니라 전송 경로에서 나옵니다. 다운로드 트래픽이 핵심인 서비스라면 R2가 눈에 띄고, 내부 연동이 핵심인 서비스라면 S3나 GCS가 더 자연스럽습니다. 이 차이는 가격표의 절대값보다도 장애 대응과 예산 관리에서 훨씬 크게 체감됩니다.

    반대로 “무조건 R2가 낫다”는 식으로 보면 안 됩니다. 예를 들어 데이터가 대부분 AWS 내부 서비스 사이에서 움직이고, 사용자가 직접 다운로드하는 비율이 낮다면 egress 무료라는 장점이 실제 청구서에서 크게 드러나지 않을 수 있습니다. 이럴 때는 새 키 관리, 새로운 엔드포인트, 마이그레이션 검증 비용이 더 큽니다. 저는 이런 경우 굳이 스토리지를 분리하지 않는 편입니다.

    Cloudflare R2와 AWS S3 GCS의 선택 기준을 정리한 비교 인포그래픽

    비용 구조와 사용 패턴에 따라 어떤 스토리지가 유리한지 요약한 비교 인포그래픽입니다.

    그래서 어떤 경우에 뭘 고르면 되나

    여기서는 애매하게 끝내지 않겠습니다. 선택 기준을 조금 더 분명하게 적어보겠습니다.

    • 외부 다운로드가 많고, 기존 AWS CLI·SDK 도구를 최대한 재사용하고 싶다: Cloudflare R2를 먼저 보시면 됩니다. 다만 S3와 완전 동일하다고 가정하지 말고, 사용하는 API 범위를 실제로 대조해보세요.
    • AWS 중심 아키텍처이고 IAM, 이벤트, 주변 서비스 연동이 중요하다: AWS S3가 가장 무난합니다. 저장소를 따로 분리해 얻는 이익보다 운영 복잡성이 더 커질 가능성이 높습니다.
    • GCP 기반 앱과 데이터 파이프라인을 굴리고 있고 운영 단순성이 중요하다: GCS가 맞습니다. 특히 서비스 계정과 버킷 권한을 한 체계로 가져가려면 더 그렇습니다.
    • 브라우저 직접 업로드·다운로드가 많다: 제품 선택만큼 CORS, presigned URL, 메타데이터 검증 설계가 중요합니다. 이 항목을 소홀히 하면 어느 스토리지를 골라도 비슷하게 고생합니다.

    현업에서 최종 선택할 때 저는 한 줄 기준으로 정리합니다. 트래픽이 인터넷으로 많이 나가면 R2 쪽으로 기울고, 애플리케이션과 권한 체계가 특정 클라우드에 깊게 묶여 있으면 그 클라우드의 기본 스토리지로 남습니다. 이 기준이 제일 실수를 덜 만들었습니다.

    FAQ와 공식 문서 체크 포인트

    • R2는 S3와 완전히 같나요? 아닙니다. S3 호환 API입니다. 세부 구현 상태는 Cloudflare의 S3 API compatibility 문서를 확인하셔야 합니다.
    • R2의 가장 큰 차별점은? 공식 가격 문서 기준으로 인터넷 방향 egress 요금이 없다는 점입니다. 다운로드 중심 워크로드일수록 이 차이가 직접적으로 보입니다.
    • S3는 언제 가장 강한가요? 저장소 자체보다 AWS 생태계와의 결합력이 중요할 때입니다. IAM, 이벤트, 주변 서비스 통합을 같이 봐야 합니다.
    • GCS는 언제 선택이 쉬워지나요? Cloud Run, GKE, 데이터 적재, 서비스 계정 운영이 이미 GCP 중심일 때입니다. 팀이 gcloud와 GCP IAM에 익숙하면 마찰이 적습니다.
    • 가격은 어디서 다시 봐야 하나요? 시점에 따라 바뀔 수 있으니 R2, S3, GCS 공식 페이지를 마지막에 다시 확인하는 게 정확합니다.

    마지막 판단만 남기면 이렇습니다. 다운로드 트래픽이 핵심이면 Cloudflare R2, 클라우드 내부 생태계 결합이 핵심이면 AWS S3 또는 GCS입니다. 세 서비스 모두 좋은 저장소지만, 강점이 드러나는 경로가 다릅니다. 그 경로만 먼저 잡아도 선택이 훨씬 쉬워집니다.

    실제 운영 시나리오별로 어떤 선택이 어울리는지 정리한 마무리 가이드 이미지입니다.