13년차의 서버실

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

[카테고리:] cloud

  • [Cloud] Cloudflare Workers 실전 가이드: 서버리스 엣지 컴퓨팅 및 최신 AI 활용 전략

    [Cloud] Cloudflare Workers 실전 가이드: 서버리스 엣지 컴퓨팅 및 최신 AI 활용 전략

    안녕하세요, 13년차 서버실 지킴이입니다. 🤓

    오늘은 제가 최근 홈랩에서 이것저것 실험하면서 꽤나 재미를 보고 있는 기술, 바로 Cloudflare Workers(클라우드플레어 워커스)에 대한 이야기를 풀어볼까 합니다. 다들 서버리스(Serverless)니, 엣지 컴퓨팅(Edge Computing)이니 하는 말 많이 들어보셨을 텐데요. 이게 실제 제 서비스에 어떻게 적용될 수 있을까 궁금하셨던 분들에게 좋은 길잡이가 되었으면 합니다.

    사실 저도 처음엔 ‘이게 뭔가 싶었는데’ 직접 써보고 나니 ‘와, 이거 진짜 물건이네!’ 싶더라고요. 특히 작은 API나 특정 요청을 처리해야 할 때, 굳이 무거운 서버 인스턴스를 띄우지 않고 전 세계 Cloudflare(클라우드플레어) 엣지 네트워크에서 코드를 바로 실행할 수 있다는 점이 매력적이었습니다. 저지연(low-latency)을 줄이고, 비용 효율적으로 서비스를 운영할 수 있는 강력한 도구거든요.

    이번 글에서는 Cloudflare Workers가 무엇인지부터 시작해서, 제가 직접 겪었던 삽질 경험과 해결 과정까지, Cloudflare Workers 실전 가이드: 서버리스 엣지 컴퓨팅 활용 전략을 자세히 알려드리겠습니다. 그럼, 함께 떠나볼까요? 🎉

    Cloudflare Workers와 엣지 컴퓨팅 아키텍처 개요 다이어그램

    Cloudflare Workers의 전체적인 아키텍처와 엣지 컴퓨팅 개념을 시각적으로 보여주는 다이어그램입니다.

    Cloudflare Workers, 대체 뭘까요?

    쉽게 말해, Cloudflare Workers는 Cloudflare의 전 세계 분산된 엣지 네트워크(Edge Network) 위에서 JavaScript나 WebAssembly 코드를 실행할 수 있게 해주는 서버리스 실행 환경(Serverless Execution Environment)이에요. 기존의 서버리스 함수(Lambda, Cloud Functions 등)와 비슷하지만, 사용자와 물리적으로 가장 가까운 엣지 로케이션에서 실행된다는 점에서 엣지 컴퓨팅의 특성을 지니고 있습니다.

    제가 처음 접했을 때 가장 놀랐던 건 ‘콜드 스타트(Cold Start)’ 거의 없이 극도로 빠른 응답 시간을 보여준다는 점이었어요. 기존 서버리스 함수들은 유휴 상태일 때 첫 요청에서 약간의 지연이 생길 수 있거든요. 하지만 Workers는 그런 걱정을 거의 할 필요가 없었습니다.

    Cloudflare Workers의 주요 장점

    • 극도로 낮은 지연 시간(Ultra-low Latency): 사용자와 가장 가까운 엣지 서버에서 코드가 실행되므로, 응답 시간이 획기적으로 단축됩니다. 이는 글로벌 네트워크를 활용한 개발자 경험 개선에 크게 기여합니다.
    • 뛰어난 확장성(Massive Scalability): Cloudflare의 강력한 인프라 위에서 작동해서 트래픽이 폭증해도 자동으로 확장되고 안정적인 서비스를 제공합니다. 제가 직접 트래픽 테스트를 해보니 정말 안정적이더라고요.
    • 비용 효율성(Cost-effectiveness): 사용량 기반(pay-per-use) 요금 모델로, 요청 수에 비례하여 비용이 발생합니다. 특히 무료 티어(Free Tier)가 넉넉해서 소규모 서비스나 실험에 정말 좋습니다.
    • 개발자 친화적(Developer-friendly): JavaScript/TypeScript 기반으로 익숙한 언어로 개발할 수 있고, Wrangler CLI 등 개발 도구도 잘 갖춰져 있어요.

    Cloudflare Workers 실전 구현: 첫 Workers 배포하기

    자, 이제 이론은 충분히 봤으니 직접 한번 만들어보고 배포해보는 시간을 가져볼까요? 제가 홈랩에서 했던 과정을 그대로 따라 해보시면 금방 첫 Workers를 만나보실 수 있을 겁니다. 준비물은 Cloudflare 계정 하나면 충분해요!

    1. Cloudflare 계정 생성 및 로그인

    아직 Cloudflare 계정이 없으시다면, Cloudflare 웹사이트에서 가입하세요. 무료 계정으로도 충분합니다. 계정을 생성하고 로그인하면 준비 끝입니다.

    2. Wrangler CLI 설치

    Cloudflare Workers를 개발하고 배포하는 데는 wrangler라는 CLI(Command Line Interface) 도구를 사용하는 것이 가장 편리합니다. Node.js가 설치되어 있어야 합니다.

    
    npm install -g wrangler
    

    설치가 완료되면, 터미널에서 wrangler login 명령어를 실행하여 Cloudflare 계정에 로그인합니다.

    
    wrangler login
    

    이 명령어를 실행하면 브라우저가 열리고 Cloudflare 인증 페이지로 리다이렉트됩니다. 인증을 완료하면 터미널로 돌아와 성공 메시지를 확인할 수 있습니다.

    3. 새 Workers 프로젝트 생성

    이제 새로운 Workers 프로젝트를 생성해봅시다. 최신 개발 환경을 위해 npm create cloudflare 명령어를 사용하는 것을 권장합니다. 여기서는 TypeScript 템플릿을 기반으로 진행하겠습니다.

    
    npm create cloudflare my-first-worker -- --typescript
    cd my-first-worker
    

    이 명령어는 TypeScript 템플릿을 사용하여 기본적인 Workers 프로젝트 구조를 만들어줍니다. my-first-worker 디렉토리로 이동해주세요.

    4. 간단한 Worker 코드 작성

    src/index.ts 파일을 열어보면 다음과 같은 코드를 볼 수 있습니다. 이게 기본 ‘Hello World’ Workers 코드에요.

    
    /**
     * Welcome to Cloudflare Workers! This is your first worker. 
     *
     * - Run `npm run dev` in your terminal to start a development server
     * - Open a browser tab at http://localhost:8787/ to see your worker in action
     * - Run `npm run deploy` to publish your worker
     *
     * Learn more at https://developers.cloudflare.com/workers/ 
     */
    
    export interface Env {
    	// Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime/kv/
    	// MY_KV_NAMESPACE: KVNamespace;
    	//	// Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime/durable-objects/
    	// MY_DURABLE_OBJECT: DurableObjectNamespace;
    	//	// Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime/r2/
    	// MY_BUCKET: R2Bucket;
    }
    
    export default {
    	async fetch(
    		request: Request,
    		env: Env,
    		ctx: ExecutionContext
    	): Promise<Response> {
    		return new Response('Hello Cloudflare Workers!');
    	},
    };
    

    여기서 new Response('Hello Cloudflare Workers!'); 부분을 원하는 메시지로 바꿔볼 수도 있어요. 예를 들어, 제가 좋아하는 메시지인 ’13년차 서버실에서 보내는 메시지!’로 바꿔보겠습니다.

    
    // ... (생략)
    
    export default {
    	async fetch(
    		request: Request,
    		env: Env,
    		ctx: ExecutionContext
    	): Promise<Response> {
    		return new Response('13년차 서버실에서 보내는 메시지! 안녕하세요!');
    	},
    };
    

    5. Workers 배포

    코드를 수정했다면, 이제 Cloudflare 엣지 네트워크에 배포할 차례입니다. wrangler deploy 명령어를 사용하면 됩니다. 배포 시 Workers의 이름은 wrangler.toml 파일에 설정된 name을 따릅니다.

    
    wrangler deploy
    

    이 명령어를 실행하면 코드가 컴파일되고 Cloudflare에 업로드됩니다. 몇 초 안에 배포가 완료되고, 배포된 Workers의 URL을 터미널에서 확인할 수 있어요. 🎉 드디어 됐다!

    Cloudflare Workers 대시보드 배포 설정 화면

    Cloudflare Workers 대시보드에서 방금 배포한 Workers의 설정 화면을 보여주는 스크린샷입니다.

    ⚠️ 주의사항 및 트러블슈팅 경험

    제가 Cloudflare Workers를 사용하면서 겪었던 몇 가지 주의사항과 ‘아, 이거 때문에 삽질 좀 했네’ 싶었던 경험들을 공유합니다.

    1. 런타임 환경 이해하기

    • Node.js 호환성: Workers는 Node.js 런타임이 아니에요. V8 엔진 기반의 독자적인 런타임인 Workers Runtime을 사용하거든요. 그래서 Node.js의 모든 내장 모듈(fs, http 등)을 사용할 수 없습니다. 특정 Node.js 모듈이 필요하다면, Cloudflare의 Node.js 호환성 문서를 참고하거나, WebAssembly(Wasm) 같은 대안을 고려해봐야 합니다. 처음엔 Node.js 모듈을 무심코 사용했다가 에러를 뿜어서 당황했던 기억이 있네요.
    • 글로벌 객체: Node.js의 process나 브라우저의 window 같은 전역 객체(Global Object)는 Workers 환경에 없어요. 대신 self나 globalThis를 사용해야 합니다.

    2. 리소스 제한(Limits)

    Cloudflare Workers는 매우 강력하지만, 엣지 환경의 특성상 몇 가지 리소스 제한이 있습니다. 저도 이 제한 때문에 몇 번 코드를 수정해야 했었죠. (이 정보는 2026년 9월 현재 기준입니다.)

    • CPU 시간: 단일 요청 처리 시 50ms(무료 플랜) ~ 30초(유료 플랜)의 CPU 시간이 주어집니다. 복잡한 연산보다는 빠르고 가벼운 요청 처리에 적합합니다.
    • 메모리: 약 128MB의 메모리 제한이 있어요. 큰 데이터를 처리하거나 복잡한 상태를 유지하는 데는 한계가 있을 수 있습니다.
    • 요청/응답 본문 크기: 기본적으로 50MB로 제한돼요. 큰 파일을 업로드하거나 다운로드하는 프록시를 만들 때는 이 점을 고려해야 합니다.

    3. 로컬 개발 환경

    wrangler dev 명령어를 사용하면 로컬에서 Workers를 개발하고 테스트할 수 있어요. 이게 진짜 편하더라고요. 실제 배포하기 전에 충분히 테스트해보는 습관을 들이는 것이 중요합니다. 💡

    
    wrangler dev
    

    이 명령어를 실행하면 로컬 개발 서버가 시작되고, 브라우저에서 http://localhost:8787/로 접속하여 Workers의 동작을 바로 확인할 수 있습니다.

    검증 및 결과 확인

    배포된 Workers가 잘 작동하는지 확인하는 과정도 중요하죠. 제가 배포한 my-first-worker의 URL을 확인한 뒤, curl 명령어나 웹 브라우저로 접속해보면 됩니다.

    1. Workers URL 확인

    배포가 성공적으로 끝나면 터미널에 다음과 비슷한 URL이 표시됩니다.

    
    # 예시 URL
    https://my-first-worker.YOUR_ACCOUNT_ID.workers.dev/
    

    2. 웹 브라우저 또는 curl로 확인

    해당 URL로 접속하면, 우리가 작성했던 메시지 '13년차 서버실에서 보내는 메시지! 안녕하세요!'가 잘 출력되는 것을 확인할 수 있어요.

    
    curl https://my-first-worker.YOUR_ACCOUNT_ID.workers.dev/
    

    응답으로 13년차 서버실에서 보내는 메시지! 안녕하세요!가 보인다면 성공입니다! ✅

    Cloudflare 대시보드에서도 Workers의 로그와 분석(Analytics)을 확인할 수 있어요. 얼마나 많은 요청이 들어왔고, CPU 시간은 얼마나 사용했는지 등을 한눈에 볼 수 있어서 모니터링하기 정말 편리하더라고요.

    Cloudflare Workers 성능 지표 대시보드 그래프

    Cloudflare Workers 대시보드에서 Workers의 요청 수, CPU 시간 등 성능 지표를 보여주는 그래프나 차트입니다.

    🌟 최신 Cloudflare Workers의 주요 변화 및 신기능

    Cloudflare Workers는 지난 몇 달간 눈부신 발전을 거듭하며 엣지 컴퓨팅의 지평을 넓히고 있습니다. 특히 AI 시대의 엣지 컴퓨팅을 선도하는 새로운 기능들이 대거 추가되었어요. 이 변화들을 통해 Workers는 단순한 서버리스 함수를 넘어, 강력한 엣지 애플리케이션 플랫폼으로 진화하고 있습니다.

    • Workers AI: 엣지 AI의 새로운 지평을 열었습니다. Workers AI를 통해 개발자들은 Cloudflare의 글로벌 네트워크에서 직접 AI 모델을 배포하고 추론을 실행할 수 있습니다. 텍스트 생성, 이미지 분류, 임베딩 등 다양한 AI 작업을 저지연으로 처리할 수 있어, 사용자에게 더 빠르고 개인화된 경험을 제공할 수 있게 되었죠. 이는 서버리스 AI의 가능성을 극대화합니다.
    • Vectorize (벡터 데이터베이스): Workers AI와 함께 사용하기 좋은 벡터 데이터베이스 서비스인 Vectorize가 정식 출시되었습니다. 벡터 임베딩을 저장하고 검색하여 AI 기반 검색, 추천 시스템 등을 엣지에서 구현할 수 있게 해줍니다.
    • D1 (서버리스 SQL 데이터베이스): Cloudflare D1은 SQLite 기반의 서버리스 SQL 데이터베이스로, Workers와 긴밀하게 통합되어 엣지에서 데이터를 영속적으로 저장하고 관리할 수 있게 합니다. 이제 Workers만으로도 복잡한 데이터 기반 애플리케이션을 구축하는 것이 더욱 쉬워졌습니다.
    • Cloudflare Queues: 안정적인 메시징 시스템인 Cloudflare Queues를 통해 Workers 간 또는 외부 시스템과의 비동기 통신을 더욱 견고하게 구축할 수 있습니다. 이는 복잡한 분산 시스템 아키텍처를 엣지에서 구현하는 데 필수적인 요소입니다.

    이러한 기능들은 Workers가 단순한 “함수”를 넘어, 완전한 “엣지 애플리케이션 플랫폼”으로 성장했음을 보여줍니다. 글로벌 네트워크를 활용한 개발자 경험은 더욱 풍부해지고 있습니다.

    마무리하며: 엣지 컴퓨팅의 미래를 Workers와 함께

    오늘은 13년차 인프라 엔지니어의 시선으로 Cloudflare Workers에 대해 자세히 알아보고, 직접 배포까지 해보는 실전 가이드를 소개해드렸습니다. 최신 업데이트를 통해 Workers가 단순한 서버리스 함수를 넘어, 엣지 AI와 데이터 스토리지를 결합한 강력한 플랫폼으로 진화하고 있음을 확인했습니다.

    처음엔 단순히 ‘클라우드플레어에서 코드 돌리는 건가?’ 싶었는데, 직접 써보고 삽질도 해보면서 엣지 컴퓨팅의 진정한 가치를 깨달았네요. 특히 지연 시간에 민감한 서비스나 글로벌 사용자들을 대상으로 하는 서비스에는 Workers가 정말 강력한 대안이 될 수 있겠다는 생각이 들었습니다.

    Cloudflare Workers의 다양한 활용 사례

    • API 게이트웨이(API Gateway): 복잡한 백엔드 없이 간단한 API 엔드포인트를 만들거나, 기존 API에 대한 인증/인가 레이어를 추가할 수 있어요.
    • 정적 웹사이트 라우팅(Static Site Routing): 여러 정적 사이트를 하나의 도메인 아래에서 라우팅하거나, A/B 테스트를 위한 트래픽 분배에 활용할 수 있습니다.
    • 이미지 최적화(Image Optimization): 요청 시점에 이미지를 동적으로 리사이징하거나 포맷을 변환하여 전달할 수 있어요.
    • 보안 및 필터링(Security & Filtering): 악성 트래픽을 차단하거나, 특정 IP 대역의 접근을 제한하는 등 보안 로직을 엣지에서 구현할 수 있습니다.
    • 데이터 캐싱(Data Caching): 자주 요청되는 데이터를 엣지에 캐싱하여 백엔드 부하를 줄이고 응답 속도를 높일 수 있어요.
    • 엣지 AI 애플리케이션: Workers AI를 활용하여 실시간 번역, 콘텐츠 모더레이션, 개인화된 추천 등 AI 기반 서비스를 엣지에서 직접 구현할 수 있습니다.

    저의 작은 경험과 삽질이 Cloudflare Workers를 시작하려는 분들에게 조금이나마 도움이 되었기를 바랍니다. 다음번에는 Workers KV나 Durable Objects 같은 Workers의 고급 기능들을 활용하는 방법에 대해 더 깊이 파고들어 볼 예정입니다. 기대해주세요! 😄

    Cloudflare Workers 주요 장점 및 활용 사례 요약 인포그래픽

    Cloudflare Workers의 주요 장점과 활용 사례를 요약하는 인포그래픽입니다.

    🔄 마지막 업데이트: 2026년 09월

  • [Cloud] Podman vs Docker: Rootless 컨테이너 보안 및 활용 가이드

    [Cloud] Podman vs Docker: Rootless 컨테이너 보안 및 활용 가이드

    Podman vs Docker: Rootless 컨테이너 보안 및 활용 가이드

    인프라 엔지니어의 고민: 더 안전한 컨테이너 운영을 찾아서

    안녕하세요, 13년차 서버실 지킴이입니다. 요즘 인프라 엔지니어라면 컨테이너(Container) 기술을 빼놓고 이야기하기 어렵죠. 저도 처음엔 도커(Docker)가 세상에 나왔을 때, ‘와, 이거 진짜 물건이다!’ 하면서 열광했던 기억이 납니다. 개발 환경부터 운영 환경까지 컨테이너 덕분에 정말 많은 게 편리해졌거든요. 그런데 말입니다, 편리함 뒤에는 늘 그림자가 따르기 마련이더라고요. 특히 보안(Security) 측면에서는 늘 마음 한편이 불안했어요.

    도커 데몬(Docker daemon)이 항상 루트(root) 권한으로 실행된다는 점, 그리고 컨테이너 탈출(Container Escape) 같은 잠재적인 보안 위협들은 저를 계속해서 고민하게 만들었죠. 홈랩(Home Lab)에서 다양한 서비스를 돌리면서 ‘어떻게 하면 좀 더 안전하게 컨테이너를 운영할 수 있을까?’ 하는 고민은 저만의 숙제는 아닐 겁니다. 혹시 여러분도 이런 고민 해보신 적 있으신가요? 이 지점에서 제가 눈여겨보게 된 것이 바로 Podman(팟맨)입니다.

    오늘 글에서는 컨테이너 보안의 새로운 대안으로 떠오른 Podman과 우리가 오랫동안 써왔던 Docker를 비교하고, 특히 Rootless 컨테이너(Rootless Container) 개념을 중심으로 Podman의 장점과 활용법을 자세히 알려드리려고 합니다. 제가 직접 삽질해가며 얻은 경험들을 바탕으로, 여러분의 컨테이너 운영에 도움이 될 만한 실질적인 팁들을 공유해볼게요! 💡

    Podman과 Docker 아키텍처 비교 다이어그램

    Podman과 Docker의 아키텍처 비교. Podman은 데몬 없이 사용자 프로세스로 실행되어 보안 이점을 가집니다.

    Rootless 컨테이너란 무엇인가?

    쉽게 말해, Rootless 컨테이너는 루트(root) 권한 없이, 일반 사용자(non-root user) 권한으로 실행되는 컨테이너를 말합니다. 기존 도커는 도커 데몬(Docker Daemon)이 호스트(Host) 시스템에서 루트 권한으로 실행되고, 이 데몬이 컨테이너를 관리하는 구조였죠. 이게 왜 문제가 되냐면, 만약 컨테이너에 보안 취약점이 있어서 외부 공격자가 컨테이너를 탈출(Escape)하는 데 성공하면, 호스트 시스템의 루트 권한까지 획득할 수 있는 심각한 상황이 발생할 수 있기 때문입니다. ⚠️

    Podman은 태생부터 이런 보안 문제를 염두에 두고 설계되었어요. Podman은 도커와 달리 데몬(Daemon)이 없거든요. 즉, 백그라운드에서 항상 루트 권한으로 돌아가는 프로세스가 없다는 뜻입니다. Podman 명령어를 실행하면, 해당 명령어는 일반 사용자 권한으로 컨테이너를 생성하고 관리하게 됩니다. 처음 이걸 알았을 땐 ‘아, 이거다!’ 싶었거든요. 이렇게 되면 컨테이너를 탈출하더라도 일반 사용자 권한까지만 접근할 수 있으니, 호스트 시스템의 피해를 최소화할 수 있습니다. 보안적인 측면에서 봤을 때 정말 큰 이점이라고 할 수 있어요.

    Podman vs Docker: 핵심 차이점 비교

    Podman과 Docker는 컨테이너를 관리하는 도구라는 공통점이 있지만, 내부적으로는 꽤 많은 차이가 있습니다. 제가 직접 써보면서 느낀 주요 차이점들을 표로 정리해봤어요.

    특징 Podman Docker
    아키텍처 (Architecture) 데몬리스 (Daemonless): 백그라운드 데몬 없음. 사용자 프로세스로 실행. 데몬 기반 (Daemon-based): Docker 데몬이 루트 권한으로 백그라운드 실행.
    루트 권한 (Root Privileges) 기본 Rootless: 일반 사용자 권한으로 컨테이너 실행. 데몬이 루트 권한으로 실행. Rootless 모드는 실험적 또는 부가 기능.
    보안 (Security) 호스트 시스템에 대한 공격 표면(Attack Surface) 감소. 데몬 취약점 발생 시 호스트 시스템 전체 위험.
    이미지 빌드 (Image Build) Buildah(빌다)를 내부적으로 활용 (podman build). Docker Build 엔진 사용 (docker build).
    오케스트레이션 (Orchestration) Kubernetes Pods(쿠버네티스 파드)와 높은 호환성. podman generate kube로 YAML 생성 가능. Docker Swarm(도커 스웜), Docker Compose(도커 컴포즈) 지원.
    명령어 호환성 (CLI Compatibility) 대부분의 docker 명령어와 유사 (podman으로 대체 가능). 표준 Docker CLI.
    Podman vs Docker 핵심 차이점 비교 인포그래픽

    Podman과 Docker의 주요 특징들을 한눈에 비교한 표입니다. 특히 데몬리스 아키텍처와 Rootless 기본 지원이 Podman의 가장 큰 차이점이죠.

    Podman으로 Rootless 컨테이너 실행하기

    그럼 이제 Podman으로 Rootless 컨테이너를 직접 실행해보면서 그 편리함을 느껴볼 시간입니다! 제가 홈랩에서 주로 쓰는 CentOS Stream 9나 Fedora, 또는 Ubuntu 22.04 이상 환경에서는 Podman 설치가 아주 쉬워요.

    1. 팟맨 설치하기

    저는 CentOS Stream 9에서 진행했습니다. 여러분의 OS에 맞게 설치해주세요.

    
    # CentOS/Fedora
    sudo dnf install podman -y
    
    # Ubuntu/Debian
    sudo apt update
    sudo apt install podman -y
    

    설치가 완료되면, podman info 명령어로 잘 설치되었는지 확인해볼 수 있습니다.

    
    podman info
    

    여기서 중요한 건, 루트 권한 없이 실행해도 잘 동작한다는 점이에요! 🎉

    2. Rootless 컨테이너 실행해보기

    가장 기본적인 컨테이너 실행부터 시작해볼까요? Alpine 리눅스 컨테이너를 실행해보겠습니다.

    
    podman run --rm -it alpine sh
    

    컨테이너 내부에서 whoami 명령어를 실행해보면, root로 나올 거예요. ‘어? Rootless라면서 왜 컨테이너 안에서는 root지?’ 하고 저처럼 당황하실 수 있는데, 이건 컨테이너 내부의 루트 사용자가 호스트 시스템의 일반 사용자에 매핑(mapping)되었기 때문입니다. 즉, 컨테이너 안에서는 여전히 강력한 권한을 가지지만, 호스트 시스템에는 아무런 영향을 주지 못하는 ‘가짜 루트’인 셈이죠.

    이제 Nginx 웹 서버를 Rootless로 띄워볼까요?

    
    podman run -d --name my-nginx -p 8080:80 nginx
    

    잘 실행되었는지 확인하고, 웹 브라우저나 curl로 접속해보세요.

    
    podman ps
    curl localhost:8080
    

    정상적으로 Nginx 초기 페이지가 보인다면 성공입니다! ✅

    3. Podman의 파드(Pod) 기능 활용해보기

    Podman은 쿠버네티스(Kubernetes)의 파드(Pod) 개념을 네이티브(Native)하게 지원합니다. 여러 컨테이너가 동일한 네트워크 네임스페이스(Network Namespace)와 저장소(Storage)를 공유해야 할 때 아주 유용하거든요. 제가 이걸 처음 써봤을 때, ‘홈랩에 간이 쿠버네티스를 구축한 느낌인데?’ 하면서 감탄했어요. 🤭

    먼저 파드를 생성합니다.

    
    podman pod create --name my-web-app-pod -p 8080:80
    

    이제 이 파드 안에 Nginx와 다른 컨테이너를 넣어보죠.

    
    podman run -d --pod my-web-app-pod --name webserver nginx
    podman run -d --pod my-web-app-pod --name data-logger alpine sleep 1h
    

    podman pod ps 명령어로 파드와 그 안에 있는 컨테이너들을 확인할 수 있습니다.

    
    podman pod ps
    

    4. 이미지 빌드하기

    Podman은 Dockerfile을 이용한 이미지 빌드도 완벽하게 지원합니다. docker build 대신 podman build를 사용하면 되죠. Dockerfile 예시를 하나 만들어볼게요.

    
    # Dockerfile
    FROM alpine:latest
    RUN apk add --no-cache nginx
    COPY ./index.html /usr/share/nginx/html/index.html
    EXPOSE 80
    CMD ["nginx", "-g", "daemon off;"]
    

    그리고 간단한 index.html 파일도 만들어줍니다.

    
    <!DOCTYPE html>
    <html>
    <head>
        <title>Hello Podman!</title>
    </head>
    <body>
        <h1>Welcome to my Rootless Nginx with Podman!</h1>
    </body>
    </html>
    

    이제 이 두 파일이 있는 디렉토리에서 이미지를 빌드합니다.

    
    podman build -t my-custom-nginx .
    

    빌드된 이미지를 확인하고 실행해보세요.

    
    podman images
    podman run -d --name my-custom-web -p 8080:80 my-custom-nginx
    curl localhost:8080
    
    Podman 이미지 빌드 프로세스 다이어그램

    Podman을 이용한 이미지 빌드 과정입니다. Docker와 거의 동일한 명령어로 편리하게 이미지를 만들 수 있어요.

    Podman 운영 시 주의사항

    제가 Podman을 처음 접하면서 겪었던 몇 가지 ‘삽질’과 그 해결책을 공유합니다. 저처럼 헤매지 마시길 바라요! ㅎㅎ

    1. 낮은 포트(Low Port) 바인딩 문제: Rootless 컨테이너는 기본적으로 1024번 이하의 포트(예: 80, 443)를 직접 바인딩(Binding)할 수 없습니다. 이는 보안상의 이유로 일반 사용자에게 허용되지 않는 권한이거든요. 처음 Nginx를 80번 포트에 바로 연결하려고 했을 때, ‘왜 안 되지?’ 하면서 한참을 헤맸습니다. 😩
      해결책: 가장 쉬운 방법은 -p 8080:80처럼 1024번보다 높은 포트에 바인딩하는 것입니다. 아니면 Nginx나 Apache 같은 리버스 프록시(Reverse Proxy)를 호스트에 설정해서 80번 포트로 들어오는 트래픽을 컨테이너의 8080번 포트로 포워딩(Forwarding)하는 방법도 있어요. (sysctl net.ipv4.ip_unprivileged_port_start=80 같은 설정을 할 수도 있지만, 보안상 권장하지는 않습니다.)

    2. 볼륨 마운트(Volume Mount) 권한 문제: Rootless 컨테이너에서 호스트의 특정 디렉토리를 볼륨으로 마운트할 때 권한 문제가 발생할 수 있어요. 컨테이너 내부의 UID/GID(User ID/Group ID)가 호스트 시스템의 UID/GID와 매핑되는 방식 때문에 생기는 문제인데, /etc/subuid와 /etc/subgid 파일에 정의된 보조 UID/GID 범위가 적절히 설정되어 있지 않으면 문제가 됩니다.
      해결책: 이 파일들을 수동으로 편집하거나, Podman이 자동으로 설정하도록 합니다. 대부분의 경우 Podman 설치 시 자동으로 설정되지만, 가끔 문제가 생기면 이 파일을 확인해보세요. 저도 특정 디렉토리에 로그를 남기려다가 권한 문제로 고생 좀 했어요.

    3. systemd 통합: Podman은 systemd와 아주 잘 통합됩니다. podman generate systemd 명령어를 사용하면 실행 중인 컨테이너나 파드를 systemd 서비스로 등록할 수 있는 유닛(Unit) 파일을 자동으로 생성해줍니다. 이걸 몰랐을 때는 직접 유닛 파일을 만들었는데, 나중에 이 기능을 알고 얼마나 편했는지 몰라요! 🤩

    검증: Rootless 컨테이너로 더 안전한 운영

    Podman으로 Rootless 컨테이너를 운영하면서 제가 가장 크게 느낀 점은 ‘마음의 평화’입니다. 😅 호스트 시스템에 대한 잠재적인 보안 위협이 크게 줄어들기 때문에, 훨씬 안심하고 컨테이너를 돌릴 수 있게 되었거든요. podman ps, podman inspect 같은 명령어로 컨테이너 상태를 확인해보면, 도커와 거의 동일한 방식으로 작동한다는 걸 알 수 있습니다. 단지 실행 주체가 루트가 아닌 일반 사용자라는 것만 다를 뿐이죠.

    특히 ~/.local/share/containers 경로를 확인해보면, 모든 컨테이너 관련 파일들이 일반 사용자 홈 디렉토리 아래에 생성된 것을 볼 수 있어요. 이게 바로 Rootless 컨테이너의 핵심 증거입니다. 👍

    Rootless 컨테이너 보안 이점 다이어그램

    Rootless 컨테이너는 호스트 시스템의 루트 권한으로부터 격리되어 보안을 강화합니다.

    마무리: 언제 Podman을 써야 할까?

    오늘 Podman과 Docker의 차이점, 그리고 Rootless 컨테이너의 중요성에 대해 이야기해봤습니다. Podman은 다음과 같은 상황에서 특히 강력한 대안이 될 수 있다고 생각해요.

    • 보안이 최우선 고려사항일 때: 특히 다중 사용자 환경이나, 호스트 시스템의 보안을 최대한 강화하고 싶을 때 Rootless Podman은 탁월한 선택입니다.
    • 쿠버네티스 환경을 준비 중일 때: Podman은 쿠버네티스 파드 개념을 네이티브하게 지원하고, podman generate kube 같은 명령어로 쉽게 YAML 파일을 생성할 수 있어 쿠버네티스 학습 및 연동에 유리합니다.
    • 경량화된 컨테이너 환경이 필요할 때: 백그라운드 데몬 없이 필요한 시점에만 프로세스가 실행되므로, 리소스 효율성 측면에서도 장점이 있어요.

    물론 Docker도 여전히 막강한 생태계와 성숙한 도구들을 가지고 있습니다. Docker Compose나 Docker Swarm 같은 기능은 아직 Podman보다 더 안정적이고 편리한 측면이 많죠. 따라서 어떤 도구를 선택할지는 여러분의 특정 요구사항과 환경에 따라 달라질 겁니다.

    저의 13년차 경험상, ‘만능 도구’는 없습니다. 하지만 각 도구의 장단점을 정확히 알고 상황에 맞게 사용하는 것이 진정한 엔지니어의 자세라고 생각합니다. Podman은 컨테이너 보안과 효율성을 한 단계 끌어올릴 수 있는 정말 매력적인 도구임은 분명해요. 다음번에는 Podman Compose를 이용한 다중 컨테이너 애플리케이션 배포에 대해 더 깊이 다뤄보도록 하겠습니다. 긴 글 읽어주셔서 감사합니다! 👋

  • [Cloud] Pulumi vs Terraform 비교: IaC 도구 선택 가이드

    IaC 도구 선택, 왜 이렇게 어렵냐고요 😅

    팀에서 인프라 자동화 도구를 새로 도입하려고 할 때, 가장 먼저 나오는 질문이 있죠. “Terraform 쓸까요, Pulumi 쓸까요?”

    저도 몇 년 전에 이 선택 앞에서 한참 고민했거든요. 당시엔 Terraform이 거의 IaC(Infrastructure as Code, 코드로 인프라를 정의하고 관리하는 방식)의 표준처럼 여겨지던 시절이었는데, Pulumi라는 새로운 녀석이 등장하면서 “이거 써야 하나?” 싶었던 기억이 납니다.

    결론부터 말씀드리면 — 둘 다 써봤고, 둘 다 장단점이 뚜렷해요. Pulumi vs Terraform 비교는 단순히 “어느 게 더 좋냐”의 문제가 아니라, 팀 상황과 프로젝트 성격에 따라 달라지는 문제더라고요. 오늘은 13년 동안 인프라 엔지니어로 일하면서 직접 겪은 경험을 바탕으로, 이 두 IaC 도구를 제대로 비교해드리겠습니다.

    Pulumi와 Terraform의 전체적인 구조와 접근 방식 차이를 보여주는 개요 다이어그램

    Terraform과 Pulumi, 각각 어떤 도구인가요?

    Terraform — IaC의 베테랑

    Terraform은 HashiCorp에서 만든 오픈소스 IaC 도구로, 2014년에 처음 출시됐습니다. HCL(HashiCorp Configuration Language)이라는 자체 DSL(Domain-Specific Language, 특정 목적을 위해 만들어진 언어)을 사용하는 게 특징이에요.

    쉽게 말해서, “인프라를 선언적으로 정의”하는 방식입니다. “EC2 인스턴스 이렇게 만들어줘”라고 적어두면, Terraform이 알아서 현재 상태와 비교해서 필요한 작업만 수행하는 거죠.

    # Terraform 예시 — AWS EC2 인스턴스 생성
    resource "aws_instance" "web_server" {
      ami           = "ami-0c55b159cbfafe1f0"
      instance_type = "t3.micro"
    
      tags = {
        Name        = "web-server"
        Environment = "production"
      }
    }
    
    output "instance_ip" {
      value = aws_instance.web_server.public_ip
    }

    처음 보면 “이게 뭔 언어야?” 싶을 수 있는데, 몇 번 써보면 꽤 직관적이더라고요. 특히 인프라 구성을 “읽는” 관점에서는 진짜 편합니다.

    Pulumi — 개발자 친화적인 도전자

    Pulumi는 2018년에 등장한 비교적 젊은 IaC 도구입니다. 가장 큰 차별점은 실제 프로그래밍 언어를 그대로 사용한다는 점이에요. Python, TypeScript, Go, C#, Java 등을 지원하거든요.

    처음 이걸 봤을 때 “오, 이거 개발자들 좋아하겠다” 싶었어요. 인프라 엔지니어보다 소프트웨어 개발자 출신이 많은 팀이라면 특히요.

    # Pulumi 예시 — Python으로 AWS EC2 인스턴스 생성
    import pulumi
    import pulumi_aws as aws
    
    web_server = aws.ec2.Instance(
        "web-server",
        ami="ami-0c55b159cbfafe1f0",
        instance_type="t3.micro",
        tags={
            "Name": "web-server",
            "Environment": "production",
        }
    )
    
    pulumi.export("instance_ip", web_server.public_ip)

    같은 결과물인데, Python 개발자라면 아래 코드가 훨씬 익숙하게 느껴질 겁니다. 이게 Pulumi의 핵심 가치예요.

    Pulumi vs Terraform 핵심 차이점 비교

    자, 이제 본격적으로 비교해 봅시다. 제가 직접 두 도구를 써보면서 느낀 차이점들을 정리했어요.

    비교 항목 Terraform Pulumi
    언어 HCL (자체 DSL) Python, TypeScript, Go, C# 등
    학습 곡선 HCL은 쉽지만, 복잡한 로직은 어려움 언어는 익숙하지만 Pulumi 개념 학습 필요
    상태 관리 로컬 파일 또는 원격 백엔드 Pulumi Cloud 또는 자체 백엔드
    프로바이더 생태계 매우 방대 (성숙한 생태계) 성장 중 (Terraform 프로바이더 브리지 지원)
    테스트 제한적 (Terratest 등 별도 도구 필요) 언어 기본 테스트 프레임워크 활용 가능
    커뮤니티 매우 활발, 레퍼런스 풍부 성장 중
    라이선스 BSL 1.1 (v1.5.5부터 변경) Apache 2.0 (오픈소스)
    엔터프라이즈 기능 Terraform Cloud/Enterprise Pulumi Cloud

    ⚠️ 라이선스 변경 이슈 주의! Terraform은 2023년 8월에 라이선스를 MPL 2.0에서 BSL(Business Source License) 1.1로 변경했습니다. 이 때문에 OpenTofu라는 포크 프로젝트가 생겼을 정도로 커뮤니티에서 논란이 됐었어요. 상업적 목적으로 사용할 때는 라이선스 조건을 꼭 확인하세요.

    Terraform의 plan/apply 워크플로우와 Pulumi의 preview/up 워크플로우를 나란히 비교한 다이어그램

    실전에서 느끼는 차이 — 직접 써보니까요

    Terraform이 빛나는 순간들

    제가 Terraform을 처음 도입했을 때가 2019년쯤이었는데, 당시 팀에 인프라 엔지니어가 저 포함 3명이었어요. 개발팀과 협업할 일이 많았는데, HCL 파일을 코드 리뷰할 때 개발자들도 꽤 잘 읽더라고요. 선언적 문법이라 “이 리소스가 이렇게 생겼구나”를 직관적으로 파악하기 좋거든요.

    특히 이런 상황에서 Terraform이 강점을 발휘했어요:

    • 💡 팀에 인프라 전문가 비율이 높을 때 — HCL에 익숙해지면 오히려 더 깔끔하게 관리됨
    • 💡 레퍼런스가 중요할 때 — 거의 모든 클라우드 리소스에 대한 예제가 넘쳐남
    • 💡 안정성이 최우선일 때 — 10년 넘은 도구라 예측 가능한 동작이 보장됨
    • 💡 모듈 재사용이 핵심일 때 — Terraform Registry의 공개 모듈 생태계가 압도적
    # Terraform 모듈 활용 예시
    module "vpc" {
      source  = "terraform-aws-modules/vpc/aws"
      version = "~> 5.0"
    
      name = "my-vpc"
      cidr = "10.0.0.0/16"
    
      azs             = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
      private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
      public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
    
      enable_nat_gateway = true
    
      tags = {
        Environment = "production"
        Terraform   = "true"
      }
    }

    Terraform Registry에서 검증된 VPC 모듈 몇 줄로 완성이에요. 이런 거 보면 “역시 생태계가 갑이다” 싶죠.

    Terraform의 아픈 부분 😅

    근데 솔직히 말씀드리면, 복잡한 로직 처리할 때는 좀 힘들어요. 예를 들어 “조건에 따라 리소스 개수를 동적으로 결정”하려면…

    # Terraform에서 동적 리소스 생성 — 이게 직관적이지 않음
    resource "aws_security_group_rule" "ingress_rules" {
      count = length(var.ingress_ports)
    
      type              = "ingress"
      from_port         = var.ingress_ports[count.index]
      to_port           = var.ingress_ports[count.index]
      protocol          = "tcp"
      cidr_blocks       = ["0.0.0.0/0"]
      security_group_id = aws_security_group.main.id
    }
    
    # for_each를 쓰면 좀 낫지만, 여전히 복잡한 로직엔 한계가 있음
    resource "aws_instance" "servers" {
      for_each = var.server_configs
    
      ami           = each.value.ami
      instance_type = each.value.instance_type
    
      tags = {
        Name = each.key
      }
    }

    count, for_each 같은 메타 인수를 쓰다 보면 “이게 프로그래밍이 아니라 퍼즐 푸는 느낌”이 들 때가 있어요. 저도 처음엔 이게 뭔가 싶었는데, 익숙해지는 데 시간이 좀 걸렸습니다 ㅎㅎ

    Pulumi가 빛나는 순간들

    반면에 Pulumi는 개발자 팀에서 진가를 발휘하더라고요. 제가 사이드 프로젝트로 홈랩 인프라를 Pulumi(Python)로 관리해봤는데, 이런 점이 진짜 좋았어요:

    # Pulumi Python — 복잡한 로직도 그냥 Python으로
    import pulumi
    import pulumi_aws as aws
    
    # 환경별 설정을 딕셔너리로 관리
    env_configs = {
        "production": {"instance_type": "t3.medium", "count": 3},
        "staging":    {"instance_type": "t3.small",  "count": 1},
        "dev":        {"instance_type": "t3.micro",  "count": 1},
    }
    
    config = pulumi.Config()
    env = config.require("environment")
    current_config = env_configs[env]
    
    # 그냥 for 루프로 인스턴스 생성 — 이게 얼마나 자연스러운지!
    instances = []
    for i in range(current_config["count"]):
        instance = aws.ec2.Instance(
            f"web-server-{i}",
            ami="ami-0c55b159cbfafe1f0",
            instance_type=current_config["instance_type"],
            tags={
                "Name": f"web-server-{i}",
                "Environment": env,
            }
        )
        instances.append(instance)
    
    # 결과 출력도 Python 리스트 컴프리헨션으로
    pulumi.export("instance_ids", [inst.id for inst in instances])

    이거 처음 써봤을 때 “드디어 됐다!” 싶었어요. 프로그래밍 언어의 모든 기능을 그대로 쓸 수 있으니까, 복잡한 조건 처리나 반복 작업이 훨씬 자연스럽거든요.

    Pulumi가 특히 유리한 상황:

    • 💡 팀이 개발자 중심일 때 — TypeScript, Python 등 이미 아는 언어로 바로 시작 가능
    • 💡 인프라 로직이 복잡할 때 — 조건문, 반복문, 함수 등을 자유롭게 사용
    • 💡 테스트 자동화가 중요할 때 — pytest, Jest 등 기존 테스트 도구 그대로 활용
    • 💡 기존 코드베이스와 통합할 때 — 앱 코드와 인프라 코드를 같은 언어로 관리

    Pulumi의 아픈 부분 ⚠️

    근데 Pulumi도 완벽하진 않아요. 제가 겪은 몇 가지 불편한 점들:

    • 레퍼런스 부족 — 문제 생겼을 때 구글링해도 Terraform만큼 자료가 안 나와요. 특히 엣지 케이스는 직접 디버깅해야 하는 경우가 많았음
    • Pulumi Cloud 의존성 — 기본 상태 관리가 Pulumi Cloud를 통하는 방식이라, 자체 관리하려면 별도 설정이 필요
    • 언어별 지원 수준 차이 — TypeScript 지원이 가장 성숙하고, 다른 언어는 업데이트 시점이 조금씩 달라요
    • 팀 온보딩 — 인프라 엔지니어가 특정 프로그래밍 언어에 익숙하지 않으면 오히려 진입 장벽이 될 수 있음

    Pulumi Cloud 콘솔에서 스택 상태와 리소스 배포 결과를 확인하는 화면

    ⚠️ 실전에서 주의해야 할 것들

    Terraform 상태 파일(State File) 관리

    Terraform 쓰면서 가장 많이 삽질하는 부분이 상태 파일 관리예요. 처음에 로컬에 `terraform.tfstate` 파일 두고 팀에서 같이 쓰다가 충돌 났던 경험, 한 번쯤은 다들 있으실 거예요 ㅎㅎ

    # 반드시 원격 백엔드 설정하세요!
    terraform {
      backend "s3" {
        bucket         = "my-terraform-state"
        key            = "production/terraform.tfstate"
        region         = "ap-northeast-2"
        encrypt        = true
        dynamodb_table = "terraform-state-lock"  # 동시 수정 방지용 잠금
      }
    }
    

    DynamoDB로 상태 잠금(State Locking) 설정 안 해두면, 두 사람이 동시에 apply 실행할 때 상태 파일이 꼬일 수 있어요. 이거 한 번 겪어보면 절대 안 잊어버리게 됩니다 😅

    Pulumi 스택(Stack) 분리 전략

    Pulumi에서는 환경별로 스택을 분리하는 게 기본 패턴인데, 초반에 이걸 제대로 설계 안 하면 나중에 꽤 고생해요.

    # Pulumi 스택 생성 및 관리
    pulumi stack init production
    pulumi stack init staging
    pulumi stack init dev
    
    # 현재 스택 확인
    pulumi stack ls
    
    # 스택 전환
    pulumi stack select production
    
    # 배포 미리보기 (Terraform의 plan에 해당)
    pulumi preview
    
    # 실제 배포
    pulumi up

    💡 팁: Pulumi에서 환경별 설정값은 `pulumi config set`으로 관리하세요. 민감한 값은 `–secret` 플래그로 암호화해서 저장할 수 있어요.

    # 일반 설정값
    pulumi config set aws:region ap-northeast-2
    pulumi config set environment production
    
    # 민감한 정보는 암호화 저장
    pulumi config set --secret db_password "super-secret-password"

    Terraform 라이선스 변경 이후 고민

    앞서 언급했지만, 2023년 Terraform의 BSL 라이선스 전환은 꽤 큰 이슈였어요. 이 때문에 OpenTofu라는 오픈소스 포크가 Linux Foundation 산하에서 시작됐고, 많은 조직에서 마이그레이션을 고려하기 시작했죠. Pulumi로 넘어가는 팀들도 일부 있었고요.

    만약 상업적 사용이나 Terraform 기반 서비스 제공을 고려하고 있다면, BSL 1.1 조건을 법무팀과 함께 꼼꼼히 검토하시길 권장합니다.

    결국 뭘 선택해야 할까요? — 상황별 가이드

    제가 컨설팅이나 팀 내 논의에서 자주 받는 질문이 “그래서 뭐 써야 해요?”인데요. 정답은 없지만, 제 기준을 공유해 드릴게요.

    ✅ Terraform을 선택하면 좋은 경우

    • 팀에 인프라 전문가 비율이 높고, HCL에 거부감이 없을 때
    • 커뮤니티 레퍼런스와 안정성이 최우선일 때
    • Terraform Registry의 공개 모듈을 적극 활용하고 싶을 때
    • 기존 Terraform 코드베이스가 있는 팀에 합류할 때
    • 비교적 단순한 인프라 구성이 주를 이룰 때

    ✅ Pulumi를 선택하면 좋은 경우

    • 팀이 소프트웨어 개발자 중심이고, 특정 언어에 능숙할 때
    • 인프라 로직이 복잡해서 프로그래밍적 표현이 필요할 때
    • 인프라 코드에 유닛 테스트를 적용하고 싶을 때
    • 앱 코드와 인프라 코드를 같은 언어로 통합 관리하고 싶을 때
    • 라이선스 이슈에서 자유로운 완전 오픈소스 도구가 필요할 때

    팀 상황과 프로젝트 특성에 따른 Terraform vs Pulumi 선택 가이드 인포그래픽

    자주 묻는 질문 (FAQ)

    Q. Terraform에서 Pulumi로 마이그레이션할 수 있나요?

    네, Pulumi에서 공식적으로 변환 도구를 제공해요. `pulumi convert –from terraform` 명령으로 기본적인 리소스 변환이 가능합니다. 완벽하지는 않지만, 간단한 구성은 꽤 잘 변환되더라고요. 다만 복잡한 모듈이나 커스텀 프로바이더는 수동 작업이 필요할 수 있어요.

    Q. 둘 다 멀티 클라우드(Multi-Cloud)를 지원하나요?

    둘 다 AWS, Azure, GCP 등 주요 클라우드 프로바이더를 지원합니다. Terraform은 프로바이더 생태계가 더 방대하고, Pulumi는 Terraform 프로바이더를 브리지(Bridge)해서 쓸 수 있어서 커버리지 차이가 많이 줄어들었어요.

    Q. Pulumi는 무료인가요?

    Pulumi 자체는 오픈소스(Apache 2.0)로 무료입니다. Pulumi Cloud의 경우 개인 사용은 무료 플랜이 있고, 팀 협업 기능이나 고급 기능은 유료 플랜이 필요해요. 자체 백엔드(S3, Azure Blob Storage 등)를 사용하면 Cloud 없이도 운영 가능합니다.

    Q. 둘 다 Kubernetes(쿠버네티스) 관리에 적합한가요?

    네, 둘 다 Kubernetes 리소스 관리를 지원합니다. 다만 Kubernetes 전용으로는 Helm이나 Kustomize와의 조합이 더 일반적이에요. 클라우드 인프라(EKS 클러스터 생성 등)와 Kubernetes 리소스를 함께 관리할 때 IaC 도구가 유용하게 쓰입니다.

    마무리 — 결국 중요한 건 팀과 컨텍스트

    Pulumi vs Terraform 비교를 오래 해봤지만, 솔직히 말씀드리면 둘 다 훌륭한 IaC 도구입니다. 어느 쪽이 “객관적으로 더 좋다”라고 말하기가 어려워요.

    제 개인적인 포지션을 말씀드리면:

    • 🏢 엔터프라이즈 환경, 인프라 팀 주도 → Terraform (안정성, 레퍼런스, 생태계)
    • 🚀 스타트업, 개발자 팀, 복잡한 로직 → Pulumi (유연성, 언어 친숙도)
    • 🏠 홈랩, 개인 프로젝트 → 둘 다 배워보세요! 경험치 쌓기엔 최고

    저는 요즘 홈랩은 Pulumi(Python)로, 업무 환경은 Terraform으로 관리하고 있어요. 두 도구를 병행하면서 각각의 강점을 더 명확하게 느끼게 됐달까요.

    혹시 이미 둘 중 하나를 쓰고 계신 분들은, 반대편 도구도 간단한 사이드 프로젝트로 한번 써보시길 추천드려요. 비교해봐야 진짜 차이가 느껴지거든요.

    다음 글에서는 Terraform 상태 관리와 원격 백엔드 설정을 더 깊이 다뤄볼 예정이에요. Terraform 쓰다가 상태 파일로 삽질한 경험이 있으신 분들이라면 도움이 될 겁니다. 이전 글에서 다뤘던 클라우드 인프라 자동화 기초도 함께 참고해보세요!

    질문이나 의견은 댓글로 남겨주세요. 저도 아직 배우는 중이라, 여러분의 경험도 궁금합니다 😊

  • [Cloud] Ansible 클라우드 보안 자동화: 취약점 관리 및 규정 준수 가이드

    클라우드 보안, 손으로 하나하나 설정하다가 사고 날 뻔했습니다

    솔직히 말씀드리면, 저도 처음엔 보안 설정을 수작업으로 했어요. EC2 인스턴스 하나하나 들어가서 패키지 업데이트 확인하고, 보안 그룹 규칙 검토하고… 인스턴스가 10개쯤 됐을 때까진 그나마 버텼는데, 50개 넘어가니까 진짜 손이 두 개로는 부족하더라고요. 어느 날 감사(Audit) 리포트 보다가 패치 안 된 서버가 12대나 있다는 걸 발견했을 때 등에 식은땀이 흘렀습니다.

    그때부터 Ansible 보안 자동화를 본격적으로 파기 시작했어요. 취약점 관리(Vulnerability Management)부터 규정 준수(Compliance) 검증까지, Ansible로 어떻게 자동화할 수 있는지 실무 경험을 바탕으로 풀어드릴게요. 클라우드 환경에서 보안을 체계적으로 관리하고 싶으신 분들께 실질적인 도움이 됐으면 합니다.

    ▲ Ansible을 중심으로 한 클라우드 보안 자동화 전체 흐름 — 인벤토리 수집부터 취약점 패치, 규정 준수 보고까지 한눈에


    Ansible 보안 자동화, 이게 왜 필요한가요?

    쉽게 말해서, 클라우드 환경은 서버가 수시로 생겼다 없어지거든요. 온프레미스(자체 서버실)처럼 고정된 인프라가 아니에요. 오늘 오토스케일링으로 인스턴스 10개 생겼다가 내일 5개로 줄어들 수 있잖아요. 이런 환경에서 수작업 보안 관리는 현실적으로 불가능합니다.

    Ansible 보안 자동화가 필요한 이유를 정리해보면:

    • 일관성(Consistency): 모든 서버에 동일한 보안 정책 적용 — 사람이 하면 실수가 생기지만 플레이북(Playbook)은 실수 안 함
    • 속도(Speed): 수백 대 서버에 패치 적용을 몇 분 안에 완료
    • 감사 추적(Audit Trail): 언제, 어떤 변경이 있었는지 코드로 기록됨
    • 반복 가능성(Repeatability): 같은 작업을 언제든 동일하게 재현 가능
    • 드리프트 감지(Configuration Drift Detection): 설정이 원하는 상태에서 벗어나면 자동으로 교정

    근데 여기서 중요한 포인트! Ansible은 에이전트리스(Agentless) 방식이에요. 관리 대상 서버에 별도 소프트웨어를 설치할 필요가 없고, SSH(또는 WinRM)만 열려 있으면 됩니다. 클라우드 환경에서 이게 엄청난 장점이거든요.


    Ansible Vault로 민감 정보 보호하기

    보안 자동화를 하면서 제일 먼저 부딪히는 문제가 뭔지 아세요? 바로 시크릿(Secret) 관리입니다. API 키, 데이터베이스 패스워드, 클라우드 자격증명… 이걸 플레이북 파일에 평문으로 넣으면 안 되잖아요.

    Ansible에는 Ansible Vault라는 내장 암호화 도구가 있어요. 파일이나 변수를 AES-256으로 암호화해서 Git에 안전하게 올릴 수 있게 해줍니다. 제가 실제로 쓰는 방식을 보여드릴게요.

    Vault 파일 생성 및 사용

    # vault 암호화 파일 생성
    ansible-vault create secrets/cloud_credentials.yml
    
    # 기존 파일 암호화
    ansible-vault encrypt vars/sensitive_vars.yml
    
    # 암호화된 파일 내용 확인
    ansible-vault view secrets/cloud_credentials.yml
    
    # 암호화된 파일 수정
    ansible-vault edit secrets/cloud_credentials.yml
    
    # 플레이북 실행 시 vault 패스워드 파일 사용
    ansible-playbook security_hardening.yml --vault-password-file ~/.vault_pass.txt
    # secrets/cloud_credentials.yml (암호화 전 내용 예시)
    ---
    aws_access_key: "AKIAIOSFODNN7EXAMPLE"
    aws_secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
    db_password: "super_secret_password_here"
    api_token: "your_api_token_here"

    💡 팁: 운영 환경에서는 vault 패스워드 파일을 직접 관리하는 것보다 HashiCorp Vault나 AWS Secrets Manager 같은 전용 시크릿 관리 서비스와 연동하는 게 훨씬 안전합니다.


    취약점 관리 자동화 실전

    이제 본격적으로 들어가볼게요. Ansible 취약점 관리의 핵심은 세 가지입니다: 취약 패키지 탐지 → 패치 적용 → 결과 보고. 제가 실제 환경에서 쓰는 플레이북 구조를 공유할게요.

    1단계: 인벤토리 동적 구성 (Dynamic Inventory)

    클라우드 환경에서는 정적 인벤토리(Static Inventory)가 아니라 동적 인벤토리(Dynamic Inventory)를 써야 해요. AWS 기준으로 보면:

    # inventory/aws_ec2.yml
    ---
    plugin: amazon.aws.aws_ec2
    regions:
      - ap-northeast-2  # 서울 리전
    filters:
      instance-state-name: running
      tag:Environment:
        - production
        - staging
    keyed_groups:
      - key: tags.Role
        prefix: role
      - key: tags.Environment
        prefix: env
    hostname_source: private-ip-address
    compose:
      ansible_host: private_ip_address
    # 동적 인벤토리 확인
    ansible-inventory -i inventory/aws_ec2.yml --list
    ansible-inventory -i inventory/aws_ec2.yml --graph

    2단계: 취약점 스캔 및 패치 플레이북

    # playbooks/vulnerability_management.yml
    ---
    - name: 취약점 스캔 및 보안 패치 자동화
      hosts: all
      become: yes
      gather_facts: yes
      vars_files:
        - ../secrets/cloud_credentials.yml
    
      vars:
        patch_reboot_required: false
        security_only: true
        report_path: "/var/log/ansible/security_report"
    
      tasks:
        - name: 리포트 디렉토리 생성
          file:
            path: "{{ report_path }}"
            state: directory
            mode: '0750'
            owner: root
            group: root
    
        - name: 패키지 캐시 업데이트 (Debian/Ubuntu)
          apt:
            update_cache: yes
            cache_valid_time: 3600
          when: ansible_os_family == "Debian"
    
        - name: 패키지 캐시 업데이트 (RHEL/CentOS/Amazon Linux)
          yum:
            update_cache: yes
          when: ansible_os_family == "RedHat"
    
        - name: 보안 업데이트 목록 확인 (Debian/Ubuntu)
          shell: apt list --upgradeable 2>/dev/null | grep -i security
          register: security_updates_deb
          changed_when: false
          failed_when: false
          when: ansible_os_family == "Debian"
    
        - name: 보안 업데이트 목록 확인 (RHEL 계열)
          shell: yum check-update --security 2>/dev/null | tail -n +3
          register: security_updates_rhel
          changed_when: false
          failed_when: security_updates_rhel.rc not in [0, 100]
          when: ansible_os_family == "RedHat"
    
        - name: 보안 패치 적용 (Debian/Ubuntu)
          apt:
            upgrade: dist
            update_cache: yes
            only_upgrade: yes
          when:
            - ansible_os_family == "Debian"
            - security_only | bool
          register: apt_upgrade_result
    
        - name: 보안 패치 적용 (RHEL 계열)
          yum:
            name: "*"
            state: latest
            security: yes
          when:
            - ansible_os_family == "RedHat"
            - security_only | bool
          register: yum_upgrade_result
    
        - name: 재부팅 필요 여부 확인 (Ubuntu)
          stat:
            path: /var/run/reboot-required
          register: reboot_required_file
          when: ansible_os_family == "Debian"
    
        - name: 결과 리포트 생성
          template:
            src: ../templates/security_report.j2
            dest: "{{ report_path }}/report_{{ ansible_date_time.date }}.txt"
            mode: '0640'
          vars:
            hostname: "{{ ansible_hostname }}"
            os_family: "{{ ansible_os_family }}"
            kernel_version: "{{ ansible_kernel }}"
            reboot_needed: "{{ reboot_required_file.stat.exists | default(false) }}"
    
        - name: 재부팅 필요 시 알림
          debug:
            msg: "⚠️ {{ ansible_hostname }} 서버는 패치 적용 후 재부팅이 필요합니다!"
          when:
            - ansible_os_family == "Debian"
            - reboot_required_file.stat.exists | default(false)

    ▲ Ansible 플레이북 실행 결과 — 각 호스트별 패치 적용 현황과 재부팅 필요 여부가 한눈에 표시됨

    3단계: 리포트 템플릿 (Jinja2)

    # templates/security_report.j2
    === 보안 패치 리포트 ===
    호스트명: {{ hostname }}
    OS 계열: {{ os_family }}
    커널 버전: {{ kernel_version }}
    실행 일시: {{ ansible_date_time.iso8601 }}
    재부팅 필요: {{ reboot_needed }}
    
    {% if ansible_os_family == 'Debian' %}
    적용된 업데이트:
    {{ apt_upgrade_result.stdout | default('변경 없음') }}
    {% elif ansible_os_family == 'RedHat' %}
    적용된 업데이트:
    {{ yum_upgrade_result.results | default('변경 없음') }}
    {% endif %}
    ========================

    규정 준수 자동화 — CIS 벤치마크 적용

    취약점 패치만큼 중요한 게 바로 Ansible 규정 준수 자동화예요. CIS(Center for Internet Security) 벤치마크나 NIST 프레임워크 같은 보안 기준을 서버에 자동으로 적용하고 검증하는 거죠.

    저는 주로 CIS 리눅스 벤치마크를 기반으로 하드닝(보안 강화) 플레이북을 만들어서 씁니다. 핵심 항목들을 추려서 보여드릴게요.

    # playbooks/cis_compliance.yml
    ---
    - name: CIS 벤치마크 기반 보안 하드닝
      hosts: "{{ target_hosts | default('all') }}"
      become: yes
      gather_facts: yes
    
      vars:
        cis_level: 1  # 1 또는 2
        disable_unused_services: true
        configure_auditd: true
    
      tasks:
        # === 파일시스템 설정 ===
        - name: "CIS 1.1.1 - /tmp 파티션 nodev 마운트 옵션 설정"
          mount:
            name: /tmp
            src: tmpfs
            fstype: tmpfs
            opts: "defaults,rw,nosuid,nodev,noexec,relatime"
            state: mounted
          tags: [filesystem, cis_1_1]
    
        # === SSH 보안 설정 ===
        - name: "CIS 5.2 - SSH 보안 설정 강화"
          lineinfile:
            path: /etc/ssh/sshd_config
            regexp: "{{ item.regexp }}"
            line: "{{ item.line }}"
            state: present
            backup: yes
          loop:
            - { regexp: '^#?Protocol', line: 'Protocol 2' }
            - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
            - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
            - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
            - { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 4' }
            - { regexp: '^#?PermitEmptyPasswords', line: 'PermitEmptyPasswords no' }
            - { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' }
            - { regexp: '^#?ClientAliveCountMax', line: 'ClientAliveCountMax 0' }
            - { regexp: '^#?LoginGraceTime', line: 'LoginGraceTime 60' }
          notify: restart sshd
          tags: [ssh, cis_5_2]
    
        # === 감사 로그(Auditd) 설정 ===
        - name: "CIS 4.1 - auditd 설치 및 활성화"
          package:
            name: auditd
            state: present
          when: configure_auditd | bool
          tags: [auditd, cis_4_1]
    
        - name: "CIS 4.1 - auditd 규칙 설정"
          copy:
            content: |
              # 시스템 콜 감사 규칙
              -a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time-change
              -a always,exit -F arch=b32 -S adjtimex -S settimeofday -S stime -k time-change
              -w /etc/localtime -p wa -k time-change
              -w /etc/group -p wa -k identity
              -w /etc/passwd -p wa -k identity
              -w /etc/gshadow -p wa -k identity
              -w /etc/shadow -p wa -k identity
              -w /etc/sudoers -p wa -k scope
              -w /var/log/sudo.log -p wa -k actions
              -w /sbin/insmod -p x -k modules
              -w /sbin/rmmod -p x -k modules
              -w /sbin/modprobe -p x -k modules
              -e 2
            dest: /etc/audit/rules.d/cis_hardening.rules
            mode: '0640'
            owner: root
            group: root
          notify: reload auditd
          when: configure_auditd | bool
          tags: [auditd, cis_4_1]
    
        # === 패스워드 정책 ===
        - name: "CIS 5.4.1 - 패스워드 만료 정책 설정"
          lineinfile:
            path: /etc/login.defs
            regexp: "{{ item.regexp }}"
            line: "{{ item.line }}"
          loop:
            - { regexp: '^PASS_MAX_DAYS', line: 'PASS_MAX_DAYS   90' }
            - { regexp: '^PASS_MIN_DAYS', line: 'PASS_MIN_DAYS   7' }
            - { regexp: '^PASS_WARN_AGE', line: 'PASS_WARN_AGE   14' }
          tags: [password_policy, cis_5_4]
    
        # === 불필요한 서비스 비활성화 ===
        - name: "CIS 2.2 - 불필요한 서비스 비활성화"
          service:
            name: "{{ item }}"
            state: stopped
            enabled: no
          loop:
            - telnet
            - rsh
            - rlogin
            - rexec
            - tftp
            - xinetd
          failed_when: false
          when: disable_unused_services | bool
          tags: [services, cis_2_2]
    
      handlers:
        - name: restart sshd
          service:
            name: sshd
            state: restarted
    
        - name: reload auditd
          service:
            name: auditd
            state: restarted

    ⚠️ 삽질 경험: 이런 실수 하지 마세요

    저도 처음엔 꽤 고생했거든요. 실제로 겪은 문제들을 공유할게요.

    실수 1: 프로덕션에 –check 없이 바로 실행

    이거 진짜 아찔했습니다. 테스트 환경에서 잘 돌아가던 플레이북을 프로덕션에 그냥 실행했다가 SSH 설정 변경으로 일부 서버 접근이 막힌 적이 있어요. 지금은 반드시 이 순서를 지킵니다:

    # 1단계: 드라이런 — 실제 변경 없이 시뮬레이션
    ansible-playbook cis_compliance.yml -i inventory/aws_ec2.yml --check --diff
    
    # 2단계: 특정 태그만 먼저 테스트
    ansible-playbook cis_compliance.yml -i inventory/aws_ec2.yml --tags ssh --check
    
    # 3단계: 한 대만 먼저 적용
    ansible-playbook cis_compliance.yml -i inventory/aws_ec2.yml --limit "specific_host_ip" 
    
    # 4단계: 전체 적용
    ansible-playbook cis_compliance.yml -i inventory/aws_ec2.yml

    실수 2: 멱등성(Idempotency) 무시

    Ansible의 핵심 철학이 멱등성(같은 작업을 여러 번 실행해도 결과가 동일)인데, 초반에 shell 모듈을 너무 남발했어요. shell로 스크립트 실행하면 매번 “changed” 상태가 되거든요. 가능하면 Ansible 내장 모듈(file, lineinfile, service 등)을 쓰는 게 맞습니다.

    실수 3: Vault 패스워드 관리 소홀

    팀원 여러 명이 같은 vault 패스워드를 공유하다가, 퇴사자 발생 시 전체 재암호화해야 하는 상황이 생겼어요. 지금은 CI/CD 파이프라인에서 환경변수로 관리하고, 팀원별 접근은 AWS IAM으로 통제합니다.

    문제 상황 잘못된 방법 올바른 방법
    민감 정보 관리 평문으로 vars 파일에 저장 Ansible Vault + Secrets Manager 연동
    플레이북 테스트 프로덕션에 직접 실행 –check –diff로 드라이런 먼저
    명령 실행 shell/command 모듈 남용 전용 모듈 우선 사용 (멱등성 보장)
    대규모 적용 전체 호스트에 한 번에 실행 –limit으로 단계적 적용
    에러 처리 failed_when 미설정 적절한 failed_when/ignore_errors 설정

    규정 준수 검증 및 리포팅 자동화

    보안 설정을 적용했으면, 실제로 잘 됐는지 검증하고 리포트를 뽑아야죠. 저는 Ansible과 함께 OpenSCAP(보안 설정 자동화 프로토콜 기반 도구)을 연동해서 씁니다.

    # playbooks/compliance_report.yml
    ---
    - name: 규정 준수 검증 및 HTML 리포트 생성
      hosts: all
      become: yes
      gather_facts: yes
    
      vars:
        report_dir: "/var/log/compliance_reports"
        scap_profile: "xccdf_org.ssgproject.content_profile_cis"
    
      tasks:
        - name: OpenSCAP 및 SCAP 보안 가이드 설치 (RHEL 계열)
          yum:
            name:
              - openscap-scanner
              - scap-security-guide
            state: present
          when: ansible_os_family == "RedHat"
    
        - name: 리포트 디렉토리 생성
          file:
            path: "{{ report_dir }}"
            state: directory
            mode: '0750'
    
        - name: SCAP 스캔 실행 및 HTML 리포트 생성
          command: >
            oscap xccdf eval
            --profile {{ scap_profile }}
            --results {{ report_dir }}/results_{{ ansible_hostname }}_{{ ansible_date_time.date }}.xml
            --report {{ report_dir }}/report_{{ ansible_hostname }}_{{ ansible_date_time.date }}.html
            /usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml
          register: scap_result
          failed_when: scap_result.rc not in [0, 2]
          when: ansible_os_family == "RedHat"
    
        - name: 스캔 결과 요약 출력
          debug:
            msg: "{{ ansible_hostname }} 스캔 완료 — 리포트: {{ report_dir }}/report_{{ ansible_hostname }}_{{ ansible_date_time.date }}.html"
    
        - name: 리포트 파일 로컬로 가져오기
          fetch:
            src: "{{ report_dir }}/report_{{ ansible_hostname }}_{{ ansible_date_time.date }}.html"
            dest: "./compliance_reports/"
            flat: no

    ▲ 규정 준수 검증 결과 대시보드 — 각 CIS 벤치마크 항목별 통과/실패/해당없음 현황을 서버별로 한눈에 파악 가능

    이렇게 하면 매주 자동으로 각 서버의 규정 준수 현황을 HTML 리포트로 뽑을 수 있어요. 경영진이나 감사팀에 제출할 때도 편하고, 뭐가 문제인지 한눈에 보이거든요.


    CI/CD 파이프라인과 보안 자동화 연동

    사실 이게 제일 강력한 부분이에요. 깃허브 액션(GitHub Actions)이나 젠킨스(Jenkins)와 연동하면, 코드 변경이 있을 때마다 자동으로 보안 검증이 돌아가거든요.

    # .github/workflows/security_automation.yml
    name: 보안 자동화 파이프라인
    
    on:
      schedule:
        - cron: '0 2 * * *'  # 매일 새벽 2시 실행
      push:
        branches: [main]
        paths:
          - 'playbooks/**'
          - 'inventory/**'
    
    jobs:
      security-scan:
        runs-on: ubuntu-latest
        steps:
          - name: 코드 체크아웃
            uses: actions/checkout@v3
    
          - name: Python 및 Ansible 설치
            run: |
              pip install ansible ansible-lint
              ansible-galaxy collection install amazon.aws community.general
    
          - name: Ansible Lint 실행 (플레이북 문법/베스트프랙티스 검사)
            run: ansible-lint playbooks/
    
          - name: Vault 패스워드 설정
            run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > ~/.vault_pass.txt
    
          - name: 드라이런 실행 (실제 변경 없이 검증)
            run: |
              ansible-playbook playbooks/vulnerability_management.yml \
                -i inventory/aws_ec2.yml \
                --vault-password-file ~/.vault_pass.txt \
                --check
            env:
              AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
              AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    
          - name: 승인 후 실제 적용 (main 브랜치만)
            if: github.ref == 'refs/heads/main'
            run: |
              ansible-playbook playbooks/vulnerability_management.yml \
                -i inventory/aws_ec2.yml \
                --vault-password-file ~/.vault_pass.txt
            env:
              AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
              AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

    이렇게 구성하면 드리프트(원하는 상태에서 벗어남)가 발생해도 다음 날 새벽에 자동으로 교정이 되거든요. 정말 마음이 편해집니다.


    정리: Ansible 클라우드 보안 자동화 핵심 체크리스트

    ▲ Ansible 클라우드 보안 자동화 핵심 구성 요소 요약 — Vault, 취약점 관리, 규정 준수, CI/CD 연동의 4가지 축

    실무에서 느낀 건, 보안은 한 번 설정하고 끝나는 게 아니라는 거예요. 지속적으로 검증하고, 자동화해야 실수가 없습니다. Ansible 보안 자동화로 구성한 체계를 정리해보면:

    1. ✅ Ansible Vault로 모든 민감 정보 암호화 — 평문 시크릿은 절대 금지
    2. ✅ 동적 인벤토리(Dynamic Inventory)로 클라우드 환경 자동 탐지
    3. ✅ 취약점 관리 플레이북으로 정기적 보안 패치 자동화
    4. ✅ CIS 벤치마크 기반 하드닝으로 일관된 보안 기준 적용
    5. ✅ OpenSCAP 연동으로 규정 준수 검증 및 리포트 자동 생성
    6. ✅ CI/CD 파이프라인 연동으로 드리프트 자동 교정
    7. ✅ 드라이런(–check –diff)을 습관화해서 실수 방지

    혹시 이 글을 보고 궁금한 점이 생기셨나요? 다음 글에서는 HashiCorp Vault와 Ansible 연동으로 더 고도화된 시크릿 관리 방법을 다뤄볼 예정이에요. Ansible 기초 인프라 자동화도 함께 보시면 이해가 더 쉬울 거예요.

    뭔가 막히는 부분 있으시면 댓글로 편하게 물어보세요. 저도 삽질하면서 배운 거라, 같이 고민해드릴 수 있습니다.


    자주 묻는 질문 (FAQ)

    Q. Ansible로 Windows 서버 보안도 자동화할 수 있나요?

    네, 가능합니다. Windows는 SSH 대신 WinRM(Windows Remote Management)을 통해 연결하고, win_updates, win_service 같은 Windows 전용 모듈을 사용합니다. 다만 설정이 리눅스보다 조금 더 복잡한 편이에요.

    Q. Ansible Tower(AWX)를 써야 하나요?

    소규모 환경(서버 50대 이하)이면 CLI만으로도 충분합니다. 팀 규모가 커지고, 역할 기반 접근 제어(RBAC)나 스케줄링, 웹 UI가 필요해지면 오픈소스인 AWX나 상용 버전인 Ansible Automation Platform을 고려해보세요.

    Q. 플레이북 실행 중 에러가 나면 어떻게 되나요?

    기본적으로 에러가 발생한 호스트에서 플레이북 실행이 중단됩니다. ignore_errors: yes나 failed_when 조건을 적절히 설정해서 에러 처리를 세밀하게 제어할 수 있어요. 중요한 보안 작업은 에러 발생 시 즉시 알림을 받도록 Slack이나 이메일 핸들러를 연동하는 걸 권장합니다.

  • [Cloud] AWS 비용 최적화: EC2, S3, RDS 절감 전략 및 Cost Explorer 활용 가이드

    [Cloud] AWS 비용 최적화: EC2, S3, RDS 절감 전략 및 Cost Explorer 활용 가이드

    클라우드 비용, 왜 통제해야 할까요? 13년차 서버실의 AWS 비용 최적화 이야기

    안녕하세요! 13년차 인프라 엔지니어, “13년차의 서버실” 운영자입니다. 클라우드를 오래 사용하다 보면 다들 한 번쯤 겪는 일이 있죠. “이번 달 AWS 요금이 왜 이렇게 많이 나왔지?” 하고 깜빡 놀라는 순간 말이에요. 저도 처음엔 이게 뭔가 싶었는데, 실제 운영 환경뿐만 아니라 제 홈랩에서 다양한 서비스를 돌리면서도 종종 이런 경험을 하더라고요. 클라우드가 편리한 건 맞지만, 비용 관리에 소홀하면 예상치 못한 지출이 발생하기 십상입니다.

    오늘은 제가 13년간 쌓아온 경험을 바탕으로 AWS 비용 최적화를 위한 실질적인 전략들을 공유해볼까 합니다. 특히 많은 분들이 사용하는 EC2, S3, RDS 서비스의 비용 절감 방안과 함께, 우리 돈이 어디로 새고 있는지 파악할 수 있는 AWS Cost Explorer 활용 가이드까지 자세히 알려드릴게요. 클라우드 비용 때문에 밤잠 설치셨던 분들이라면, 오늘 글이 정말 유용할 거예요! ✅

    AWS 클라우드 비용 증가 추이와 효율적인 최적화 전략 필요성을 보여주는 다이어그램

    AWS 클라우드 비용 증가 추이와 효율적인 최적화 전략 필요성을 보여주는 다이어그램

    AWS 비용 최적화의 핵심 개념: “쓰는 만큼 낸다”의 함정

    AWS를 포함한 클라우드의 가장 큰 장점 중 하나는 바로 Pay-as-you-go(사용한 만큼 지불) 모델이에요. 필요한 만큼만 쓰고, 쓴 만큼만 비용을 내니 얼마나 합리적인가요? 하지만 이 “합리적”이라는 말 뒤에는 무서운 함정이 숨어있습니다. 바로 “쓰고 있는지도 모르게 계속 돈이 나간다”는 거죠. 저도 처음엔 이게 참 헷갈리더라고요.

    클라우드 비용 최적화(Cost Optimization)는 단순히 요금을 줄이는 것을 넘어, 우리가 지불하는 비용 대비 최대 가치(Value)를 얻는 것을 목표로 합니다. 이걸 요즘은 FinOps(Financial Operations)라고 부르기도 하죠. 핵심은 크게 세 가지입니다:

    • Right-sizing (적정 크기 조정): 워크로드에 맞는 최적의 리소스 크기를 찾는 거예요. 너무 과하게 프로비저닝하면 돈 낭비겠죠?
    • Discount Programs (할인 프로그램 활용): 예약 인스턴스(Reserved Instances, RI)나 세이빙 플랜(Savings Plans)처럼 장기 약정을 통해 할인을 받는 방법입니다.
    • Automation (자동화): 사용하지 않는 리소스는 자동으로 중지하거나 종료해서 불필요한 비용을 줄이는 거죠.

    이 개념들을 잘 이해하고 적용하는 게 중요합니다. 저도 처음엔 무조건 싸게만 하려다가 성능 이슈로 삽질 좀 했습니다. ㅎㅎ

    EC2 비용 절감 전략: 인스턴스 유형 선택부터 예약 인스턴스까지

    AWS EC2(Elastic Compute Cloud)는 클라우드 컴퓨팅의 핵심 서비스인 만큼, 여기서 나가는 비용이 전체 청구서의 상당 부분을 차지할 때가 많습니다. EC2 비용을 줄이는 몇 가지 실질적인 방법을 소개할게요.

    1. Right-sizing (적정 크기 조정)

    가장 기본적이면서도 중요한 전략입니다. 제가 홈랩에서 이것저것 실험하다 보면, 처음에 테스트용으로 큰 인스턴스를 띄워놓고 나중에 줄이는 걸 깜빡하는 경우가 많아요. 😅

    1. 모니터링 데이터 분석: CloudWatch 같은 도구를 활용해서 EC2 인스턴스의 CPU 사용률, 메모리 사용량, 네트워크 I/O 등을 꾸준히 확인하면 돼요.
    2. 불필요한 리소스 식별: 평균 CPU 사용률이 10~20% 미만으로 계속 유지된다면, 더 작은 인스턴스 타입으로 변경할 여지가 크다는 뜻입니다.
    3. Auto Scaling (오토 스케일링) 활용: 워크로드 변동이 심한 서비스라면, 트래픽에 따라 자동으로 인스턴스 개수를 조절하는 Auto Scaling 그룹을 구성하는 게 훨씬 효율적입니다.

    2. 구매 옵션 활용

    EC2는 다양한 구매 옵션을 제공하는데, 이를 잘 활용하면 비용을 크게 아낄 수 있어요.

    • On-Demand Instances (온디맨드 인스턴스): 가장 유연하지만 가장 비싼 옵션이에요. 단기적인 사용이나 예측 불가능한 워크로드에 적합합니다.
    • Reserved Instances (예약 인스턴스, RI): 1년 또는 3년 약정을 통해 온디맨드 가격 대비 최대 75%까지 할인받을 수 있죠. 꾸준히 사용량이 있는 베이스라인 워크로드에는 정말 좋아요. 저도 프로덕션 환경에서는 거의 RI를 사용합니다.
    • Savings Plans (세이빙 플랜): RI보다 더 유연한 할인 모델입니다. 컴퓨팅 사용량(시간당 $X)을 약정하면 EC2, Fargate, Lambda 등 다양한 컴퓨팅 서비스에 할인이 적용되거든요.
    • Spot Instances (스팟 인스턴스): AWS의 여유 컴퓨팅 자원을 경매 방식으로 저렴하게 구매하는 옵션이에요. 온디맨드 대비 최대 90%까지 저렴하지만, AWS가 자원이 필요하면 언제든지 회수해갈 수 있습니다. 배치 작업, 개발/테스트 환경처럼 중단되어도 괜찮은 워크로드에 정말 좋아요.

    3. 사용하지 않는 리소스 중지/종료

    이건 정말 중요합니다! 개발/테스트용 인스턴스를 퇴근할 때 중지(Stop)하거나, 아예 필요 없으면 종료(Terminate)하는 습관을 들여야 해요. 중지된 인스턴스는 컴퓨팅 비용은 나가지 않지만, EBS(Elastic Block Store) 볼륨 비용은 계속 청구되니 주의해야 합니다. 제가 예전에 개발팀에서 테스트용으로 띄워놓은 인스턴스들을 정리 안 해서 비용이 꽤 많이 나온 적이 있었죠. 😅

    S3 비용 절감 전략: 스토리지 클래스와 수명 주기 정책 활용

    AWS S3(Simple Storage Service)는 무제한에 가까운 스토리지 서비스를 제공하지만, 데이터를 어떻게 저장하고 관리하느냐에 따라 비용이 천차만별이에요. 저도 처음엔 무조건 S3 Standard에 다 때려 박았다가 나중에 후회했었죠. ㅎㅎ

    1. S3 Storage Classes (스토리지 클래스) 활용

    S3는 데이터 접근 빈도와 복원 요구 사항에 따라 다양한 스토리지 클래스를 제공합니다. 데이터의 특성에 맞춰 올바른 클래스를 선택하는 게 핵심이에요.

    여기 주요 S3 스토리지 클래스를 비교한 표가 있습니다. 💡

    AWS S3 스토리지 클래스별 비용, 성능, 사용 사례를 비교한 표

    AWS S3 스토리지 클래스별 비용, 성능, 사용 사례를 비교한 표

    스토리지 클래스 주요 특징 적합한 용도 비용 (상대적)
    S3 Standard 자주 액세스, 높은 처리량 웹사이트 콘텐츠, 모바일/게임 앱, 빅데이터 분석 높음
    S3 Standard-IA (Infrequent Access) 자주 액세스하지 않지만 필요 시 빠르게 검색 재해 복구 백업, 장기 아카이빙 중간
    S3 One Zone-IA IA와 유사하나 단일 AZ 저장, 가용성 낮음 보조 백업, 재생성 가능한 데이터 Standard-IA보다 약간 낮음
    S3 Glacier 장기 아카이빙, 몇 분~몇 시간 내 검색 규제 준수 아카이브, 의료 기록 낮음
    S3 Glacier Deep Archive 가장 저렴한 장기 아카이빙, 몇 시간~12시간 내 검색 법적 보존, 미디어 아카이브 매우 낮음

    2. Lifecycle Policies (수명 주기 정책) 설정

    이건 정말 제가 강력하게 추천하는 기능입니다! 수명 주기 정책(Lifecycle Policies)을 사용하면, 특정 기간이 지난 객체를 자동으로 더 저렴한 스토리지 클래스로 전환하거나 아예 삭제할 수 있어요. 예를 들어, 30일이 지난 로그 파일은 S3 Standard-IA로, 90일이 지나면 Glacier로 옮기고, 1년이 지나면 영구 삭제하는 식이죠. 이걸 설정하고 나면 정말 비용이 확 줄어드는 걸 경험하실 수 있을 거예요. 🎉

    예를 들어, 30일 후 STANDARD_IA로 전환하고 365일 후 만료시키는 정책은 아래와 같은 JSON으로 정의하면 돼요. AWS CLI를 사용하면 정말 간단하게 적용할 수 있습니다.

    {\n  "Rules": [\n    {\n      "ID": "TransitionToIAAndExpire",\n      "Filter": {\n        "Prefix": "logs/"\n      },\n      "Status": "Enabled",\n      "Transitions": [\n        {\n          "Days": 30,\n          "StorageClass": "STANDARD_IA"\n        }\n      ],\n      "Expiration": {\n        "Days": 365\n      }\n    }\n  ]\n}
    aws s3api put-bucket-lifecycle-configuration \\\n  --bucket your-bucket-name \\\n  --lifecycle-configuration file://lifecycle-policy.json

    RDS 비용 절감 전략: 데이터베이스 최적화와 예약 인스턴스

    AWS RDS(Relational Database Service)는 관리형 데이터베이스 서비스로, 많은 분들이 편리함 때문에 사용하죠. 하지만 데이터베이스도 EC2 못지않게 비용이 많이 나갈 수 있어요. 특히 Multi-AZ(다중 AZ) 설정은 가용성을 높여주지만 비용도 그만큼 증가하죠.

    1. Right-sizing (적정 크기 조정)

    EC2와 마찬가지로, RDS 인스턴스도 워크로드에 맞는 적절한 크기를 선택하는 것이 정말 중요해요. CloudWatch 지표(CPU, 메모리, Read/Write IOPS)를 분석해서 불필요하게 큰 인스턴스를 사용하고 있지는 않은지 확인하세요. 특히 개발/테스트 환경에서는 작은 인스턴스 타입을 사용하고, 필요할 때만 잠시 스케일 업(Scale Up)하는 전략도 효과적입니다.

    2. Reserved Instances (예약 인스턴스, RI) 활용

    RDS도 EC2처럼 예약 인스턴스를 구매할 수 있어요. 1년 또는 3년 약정을 통해 온디맨드 가격 대비 최대 70% 이상 할인받을 수 있거든요. 프로덕션 데이터베이스처럼 꾸준히 운영해야 하는 서비스에는 정말 필수적인 전략입니다. 저도 운영 중인 모든 프로덕션 RDS는 RI로 구매해서 사용하고 있어요.

    3. 개발/테스트용 RDS 인스턴스 중지/시작

    이건 정말 꿀팁입니다! ⚠️ RDS는 EC2와 달리 “중지(Stop)” 기능이 없었는데, 이제는 최대 7일까지 중지할 수 있게 됐어요. 개발/테스트용 데이터베이스라면 업무 시간 외에는 중지해두고, 필요할 때만 다시 시작(Start)해서 컴퓨팅 비용을 아낄 수 있습니다. (스토리지 비용은 계속 발생하긴 해요)

    콘솔에서 해당 RDS 인스턴스를 선택하고 ‘작업(Actions)’ 메뉴에서 ‘중지(Stop)’를 클릭하면 끝입니다. 저도 홈랩에서 테스트하는 DB들은 이 기능을 적극 활용하고 있어요. 💡

    4. 백업 및 스냅샷 보존 정책 최적화

    RDS는 자동 백업과 스냅샷을 제공하는데, 이 보존 기간이 길어질수록 스토리지 비용이 증가해요. 필요 없는 백업은 삭제하고, 보존 기간을 합리적으로 설정해서 불필요한 비용 지출을 막아야 합니다.

    AWS Cost Explorer 활용 가이드: 우리 돈은 어디로 새고 있을까?

    아무리 전략을 잘 세워도, 우리 비용이 어디서 어떻게 발생하는지 모르면 AWS 비용 최적화는 불가능합니다. 이때 필요한 것이 바로 AWS Cost Explorer(코스트 익스플로러)예요. 저도 처음엔 이 복잡한 대시보드가 뭔가 싶었는데, 몇 번 써보니 정말 편하더라고요.

    1. Cost Explorer 접속 및 기본 사용법

    1. AWS Management Console에 로그인 한 후, 검색창에 “Cost Explorer”를 입력하면 접속할 수 있어요.
    2. 기본적으로 지난 12개월 또는 현재 월의 비용 추이를 그래프로 보여줍니다.

    2. 비용 분석 필터 및 그룹화

    Cost Explorer의 강력한 기능은 바로 필터링(Filtering)과 그룹화(Grouping)입니다. 이걸 활용하면 비용을 다양한 관점에서 분석할 수 있거든요.

    • Service (서비스): EC2, S3, RDS 등 서비스별로 비용을 확인할 수 있어요.
    • Region (리전): 특정 리전에서 발생하는 비용을 분석할 수 있습니다.
    • Usage Type (사용 유형): 데이터 전송(Data Transfer), 스토리지(Storage) 등 세부 사용 유형별로 비용을 볼 수 있어요.
    • Tag (태그): 가장 유용한 기능 중 하나입니다. 여러분이 리소스에 “Project”, “Environment”, “Owner” 같은 태그를 잘 붙여놓았다면, 태그별로 비용을 그룹화해서 어떤 프로젝트가 비용을 많이 쓰는지, 어떤 팀이 비용 효율적인지 파악할 수 있어요. “태그는 사랑입니다!” 제가 늘 강조하는 부분이죠.
    • Linked Account (연결된 계정): AWS Organizations를 사용한다면, 각 계정별 비용을 분석할 수 있습니다.

    이 필터들을 조합해서 “지난달 us-east-1 리전에서 실행된 개발 환경 EC2 인스턴스의 비용” 같은 복잡한 쿼리도 쉽게 만들 수 있어요.

    AWS Cost Explorer 대시보드에서 서비스별 비용을 분석하는 화면 예시

    AWS Cost Explorer 대시보드에서 서비스별 비용을 분석하는 화면 예시

    3. 예산(Budgets) 설정 및 알림

    비용을 분석하는 것만큼 중요한 것이 예산(Budgets) 설정입니다. Cost Explorer에서 특정 서비스나 태그, 또는 전체 계정에 대한 월별 예산을 설정하고, 예산의 일정 비율(예: 80%, 100%)에 도달했을 때 이메일이나 SNS 알림을 받도록 설정할 수 있어요. 이걸 설정해두면 “이번 달 AWS 요금이 왜 이렇게 많이 나왔지?” 하고 당황할 일이 훨씬 줄어들 거예요. 🔔

    저도 홈랩에서 예상치 못한 비용이 나가는 걸 막기 위해 예산을 빡빡하게 설정해두고 사용합니다. 드디어 됐다! 하고 뿌듯할 때도 많아요. 🎉

    삽질 경험 공유 및 주의사항: 제가 겪었던 실수들

    13년차 엔지니어도 삽질은 합니다! 저도 AWS 비용 최적화를 하면서 여러 실수를 겪었는데요, 여러분은 저 같은 실수를 하지 않으시길 바라며 몇 가지 공유해봅니다. ⚠️

    • 개발/테스트 인스턴스 종료 누락: 가장 흔한 실수예요. 퇴근할 때 인스턴스 중지/종료하는 걸 깜빡해서 주말 내내 돈이 나가는 경우가 많았어요. ㅠㅠ Lambda나 Step Functions를 활용해서 특정 시간에 자동으로 중지/시작하는 스케줄러를 만들면 정말 좋습니다.
    • 데이터 전송(Egress) 비용 무시: AWS에서 외부(인터넷)로 데이터를 전송하는 비용은 생각보다 커요. S3에서 대량의 데이터를 다운로드 받거나, EC2에서 외부로 스트리밍하는 경우 Egress 비용이 폭탄이 될 수 있거든요. CDN(Content Delivery Network, 예: CloudFront)을 사용하면 비용을 줄일 수 있는 경우가 많습니다.
    • 스냅샷/백업 관리 소홀: RDS나 EBS 스냅샷은 데이터 양이 많아지면 비용도 상당해져요. 주기적으로 오래된 스냅샷을 삭제하거나, 보존 정책을 잘 설정해야 합니다.
    • RI/Savings Plans 잘못된 약정: “할인율 높으니까 무조건 길게” 생각했다가, 나중에 워크로드가 변경되거나 서비스가 종료될 때 약정 비용을 계속 내야 하는 경우가 있어요. 충분히 예측 가능한 워크로드에만 적용하고, 유연성이 더 필요하다면 Savings Plans를 고려하는 게 좋습니다.

    절감 효과 검증 및 지속적인 관리: 돈 아끼고 뿌듯함 느끼기!

    AWS 비용 최적화는 한 번 하고 끝나는 일이 아니에요. 지속적인 모니터링과 개선이 필요합니다. Cost Explorer를 통해 변경 사항 적용 전후의 비용을 비교하고, 실제로 얼마나 절감되었는지 확인하는 과정을 거쳐야 합니다.

    1. 정기적인 Cost Explorer 검토: 매주 또는 매월 Cost Explorer를 확인해서 이상 비용이 없는지, 최적화 전략이 잘 작동하는지 점검하세요.
    2. AWS Trusted Advisor (트러스티드 어드바이저) 활용: Trusted Advisor는 AWS 계정을 분석해서 비용 최적화, 성능, 보안 등에 대한 권장 사항을 제공해요. 특히 비용 최적화 탭에서 “Idle EC2 Instances”나 “Underutilized EBS Volumes” 같은 항목을 확인하면 개선할 점을 찾아낼 수 있습니다.
    3. 비용 지표 대시보드 구축: Grafana나 CloudWatch 대시보드를 활용해서 핵심 비용 지표들을 한눈에 볼 수 있도록 대시보드를 구축하는 것도 효과적이에요.

    이렇게 꾸준히 관리하면, 여러분의 클라우드 운영 비용은 분명히 줄어들 거예요. 그리고 무엇보다, 돈을 아끼는 만큼 회사(또는 나의 홈랩!)의 ROI(Return On Investment)도 높아지는 거니까요! 뿌듯함은 덤입니다. 😊

    EC2, S3, RDS 등 AWS 서비스별 주요 비용 최적화 전략을 요약한 인포그래픽

    EC2, S3, RDS 등 AWS 서비스별 주요 비용 최적화 전략을 요약한 인포그래픽

    마무리: 클라우드 비용, 이제는 ‘관리’의 영역입니다.

    오늘은 13년차 인프라 엔지니어의 시선으로 AWS 비용 최적화의 다양한 전략들을 살펴봤습니다. EC2의 적정 크기 조정부터 예약 인스턴스 활용, S3 스토리지 클래스와 수명 주기 정책, 그리고 RDS의 효율적인 운영 방법까지. 마지막으로 AWS Cost Explorer를 통한 비용 분석과 예산 설정의 중요성도 강조했죠. 사실 제가 겪었던 삽질 경험들을 솔직하게 공유하면서, 여러분은 같은 실수를 반복하지 않으셨으면 하는 바람도 있었습니다.

    클라우드는 무궁무진한 가능성을 제공하지만, 그만큼 제대로 관리하지 않으면 예상치 못한 비용으로 돌아올 수 있어요. 이제 클라우드 비용은 단순히 “사용료”가 아니라, 적극적으로 “관리”해야 하는 중요한 영역이 되었다고 생각합니다. 오늘 다룬 내용들을 바탕으로 여러분의 AWS 비용을 효율적으로 관리하고, 더 나아가 클라우드 인프라 운영의 고수로 거듭나시길 응원합니다! 💪

    혹시 궁금한 점이 있으시거나, 다른 서비스의 비용 최적화 전략에 대해 알고 싶으시다면 댓글로 알려주세요. 다음 글에서는 또 다른 흥미로운 인프라 이야기로 찾아뵙겠습니다. 감사합니다!

  • [Cloud] Terraform으로 AWS/GCP/Azure 멀티 클라우드 환경 구축 가이드

    멀티 클라우드, 왜 지금 이 시점에 Terraform인가요?

    솔직히 말씀드리면, 저도 처음엔 멀티 클라우드가 ‘대기업 얘기’인 줄 알았거든요. AWS 하나만 잘 써도 충분하지 않냐고 생각했는데… 현실은 달랐습니다. 어느 날 갑자기 고객사가 “GCP도 써야 한다”고 했을 때, 그 막막함이란 😅

    요즘은 규모에 상관없이 AWS, GCP, Azure를 동시에 다뤄야 하는 상황이 생각보다 자주 옵니다. 벤더 종속(Vendor Lock-in)을 피하려는 전략적 이유도 있고, 각 클라우드의 강점을 조합하려는 경우도 많죠. ML/AI 워크로드는 GCP, 기업 솔루션은 Azure, 메인 인프라는 AWS처럼요.

    여기서 Terraform 멀티 클라우드 구성이 빛을 발합니다. 하나의 도구로 세 개의 클라우드를 동시에 관리할 수 있다는 건, 13년 동안 인프라 일을 해온 저한테도 꽤 충격적인 경험이었어요. 이 글에서는 제가 직접 홈랩에서 구성해보고 실무에 적용한 경험을 바탕으로, Terraform으로 AWS/GCP/Azure 멀티 클라우드 환경을 구축하는 방법을 단계별로 알려드릴게요.

    ▲ Terraform 하나로 AWS, GCP, Azure를 동시에 관리하는 멀티 클라우드 아키텍처 개요도

    Terraform 멀티 클라우드가 뭔지 쉽게 풀어볼게요

    Terraform은 HashiCorp에서 만든 IaC(Infrastructure as Code) 도구예요. 쉽게 말해, 클라우드 콘솔에서 마우스로 클릭클릭 하던 걸 코드로 대체하는 거죠. 인프라 구성을 코드로 정의하고 관리하는 방식이라고 보면 됩니다.

    핵심 개념 몇 가지만 짚고 넘어갈게요.

    • Provider(프로바이더): 각 클라우드 벤더와 통신하는 플러그인. AWS, GCP, Azure 각각 별도 프로바이더가 있어요.
    • Resource(리소스): 실제로 생성할 인프라 구성 요소. EC2 인스턴스, GCS 버킷, Azure VM 같은 것들이죠.
    • State(스테이트): Terraform이 현재 인프라 상태를 추적하는 파일. 멀티 클라우드에서 이게 특히 중요합니다.
    • Module(모듈): 재사용 가능한 Terraform 코드 묶음. 멀티 클라우드 구성에서 필수예요.
    • Workspace(워크스페이스): 동일한 코드베이스로 여러 환경(dev/staging/prod)을 관리하는 방법.

    멀티 클라우드 구성에서 핵심은 하나의 Terraform 프로젝트에 여러 프로바이더를 동시에 선언하는 거예요. 처음엔 이게 되나 싶었는데, 실제로 해보니까 생각보다 깔끔하게 동작하더라고요.

    단일 클라우드 vs 멀티 클라우드 Terraform 비교

    구분 단일 클라우드 멀티 클라우드
    Provider 수 1개 2개 이상
    State 관리 단일 backend 분리 또는 통합 backend
    인증 관리 단순 각 클라우드별 별도 인증 필요
    코드 복잡도 낮음 중~높음 (모듈화 필수)
    장점 단순, 빠른 구성 벤더 독립성, 최적 서비스 조합

    사전 준비: 환경 세팅부터 제대로

    본격적인 구성 전에 준비해야 할 것들이 있어요. 저도 이 부분에서 삽질을 좀 했습니다 ㅎㅎ. 특히 인증 부분에서요.

    1. Terraform 설치

    Terraform은 1.0 버전 이후로 꽤 안정화됐어요. 멀티 클라우드 구성을 처음 하신다면 최신 stable 버전을 사용하시길 권장합니다.

    # macOS (Homebrew)
    brew tap hashicorp/tap
    brew install hashicorp/tap/terraform
    
    # Ubuntu/Debian
    wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
    echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
    sudo apt update && sudo apt install terraform
    
    # 버전 확인
    terraform version

    2. 각 클라우드 CLI 설치 및 인증

    Terraform이 각 클라우드와 통신하려면 인증 정보가 필요해요. CLI를 통한 인증이 가장 깔끔합니다.

    # AWS CLI 인증
    aws configure
    # AWS Access Key ID, Secret Access Key, Region 입력
    
    # GCP 인증
    gcloud auth application-default login
    # 브라우저에서 Google 계정 인증
    
    # Azure CLI 인증
    az login
    # 브라우저에서 Microsoft 계정 인증

    💡 팁: 실무에서는 각 클라우드의 서비스 계정(Service Account) 또는 IAM Role을 사용하는 게 보안상 훨씬 좋아요. 개인 계정 인증은 개발/테스트 환경에서만 쓰세요.

    실전 구현: 멀티 클라우드 Terraform 프로젝트 구성

    자, 이제 본론입니다. 제가 실제로 구성한 디렉터리 구조부터 보여드릴게요. 처음에 플랫 구조로 했다가 나중에 너무 복잡해져서 모듈 구조로 전면 개편했던 경험이 있어서, 처음부터 제대로 된 구조로 시작하시길 권해드립니다.

    프로젝트 디렉터리 구조

    multi-cloud-infra/
    ├── main.tf              # 메인 진입점, 프로바이더 설정
    ├── variables.tf         # 공통 변수 정의
    ├── outputs.tf           # 출력값 정의
    ├── terraform.tfvars     # 실제 변수값 (git에 올리지 마세요!)
    ├── backend.tf           # State 저장소 설정
    └── modules/
        ├── aws/
        │   ├── main.tf
        │   ├── variables.tf
        │   └── outputs.tf
        ├── gcp/
        │   ├── main.tf
        │   ├── variables.tf
        │   └── outputs.tf
        └── azure/
            ├── main.tf
            ├── variables.tf
            └── outputs.tf

    Step 1: 멀티 프로바이더 선언 (main.tf)

    여기가 핵심이에요. 세 개의 프로바이더를 동시에 선언합니다. alias(별칭)를 사용하면 같은 프로바이더를 여러 리전에서 사용할 수도 있어요.

    terraform {
      required_version = ">= 1.3.0"
    
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.0"
        }
        google = {
          source  = "hashicorp/google"
          version = "~> 5.0"
        }
        azurerm = {
          source  = "hashicorp/azurerm"
          version = "~> 3.0"
        }
      }
    }
    
    # AWS 프로바이더
    provider "aws" {
      region = var.aws_region
      # 환경변수 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 사용 권장
    }
    
    # GCP 프로바이더
    provider "google" {
      project = var.gcp_project_id
      region  = var.gcp_region
      # GOOGLE_APPLICATION_CREDENTIALS 환경변수 사용 권장
    }
    
    # Azure 프로바이더
    provider "azurerm" {
      features {}
      subscription_id = var.azure_subscription_id
      # az login으로 인증된 정보 사용
    }

    Step 2: 변수 정의 (variables.tf)

    variable "aws_region" {
      description = "AWS 배포 리전"
      type        = string
      default     = "ap-northeast-2"  # 서울 리전
    }
    
    variable "gcp_project_id" {
      description = "GCP 프로젝트 ID"
      type        = string
    }
    
    variable "gcp_region" {
      description = "GCP 배포 리전"
      type        = string
      default     = "asia-northeast3"  # 서울 리전
    }
    
    variable "azure_subscription_id" {
      description = "Azure 구독 ID"
      type        = string
      sensitive   = true
    }
    
    variable "environment" {
      description = "배포 환경 (dev/staging/prod)"
      type        = string
      default     = "dev"
    }
    
    variable "project_name" {
      description = "프로젝트명 (리소스 태그에 사용)"
      type        = string
    }

    ▲ 모듈화된 Terraform 프로젝트 구조 — 각 클라우드별 모듈이 독립적으로 관리되는 모습

    Step 3: 각 클라우드 모듈 구현

    이제 각 클라우드별 리소스를 모듈로 만들어볼게요. 예시로 각 클라우드에 VPC/네트워크와 기본 컴퓨팅 리소스를 만드는 구성입니다.

    AWS 모듈 (modules/aws/main.tf)

    # AWS VPC 생성
    resource "aws_vpc" "main" {
      cidr_block           = var.vpc_cidr
      enable_dns_hostnames = true
      enable_dns_support   = true
    
      tags = {
        Name        = "${var.project_name}-vpc"
        Environment = var.environment
        ManagedBy   = "terraform"
      }
    }
    
    # 퍼블릭 서브넷
    resource "aws_subnet" "public" {
      vpc_id                  = aws_vpc.main.id
      cidr_block              = var.public_subnet_cidr
      availability_zone       = "${var.aws_region}a"
      map_public_ip_on_launch = true
    
      tags = {
        Name        = "${var.project_name}-public-subnet"
        Environment = var.environment
      }
    }
    
    # S3 버킷 (State 저장용 또는 앱 스토리지)
    resource "aws_s3_bucket" "app_storage" {
      bucket = "${var.project_name}-${var.environment}-storage"
    
      tags = {
        Name        = "${var.project_name}-storage"
        Environment = var.environment
      }
    }
    
    resource "aws_s3_bucket_versioning" "app_storage" {
      bucket = aws_s3_bucket.app_storage.id
      versioning_configuration {
        status = "Enabled"
      }
    }

    GCP 모듈 (modules/gcp/main.tf)

    # GCP VPC 네트워크
    resource "google_compute_network" "main" {
      name                    = "${var.project_name}-network"
      auto_create_subnetworks = false
      project                 = var.gcp_project_id
    }
    
    # GCP 서브넷
    resource "google_compute_subnetwork" "main" {
      name          = "${var.project_name}-subnet"
      ip_cidr_range = var.subnet_cidr
      region        = var.gcp_region
      network       = google_compute_network.main.id
      project       = var.gcp_project_id
    }
    
    # GCS 버킷 (Google Cloud Storage)
    resource "google_storage_bucket" "app_storage" {
      name          = "${var.project_name}-${var.environment}-gcs"
      location      = "ASIA"
      project       = var.gcp_project_id
      force_destroy = false
    
      versioning {
        enabled = true
      }
    
      labels = {
        environment = var.environment
        managed_by  = "terraform"
      }
    }

    Azure 모듈 (modules/azure/main.tf)

    # Azure 리소스 그룹
    resource "azurerm_resource_group" "main" {
      name     = "${var.project_name}-${var.environment}-rg"
      location = var.azure_location
    
      tags = {
        environment = var.environment
        managed_by  = "terraform"
      }
    }
    
    # Azure Virtual Network
    resource "azurerm_virtual_network" "main" {
      name                = "${var.project_name}-vnet"
      address_space       = [var.vnet_cidr]
      location            = azurerm_resource_group.main.location
      resource_group_name = azurerm_resource_group.main.name
    
      tags = {
        environment = var.environment
      }
    }
    
    # Azure 서브넷
    resource "azurerm_subnet" "main" {
      name                 = "${var.project_name}-subnet"
      resource_group_name  = azurerm_resource_group.main.name
      virtual_network_name = azurerm_virtual_network.main.name
      address_prefixes     = [var.subnet_cidr]
    }
    
    # Azure Storage Account
    resource "azurerm_storage_account" "main" {
      name                     = "${replace(var.project_name, "-", "")}${var.environment}sa"
      resource_group_name      = azurerm_resource_group.main.name
      location                 = azurerm_resource_group.main.location
      account_tier             = "Standard"
      account_replication_type = "LRS"
    
      tags = {
        environment = var.environment
      }
    }

    Step 4: 모듈 호출 (루트 main.tf에 추가)

    # AWS 모듈 호출
    module "aws_infra" {
      source = "./modules/aws"
    
      project_name       = var.project_name
      environment        = var.environment
      aws_region         = var.aws_region
      vpc_cidr           = "10.0.0.0/16"
      public_subnet_cidr = "10.0.1.0/24"
    }
    
    # GCP 모듈 호출
    module "gcp_infra" {
      source = "./modules/gcp"
    
      project_name    = var.project_name
      environment     = var.environment
      gcp_project_id  = var.gcp_project_id
      gcp_region      = var.gcp_region
      subnet_cidr     = "10.1.0.0/24"
    }
    
    # Azure 모듈 호출
    module "azure_infra" {
      source = "./modules/azure"
    
      project_name   = var.project_name
      environment    = var.environment
      azure_location = "koreacentral"
      vnet_cidr      = "10.2.0.0/16"
      subnet_cidr    = "10.2.1.0/24"
    }

    Step 5: State Backend 설정 (backend.tf)

    멀티 클라우드에서 State 관리는 정말 중요해요. 팀 작업을 한다면 로컬 state 파일은 절대 안 됩니다. 저는 AWS S3를 State 저장소로, DynamoDB를 락(Lock) 관리에 사용했어요.

    terraform {
      backend "s3" {
        bucket         = "your-terraform-state-bucket"
        key            = "multi-cloud/terraform.tfstate"
        region         = "ap-northeast-2"
        encrypt        = true
        dynamodb_table = "terraform-state-lock"
      }
    }

    Step 6: 배포 실행

    # 초기화 (프로바이더 플러그인 다운로드)
    terraform init
    
    # 변경사항 미리보기
    terraform plan -var-file="terraform.tfvars"
    
    # 실제 배포
    terraform apply -var-file="terraform.tfvars"
    
    # 특정 모듈만 배포하고 싶을 때
    terraform apply -target=module.aws_infra
    
    # 리소스 삭제
    terraform destroy -var-file="terraform.tfvars"

    ⚠️ 삽질 기록: 이런 문제들 겪었습니다

    제가 처음 멀티 클라우드 Terraform 구성할 때 겪었던 문제들이에요. 여러분은 같은 삽질 안 하셨으면 해서 솔직하게 공유합니다.

    문제 1: Provider 버전 충돌

    terraform init 할 때 프로바이더 버전 충돌 에러가 났어요. ~> 연산자로 버전 범위를 지정하면 대부분 해결됩니다. 너무 엄격하게 고정하면 나중에 업그레이드할 때 고생해요.

    # 이렇게 하면 마이너 버전 업그레이드는 허용
    version = "~> 5.0"   # 5.x 버전 허용, 6.0은 안 됨
    
    # 이렇게 하면 패치 버전만 허용
    version = "~> 5.1.0" # 5.1.x만 허용

    문제 2: Azure Storage Account 이름 규칙

    Azure Storage Account 이름은 소문자와 숫자만 되고, 하이픈(-) 사용이 불가예요. 이거 모르고 프로젝트명에 하이픈 넣었다가 에러 폭탄 맞았습니다… replace() 함수로 해결했어요.

    # 하이픈 제거
    name = "${replace(var.project_name, "-", "")}${var.environment}sa"

    문제 3: GCP API 활성화 누락

    GCP는 사용하려는 API를 사전에 활성화해야 해요. Terraform으로 리소스 만들려는데 API가 비활성화 상태면 에러가 납니다. 이것도 Terraform으로 관리할 수 있어요.

    resource "google_project_service" "compute" {
      project = var.gcp_project_id
      service = "compute.googleapis.com"
    
      disable_on_destroy = false
    }
    
    resource "google_project_service" "storage" {
      project = var.gcp_project_id
      service = "storage.googleapis.com"
    
      disable_on_destroy = false
    }
    
    # 리소스가 API 활성화 후 생성되도록 의존성 설정
    resource "google_compute_network" "main" {
      depends_on = [google_project_service.compute]
      # ...
    }

    문제 4: State 파일 잠금(Lock) 충돌

    팀원이랑 동시에 terraform apply를 실행했다가 State Lock 에러가 났어요. DynamoDB 락 테이블을 꼭 설정하세요. 혼자 작업해도 비정상 종료 후 락이 안 풀리는 경우가 있는데, 이때는 terraform force-unlock 명령어로 해결합니다.

    # 락 강제 해제 (Lock ID는 에러 메시지에서 확인)
    terraform force-unlock LOCK_ID

    검증: 제대로 만들어졌는지 확인하기

    드디어 됐다! 싶을 때 꼭 확인해야 할 것들이 있어요. terraform apply가 성공했다고 끝이 아니거든요.

    # 현재 State 확인
    terraform show
    
    # 특정 리소스 상세 확인
    terraform state show module.aws_infra.aws_vpc.main
    
    # 모든 리소스 목록 확인
    terraform state list
    
    # 출력값 확인
    terraform output

    각 클라우드 콘솔에서도 직접 확인해보세요. AWS 콘솔에서 VPC가 생겼는지, GCP 콘솔에서 네트워크가 보이는지, Azure 포털에서 리소스 그룹이 만들어졌는지 확인하는 거예요. 코드만 믿지 말고 눈으로 직접 확인하는 습관이 정말 중요합니다.

    ▲ terraform apply 완료 후 각 클라우드 콘솔에서 리소스가 정상 생성된 결과 확인 화면

    Outputs으로 중요 정보 추출하기

    # outputs.tf
    output "aws_vpc_id" {
      description = "생성된 AWS VPC ID"
      value       = module.aws_infra.vpc_id
    }
    
    output "gcp_network_name" {
      description = "생성된 GCP 네트워크 이름"
      value       = module.gcp_infra.network_name
    }
    
    output "azure_resource_group" {
      description = "생성된 Azure 리소스 그룹 이름"
      value       = module.azure_infra.resource_group_name
    }

    💡 실무에서 쓰는 Best Practice 몇 가지

    13년 동안 인프라 일 하면서 쌓인 노하우를 좀 더 공유할게요.

    • terraform.tfvars는 절대 Git에 올리지 마세요. .gitignore에 꼭 추가하고, 민감 정보는 환경변수나 Vault로 관리하세요.
    • 환경별 tfvars 파일을 분리하세요. dev.tfvars, staging.tfvars, prod.tfvars처럼요. -var-file 옵션으로 지정해서 사용합니다.
    • Terraform Cloud 또는 Atlantis 도입을 고려하세요. 팀 규모가 커지면 PR 기반 Terraform 실행 환경이 필요해집니다.
    • 모든 리소스에 태그/라벨을 달아두세요. 멀티 클라우드에서 비용 관리하려면 태그 전략이 필수예요. ManagedBy = terraform, Environment = dev 같은 공통 태그부터 시작하세요.
    • Plan 결과는 팀원과 공유하세요. terraform plan -out=plan.tfplan으로 저장하고, terraform show plan.tfplan으로 리뷰받는 프로세스를 만들면 좋아요.

    ▲ Terraform 멀티 클라우드 운영을 위한 Best Practice 요약 — 보안, 협업, 비용 관리 핵심 포인트

    자주 묻는 질문 (FAQ)

    Q. Terraform과 Pulumi 중 어떤 걸 써야 하나요?

    Pulumi는 일반 프로그래밍 언어(Python, TypeScript 등)로 인프라를 정의할 수 있어서 개발자 친화적이에요. 반면 Terraform은 HCL이라는 전용 언어를 쓰지만 생태계가 훨씬 크고, 커뮤니티 모듈이 풍부하거든요. 처음 IaC를 시작한다면 Terraform을 추천해요.

    Q. 멀티 클라우드 구성에서 State를 어디에 저장해야 하나요?

    저는 AWS S3 + DynamoDB 조합을 주로 사용합니다. Terraform Cloud를 쓰면 State 관리, 락, 협업 기능을 한 번에 해결할 수 있어요. 팀 규모가 있다면 Terraform Cloud 무료 플랜부터 시작해보세요.

    Q. 멀티 클라우드 비용이 너무 많이 나올 것 같아요.

    맞아요, 이게 현실적인 고민이죠. 각 클라우드의 프리 티어(Free Tier)를 최대한 활용하고, 개발 환경은 작업이 끝나면 terraform destroy로 날리는 습관을 들이세요. 저도 홈랩에서는 퇴근 전에 꼭 destroy 합니다 😅

    마무리: 멀티 클라우드는 복잡하지만, Terraform이 있으면 달라요

    처음에 AWS IaC 하나 배우는 것도 버거웠는데, GCP IaC, Azure IaC까지 동시에 다루게 될 줄은 몰랐어요. 근데 Terraform 멀티 클라우드 구성을 제대로 한 번 해놓으면, 그다음부터는 정말 편하더라고요. 새로운 리소스 추가할 때 코드 몇 줄 추가하고 apply만 하면 되거든요.

    이 글에서 다룬 내용을 정리하면:

    1. Terraform에서 여러 Provider를 동시에 선언해 멀티 클라우드 관리 가능
    2. 모듈(Module) 구조로 각 클라우드 코드를 분리해 유지보수성 향상
    3. State는 반드시 원격 Backend(S3 + DynamoDB 등)에 저장
    4. 인증 정보, 민감 변수는 절대 코드에 하드코딩 금지
    5. 태그 전략을 처음부터 설계해야 멀티 클라우드 비용 관리가 됨

    다음 글에서는 Terraform과 GitHub Actions를 연동해서 CI/CD 파이프라인을 구축하는 방법을 다룰 예정이에요. Pull Request가 올라오면 자동으로 terraform plan 결과를 코멘트로 달아주는 그 워크플로우요. 꽤 실용적이니까 기대해주세요!

    질문이나 다른 삽질 경험 있으시면 댓글로 남겨주세요. 같이 고민해봐요 😊

  • [Cloud] Cloudflare Workers로 서버리스 API 및 엣지 컴퓨팅 구축 가이드

    서버 없이 전 세계에서 실행되는 API? Cloudflare Workers 이야기

    솔직히 말씀드리면, 처음 Cloudflare Workers를 접했을 때 “이게 뭔 소리지?” 싶었거든요. 서버리스(Serverless)라는 개념 자체는 알고 있었는데, “엣지에서 실행된다”는 말이 딱 와닿지 않았습니다. 그러다 실제로 간단한 API 하나를 올려봤는데… 서울에서 요청하면 서울 근처 데이터센터에서, 미국에서 요청하면 미국 데이터센터에서 응답이 오는 거예요. 레이턴시(Latency, 응답 지연)가 확 줄어드는 게 느껴졌습니다. 그때 “아, 이래서 엣지 컴퓨팅이구나” 하고 감이 왔죠.

    홈랩에서 이것저것 돌리다 보면 늘 고민이 생기잖아요. 외부에서 접근 가능한 간단한 API가 필요한데, 서버 하나 띄우자니 유지보수가 귀찮고, 클라우드 VM 쓰자니 비용이 아깝고. 그 틈새를 Cloudflare Workers가 정말 잘 채워줬습니다. 오늘은 제가 실제로 Workers로 서버리스 API를 구축하면서 겪은 경험을 바탕으로, 처음 시작하시는 분들도 따라할 수 있게 정리해 드릴게요.

    ▲ Cloudflare Workers의 엣지 컴퓨팅 구조 — 전 세계 데이터센터에서 코드가 실행되는 개념도


    Cloudflare Workers가 뭔지 쉽게 풀어보기

    엣지 컴퓨팅(Edge Computing)이란?

    일반적인 클라우드 서버는 특정 리전(Region, 지역)에 고정되어 있어요. 예를 들어 AWS ap-northeast-2(서울 리전)에 서버를 올리면, 미국 사용자가 접속할 때는 태평양을 건너서 요청이 오가야 하죠. 이게 레이턴시의 주범입니다.

    엣지 컴퓨팅은 이 문제를 뒤집어서 생각한 거예요. “서버를 중앙에 두지 말고, 사용자 가까이에 두자!” 라는 발상이죠. Cloudflare는 전 세계 200개 이상의 도시에 데이터센터(PoP, Point of Presence)를 운영하고 있는데, Workers 코드가 이 모든 위치에서 실행됩니다. 쉽게 말해, 사용자가 어디에 있든 가장 가까운 서버에서 응답이 오는 거예요.

    Workers의 핵심 특징

    • V8 엔진 기반: Node.js가 아닌 V8 Isolate(아이솔레이트) 방식으로 실행됩니다. 컨테이너보다 훨씬 빠르게 시작해요 (콜드 스타트 거의 없음)
    • JavaScript / TypeScript 지원: 프론트엔드 개발자분들도 친숙하게 쓸 수 있어요
    • Workers KV: 엣지에서 사용 가능한 키-값(Key-Value) 스토리지
    • 무료 티어 존재: 하루 10만 요청까지 무료 (최신 정보는 Cloudflare 공식 가격 페이지 확인 권장)
    • 배포가 초간단: Wrangler CLI 명령어 하나로 전 세계 배포 완료
    구분 전통적 서버 일반 서버리스 (Lambda 등) Cloudflare Workers
    실행 위치 단일 리전 단일 리전 전 세계 엣지
    콜드 스타트 없음 수백ms ~ 수초 거의 없음 (0ms 수준)
    관리 부담 높음 낮음 매우 낮음
    런타임 자유 Node.js, Python 등 V8 (JS/TS/Wasm)
    무료 티어 없음 있음 있음

    환경 설정: Wrangler CLI 설치부터 시작

    본격적으로 시작해볼게요. Wrangler는 Cloudflare Workers 개발을 위한 공식 CLI(Command Line Interface) 도구입니다. 이게 없으면 아무것도 못 하니까 먼저 설치부터요.

    1단계: Node.js 및 Wrangler 설치

    Node.js 16 이상이 설치되어 있다는 전제 하에 진행합니다.

    # Wrangler CLI 전역 설치
    npm install -g wrangler
    
    # 설치 확인
    wrangler --version
    
    # Cloudflare 계정 로그인 (브라우저가 열립니다)
    wrangler login

    로그인하면 브라우저에서 Cloudflare 대시보드로 이동해서 권한 허용을 요청해요. 그냥 허용 누르시면 됩니다. 처음에 이 과정이 좀 낯설었는데, 익숙해지면 진짜 편하더라고요.

    2단계: 새 Workers 프로젝트 생성

    # 새 프로젝트 생성 (대화형 설정)
    npm create cloudflare@latest my-api-worker
    
    # 프로젝트 디렉토리로 이동
    cd my-api-worker

    생성 과정에서 몇 가지 질문이 나오는데, “Hello World” 템플릿 선택하고, TypeScript 쓸지 JavaScript 쓸지 고르면 됩니다. 저는 보통 TypeScript를 선택하는 편이에요. 타입 안정성이 있으니까요.

    프로젝트 구조는 이렇게 됩니다:

    my-api-worker/
    ├── src/
    │   └── index.ts        # 메인 Worker 코드
    ├── wrangler.toml       # Worker 설정 파일
    ├── package.json
    └── tsconfig.json

    실전 구현: 서버리스 REST API 만들기

    이제 진짜 코드를 써볼게요. 단순한 “Hello World”는 재미없으니까, 실제로 쓸 만한 간단한 REST API를 만들어보겠습니다. 라우팅(Routing)과 메서드 분기를 포함한 구조예요.

    ▲ Wrangler를 이용한 로컬 개발 환경 — wrangler dev 명령으로 로컬에서 Workers를 테스트할 수 있어요

    기본 라우팅이 포함된 API Worker

    // src/index.ts
    
    export interface Env {
      // 나중에 KV 바인딩을 여기 추가할 거예요
      MY_KV: KVNamespace;
    }
    
    export default {
      async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
        const url = new URL(request.url);
        const path = url.pathname;
        const method = request.method;
    
        // CORS 헤더 설정
        const corsHeaders = {
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
          'Access-Control-Allow-Headers': 'Content-Type',
        };
    
        // Preflight 요청 처리
        if (method === 'OPTIONS') {
          return new Response(null, { headers: corsHeaders });
        }
    
        // 라우팅 처리
        if (path === '/api/hello' && method === 'GET') {
          return Response.json(
            { message: '안녕하세요! Cloudflare Workers에서 응답합니다.', timestamp: Date.now() },
            { headers: corsHeaders }
          );
        }
    
        if (path === '/api/items' && method === 'GET') {
          return handleGetItems(env, corsHeaders);
        }
    
        if (path === '/api/items' && method === 'POST') {
          return handlePostItem(request, env, corsHeaders);
        }
    
        // 404 처리
        return Response.json(
          { error: 'Not Found', path },
          { status: 404, headers: corsHeaders }
        );
      },
    };
    
    // GET /api/items — KV에서 아이템 목록 조회
    async function handleGetItems(env: Env, headers: Record<string, string>): Promise<Response> {
      const data = await env.MY_KV.get('items', 'json') as string[] | null;
      return Response.json(
        { items: data ?? [] },
        { headers }
      );
    }
    
    // POST /api/items — KV에 아이템 추가
    async function handlePostItem(
      request: Request,
      env: Env,
      headers: Record<string, string>
    ): Promise<Response> {
      const body = await request.json() as { name: string };
    
      if (!body.name) {
        return Response.json(
          { error: 'name 필드가 필요합니다' },
          { status: 400, headers }
        );
      }
    
      const existing = await env.MY_KV.get('items', 'json') as string[] | null;
      const items = existing ?? [];
      items.push(body.name);
    
      await env.MY_KV.put('items', JSON.stringify(items));
    
      return Response.json(
        { success: true, items },
        { status: 201, headers }
      );
    }

    코드 보시면 구조가 꽤 직관적이죠? fetch 함수가 모든 HTTP 요청의 진입점이고, URL 경로와 HTTP 메서드로 분기처리를 합니다. Node.js의 Express 같은 느낌이에요.

    Workers KV 설정하기

    Workers KV는 엣지에서 사용 가능한 분산 키-값 스토리지예요. 데이터베이스를 별도로 운영하지 않아도 간단한 데이터를 저장하고 조회할 수 있거든요. 쓰기는 약간의 전파 지연이 있지만, 읽기는 엣지에서 바로 처리되니까 빠릅니다.

    # KV 네임스페이스(Namespace) 생성
    wrangler kv:namespace create MY_KV
    
    # 로컬 개발용 프리뷰 네임스페이스도 생성
    wrangler kv:namespace create MY_KV --preview

    명령어 실행 후 출력되는 ID를 wrangler.toml에 붙여넣어야 합니다.

    # wrangler.toml
    name = "my-api-worker"
    main = "src/index.ts"
    compatibility_date = "2024-01-01"
    
    [[kv_namespaces]]
    binding = "MY_KV"          # 코드에서 env.MY_KV로 접근
    id = "여기에_실제_KV_ID_입력"
    preview_id = "여기에_프리뷰_KV_ID_입력"

    💡 팁: binding 이름이 TypeScript 인터페이스의 Env에 선언한 이름과 일치해야 해요. 처음에 이게 안 맞아서 에러가 났었는데, 삽질 좀 했습니다 ㅎㅎ

    로컬에서 테스트하기

    # 로컬 개발 서버 실행
    wrangler dev
    
    # 기본적으로 http://localhost:8787 에서 실행됩니다
    
    # 다른 터미널에서 API 테스트
    curl http://localhost:8787/api/hello
    
    # POST 테스트
    curl -X POST http://localhost:8787/api/items \
      -H "Content-Type: application/json" \
      -d '{"name": "테스트 아이템"}'
    
    # GET으로 확인
    curl http://localhost:8787/api/items

    로컬에서 잘 돌아가면 이제 배포할 차례예요!

    # 전 세계 배포 (진짜 이 명령어 하나예요)
    wrangler deploy
    
    # 배포 후 URL이 출력됩니다
    # 예: https://my-api-worker.your-subdomain.workers.dev

    🎉 배포 완료! 이 URL로 전 세계 어디서든 접근 가능합니다. 처음에 이게 진짜로 되는 건지 믿기지 않아서 VPN으로 미국, 일본, 유럽 각각에서 테스트해봤는데 전부 잘 됐더라고요.


    ⚠️ 주의사항 및 실제 겪은 트러블슈팅

    좋은 것만 얘기하면 재미없죠. 실제로 쓰다 보면 꼭 한 번씩 걸리는 것들이 있어요.

    문제 1: Workers KV 쓰기 전파 지연

    KV에 데이터를 쓰고 바로 읽으면 이전 값이 나오는 경우가 있어요. KV는 최종 일관성(Eventual Consistency) 모델이라서, 쓰기 작업이 전 세계 엣지에 전파되는 데 시간이 걸립니다. 공식 문서에 따르면 최대 60초 정도 걸릴 수 있어요. 로컬 개발 환경에서는 이게 즉시 반영되는 것처럼 보이는데, 프로덕션에서는 다를 수 있습니다.

    해결책: 쓰기 직후 즉시 읽기가 필요한 로직은 KV 대신 Durable Objects(내구성 있는 객체, 강한 일관성 보장)를 고려하세요. 단순 캐시나 설정값 저장에는 KV가 딱 좋습니다.

    문제 2: CPU 시간 제한

    Workers는 요청당 CPU 시간 제한이 있어요. 무료 티어는 요청당 10ms, 유료 플랜은 더 길어요. 무거운 연산(이미지 처리, 복잡한 암호화 등)은 Workers에 맞지 않습니다. 저도 처음에 이미지 리사이징 로직을 Workers에 넣으려다 바로 한계에 부딪혔어요.

    해결책: Workers는 가볍고 빠른 로직에 최적화되어 있거든요. 무거운 작업은 백엔드 서버로 위임하고, Workers는 게이트웨이(Gateway, 진입 관문) 역할만 맡기는 패턴이 좋습니다.

    문제 3: Node.js API 호환성 이슈

    Workers는 Node.js 런타임이 아니에요. V8 Isolate 기반이라 Node.js 전용 API(예: fs, path, crypto 일부)가 기본적으로 없어요. npm 패키지 중에도 Node.js 내장 모듈에 의존하는 것들은 Workers에서 안 돌아갈 수 있습니다.

    해결책: wrangler.toml에 node_compat = true를 추가하면 일부 Node.js API 폴리필(Polyfill, 하위 호환 구현체)이 활성화됩니다. 그래도 안 되는 경우엔 Workers 환경에 맞는 대안 라이브러리를 찾아야 해요.

    # wrangler.toml에 추가
    node_compat = true

    문제 4: 환경 변수 관리

    API 키 같은 민감한 정보를 코드에 하드코딩하면 절대 안 되죠. Workers에는 Secrets(시크릿) 기능이 있어요.

    # 시크릿 등록 (값은 프롬프트로 입력)
    wrangler secret put MY_API_KEY
    
    # 등록된 시크릿 목록 확인
    wrangler secret list
    // 코드에서 env로 접근
    export interface Env {
      MY_KV: KVNamespace;
      MY_API_KEY: string;  // 시크릿도 Env에 선언
    }
    
    // 사용 예시
    const apiKey = env.MY_API_KEY;

    결과 확인: 대시보드에서 모니터링하기

    배포 후 Cloudflare 대시보드에서 Workers 섹션을 보면 꽤 유용한 정보들을 확인할 수 있어요.

    ▲ Cloudflare Workers 대시보드 — 요청 수, 에러율, CPU 시간 등 실시간 모니터링이 가능해요

    • ✅ 요청 수(Requests): 분/시간/일별 요청량 확인
    • ✅ 에러율(Error Rate): 4xx, 5xx 에러 비율
    • ✅ CPU 시간: 요청당 평균 CPU 사용 시간
    • ✅ 실시간 로그: wrangler tail 명령으로 실시간 로그 스트리밍 가능
    # 실시간 로그 스트리밍
    wrangler tail
    
    # 특정 Worker 지정
    wrangler tail my-api-worker

    로그에서 요청 경로, 응답 코드, 실행 시간 등이 다 나와요. 디버깅할 때 꽤 유용하더라고요. 처음에 이 기능 몰라서 console.log 찍어가며 고생했는데, 알고 나서 “진작 쓸걸” 했죠.

    커스텀 도메인 연결

    기본 제공되는 *.workers.dev 도메인 말고, 본인 도메인을 연결할 수도 있어요. Cloudflare에 도메인이 등록되어 있다면 Workers 설정에서 Route(라우트, 트래픽 경로)를 추가하면 됩니다.

    # wrangler.toml에 라우트 추가
    [[routes]]
    pattern = "api.yourdomain.com/*"
    zone_name = "yourdomain.com"

    이렇게 하면 api.yourdomain.com으로 들어오는 모든 요청이 Workers로 처리돼요. 깔끔하죠?


    마무리: Workers가 잘 맞는 상황 vs 아닌 상황

    ▲ Cloudflare Workers 적합/비적합 사용 사례 요약 — 어떤 상황에서 Workers를 선택해야 할지 한눈에 보기

    몇 달 써보면서 느낀 건, Workers는 만능이 아니에요. 잘 맞는 상황이 있고, 억지로 쓰면 오히려 복잡해지는 상황도 있더라고요.

    ✅ Workers가 잘 맞는 경우

    • CORS 프록시, API 게이트웨이
    • A/B 테스트, 기능 플래그(Feature Flag) 처리
    • 간단한 인증/인가(Authentication/Authorization) 미들웨어
    • 정적 사이트의 동적 기능 추가 (Jamstack 패턴)
    • 봇 차단, 요청 필터링
    • 지리적으로 분산된 캐시 레이어

    ⚠️ 다른 선택지를 고려해야 할 경우

    • 무거운 연산이나 긴 실행 시간이 필요한 작업
    • 강한 데이터 일관성이 필요한 트랜잭션 처리
    • 파일 시스템 접근이 필요한 경우
    • 기존 Node.js/Python 등 런타임 의존성이 많은 경우

    자주 묻는 질문 (FAQ)

    Q. Workers KV와 일반 데이터베이스의 차이는?
    A. Workers KV는 읽기에 최적화된 분산 스토리지예요. 복잡한 쿼리나 관계형 데이터가 필요하다면 Cloudflare D1(SQLite 기반) 이나 외부 DB를 연동하는 게 낫습니다.
    Q. 무료로 얼마나 쓸 수 있나요?
    A. 공식 문서 기준 무료 티어는 하루 10만 요청까지 제공합니다. 정확한 현행 조건은 Cloudflare 공식 가격 페이지에서 반드시 확인하세요. 자주 바뀌는 편이거든요.
    Q. TypeScript 말고 다른 언어도 되나요?
    A. WebAssembly(웹어셈블리)로 컴파일되는 언어라면 이론상 가능해요. Rust로 Workers를 작성하는 사례도 있습니다. 다만 생태계는 JS/TS가 가장 풍부해요.

    오늘 다룬 내용이 Cloudflare Workers로 서버리스 API와 엣지 컴퓨팅을 시작하는 데 도움이 됐으면 좋겠어요. 다음 글에서는 Cloudflare D1(서버리스 SQLite 데이터베이스)과 Workers를 연동해서 더 완성도 있는 API를 만드는 방법을 다룰 예정입니다. Workers KV만으로는 아쉬울 때 딱 좋은 조합이거든요.

    궁금한 점이나 직접 해보다가 막히는 부분 있으시면 댓글로 남겨주세요. 같이 삽질해봅시다! 😄

  • [Cloud] Terraform 원격 상태 관리: S3, GCS, Azure Blob 백엔드 설정 가이드

    로컬 tfstate 파일, 언제까지 쓸 건가요?

    Terraform을 처음 배울 때 저도 그냥 로컬에 terraform.tfstate 파일 두고 썼거든요. 혼자 쓸 때는 문제없었는데, 팀원이 생기고 나서 진짜 지옥이 시작됐습니다. 누군가 apply 해놓고 상태 파일 커밋을 깜빡한 거예요. 그 다음 날 제가 apply 했더니… 리소스 중복 생성 오류가 펑펑 터졌습니다. 😅

    그때부터 Terraform 원격 상태 관리를 제대로 공부하기 시작했어요. S3, GCS, Azure Blob — 클라우드별로 백엔드 설정 방법이 다 다르거든요. 오늘은 실제 현업에서 직접 써본 경험을 바탕으로, 세 가지 백엔드를 한 번에 정리해드리겠습니다.

    ▲ Terraform 원격 백엔드 구성 개요 — 여러 팀원이 동일한 원격 상태 파일을 공유하며 협업하는 구조입니다.


    Terraform 상태(State)란 뭔가요? — 개념부터 잡고 가기

    쉽게 말해서, Terraform은 자기가 만든 인프라를 상태 파일(state file)에 기록해둡니다. 다음에 plan이나 apply를 실행할 때 “지금 실제로 뭐가 있는지”를 이 파일 보고 판단하는 거예요. 일종의 인프라 장부 같은 거죠.

    기본값은 로컬 파일(terraform.tfstate)인데, 여기서 문제가 생깁니다.

    • 팀원끼리 상태 파일이 달라지는 상태 불일치(State Drift) 발생
    • 동시에 apply하면 상태 파일이 깨지는 동시성 문제
    • 상태 파일에 민감한 정보(비밀번호, 키 등)가 포함될 수 있는 보안 문제
    • 로컬 파일 분실 시 인프라 관리 불가능해지는 복구 불가 리스크

    이 모든 걸 해결해주는 게 바로 원격 백엔드(Remote Backend)입니다. 상태 파일을 클라우드 스토리지에 저장하고, 잠금(Lock) 메커니즘으로 동시 접근을 막는 방식이에요.

    백엔드별 잠금 메커니즘 비교

    백엔드 상태 저장소 상태 잠금 암호화 주요 클라우드
    S3 백엔드 AWS S3 DynamoDB 테이블 SSE-S3 / SSE-KMS AWS
    GCS 백엔드 Google Cloud Storage GCS 내장 Google 관리 키 / CMEK GCP
    Azure Blob 백엔드 Azure Blob Storage Blob 리스(Lease) Azure Storage 암호화 Azure

    GCS는 별도 잠금 인프라 없이 내장 기능으로 처리해줘서 가장 설정이 간단하더라고요. S3는 DynamoDB를 따로 만들어야 해서 초반에는 좀 번거롭긴 한데, 한 번 설정하면 정말 탄탄합니다.


    AWS S3 백엔드 설정 — Terraform 원격 상태 관리 실전 가이드

    AWS를 메인으로 쓰신다면 S3 백엔드가 가장 표준적인 선택입니다. 저도 현업에서 가장 많이 써본 조합이고, 안정성도 검증된 방식이거든요.

    1단계: S3 버킷과 DynamoDB 테이블 생성

    먼저 상태 파일을 저장할 S3 버킷과 잠금용 DynamoDB 테이블을 만들어야 합니다. 이걸 직접 Terraform으로 만들어도 되는데, 닭이 먼저냐 달걀이 먼저냐 문제가 생기거든요. 저는 보통 AWS CLI로 먼저 만듭니다.

    # S3 버킷 생성 (버전 관리 활성화 필수!)
    aws s3api create-bucket \
      --bucket my-terraform-state-prod \
      --region ap-northeast-2 \
      --create-bucket-configuration LocationConstraint=ap-northeast-2
    
    # 버전 관리 활성화 — 상태 파일 롤백을 위해 반드시 켜세요
    aws s3api put-bucket-versioning \
      --bucket my-terraform-state-prod \
      --versioning-configuration Status=Enabled
    
    # 퍼블릭 액세스 차단 — 보안상 필수
    aws s3api put-public-access-block \
      --bucket my-terraform-state-prod \
      --public-access-block-configuration \
        BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
    
    # DynamoDB 테이블 생성 (LockID가 파티션 키여야 합니다)
    aws dynamodb create-table \
      --table-name terraform-state-lock \
      --attribute-definitions AttributeName=LockID,AttributeType=S \
      --key-schema AttributeName=LockID,KeyType=HASH \
      --billing-mode PAY_PER_REQUEST \
      --region ap-northeast-2

    2단계: backend.tf 설정 파일 작성

    # backend.tf
    terraform {
      backend "s3" {
        bucket         = "my-terraform-state-prod"
        key            = "prod/ap-northeast-2/vpc/terraform.tfstate"
        region         = "ap-northeast-2"
        encrypt        = true  # 서버 사이드 암호화 활성화
        dynamodb_table = "terraform-state-lock"  # 잠금용 DynamoDB 테이블
    
        # KMS 키로 암호화하려면 아래 추가
        # kms_key_id = "arn:aws:kms:ap-northeast-2:123456789:key/your-key-id"
      }
    }

    💡 key 경로 설계 팁: 환경/리전/서비스명/terraform.tfstate 형태로 계층 구조를 잡으면 나중에 상태 파일이 수십 개 생겨도 관리하기 훨씬 편합니다. 저는 이 규칙 안 지켰다가 나중에 상태 파일 찾느라 정말 고생했거든요. 🤦

    3단계: 백엔드 초기화

    terraform init
    
    # 기존 로컬 상태를 원격으로 마이그레이션할 때
    terraform init -migrate-state

    S3 백엔드용 IAM 최소 권한 정책

    팀원들이 Terraform을 실행할 때 필요한 최소 권한만 부여하세요. 과도한 권한은 보안 위험이거든요.

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "s3:ListBucket",
            "s3:GetBucketVersioning"
          ],
          "Resource": "arn:aws:s3:::my-terraform-state-prod"
        },
        {
          "Effect": "Allow",
          "Action": [
            "s3:GetObject",
            "s3:PutObject",
            "s3:DeleteObject"
          ],
          "Resource": "arn:aws:s3:::my-terraform-state-prod/prod/*"
        },
        {
          "Effect": "Allow",
          "Action": [
            "dynamodb:DescribeTable",
            "dynamodb:GetItem",
            "dynamodb:PutItem",
            "dynamodb:DeleteItem"
          ],
          "Resource": "arn:aws:dynamodb:ap-northeast-2:123456789012:table/terraform-state-lock"
        }
      ]
    }

    ⚠️ 중요: 123456789012는 여러분의 AWS 계정 ID로 바꿔야 합니다. 또한 /prod/* 경로만 허용해서 다른 환경의 상태 파일은 건드리지 못하게 제한하는 게 좋습니다.

  • [Cloud] Terraform 멀티 클라우드 IaC 자동화 가이드 — AWS/GCP/Azure 통합 관리

    [Cloud] Terraform 멀티 클라우드 IaC 자동화 가이드 — AWS/GCP/Azure 통합 관리

    멀티 클라우드, 왜 이렇게 복잡한 걸까요?

    솔직히 말씀드리면, 저도 처음 멀티 클라우드 환경을 맡았을 때 머리가 좀 아팠습니다. AWS 콘솔 따로, GCP 콘솔 따로, Azure 포털 따로… 각각 로그인하고, 각각 다른 방식으로 리소스 만들고, 나중에 뭘 어디에 만들었는지 파악도 안 되는 그 상황 말이에요. 혹시 이런 경험 있으신가요?

    13년 동안 인프라 엔지니어로 일하면서 클라우드 환경이 단일 벤더에서 멀티 클라우드로 넘어가는 걸 직접 겪었는데요. 이게 비즈니스 연속성 확보나 벤더 종속(Vendor Lock-in) 방지 측면에서는 분명히 좋은 전략이에요. 근데 관리가 지옥이 되기 시작하거든요.

    그 해결책이 바로 Terraform 멀티 클라우드 IaC 자동화입니다. 오늘은 Terraform을 이용해서 AWS, GCP, Azure를 하나의 코드베이스로 관리하는 방법을 실제 경험 기반으로 풀어드릴게요.

    Terraform 멀티 클라우드 아키텍처 다이어그램 — AWS, GCP, Azure 동시 관리

    ▲ Terraform 하나로 AWS, GCP, Azure를 동시에 관리하는 멀티 클라우드 아키텍처 개요

    Terraform IaC가 뭔지 먼저 짚고 넘어가요

    IaC(Infrastructure as Code, 코드로 인프라를 정의하는 방식)는 이름 그대로 서버, 네트워크, 데이터베이스 같은 인프라를 코드 파일로 정의하고 버전 관리하는 방식입니다. 쉽게 말해, 클릭클릭으로 콘솔에서 만들던 걸 코드로 적어두는 거예요.

    Terraform은 HashiCorp가 만든 오픈소스 IaC 도구인데요. 가장 큰 장점이 프로바이더(Provider) 개념입니다. AWS용 프로바이더, GCP용 프로바이더, Azure용 프로바이더를 각각 선언하면, 하나의 Terraform 코드베이스에서 세 클라우드를 동시에 다룰 수 있어요. 이게 진짜 강력한 포인트예요.

    비교 항목 콘솔 수동 관리 Terraform IaC 자동화
    반복 작업 매번 클릭 필요 코드 한 번 작성 후 재사용
    버전 관리 불가능 (변경 이력 추적 어려움) Git으로 완전한 이력 관리
    멀티 클라우드 각 콘솔 별도 접근 필요 단일 코드베이스로 통합 관리
    팀 협업 “누가 뭘 만들었는지” 파악 어려움 코드 리뷰로 변경 사항 투명 공유
    재현성 동일 환경 재현 어려움 동일 코드로 동일 환경 재현 보장

    프로젝트 구조 잡기 — 이게 제일 중요합니다

    처음 멀티 클라우드 Terraform을 짤 때 가장 많이 실수하는 게 디렉토리 구조거든요. 저도 처음엔 파일 다 때려넣고 나중에 엉망이 돼서 처음부터 다시 짠 적이 있습니다. 클라우드별로 분리하고, 환경(dev/prod)별로도 분리하는 구조를 추천드려요.

    multi-cloud-infra/
    ├── modules/                    # 재사용 가능한 모듈 모음
    │   ├── aws/
    │   │   ├── vpc/
    │   │   │   ├── main.tf
    │   │   │   ├── variables.tf
    │   │   │   └── outputs.tf
    │   │   └── ec2/
    │   │       ├── main.tf
    │   │       ├── variables.tf
    │   │       └── outputs.tf
    │   ├── gcp/
    │   │   ├── vpc/
    │   │   └── compute/
    │   └── azure/
    │       ├── vnet/
    │       └── vm/
    ├── environments/
    │   ├── dev/
    │   │   ├── main.tf
    │   │   ├── variables.tf
    │   │   └── terraform.tfvars
    │   └── prod/
    │       ├── main.tf
    │       ├── variables.tf
    │       └── terraform.tfvars
    └── backend.tf                  # 원격 상태 저장소 설정
    

    💡 팁: modules 디렉토리에 클라우드별로 공통 모듈을 만들어두면, 나중에 환경을 추가할 때 모듈만 호출하면 되니까 진짜 편해요. 처음 구조 잡는 데 시간 좀 써도 나중에 열 배로 돌아옵니다.

    실전 구현 — Terraform 멀티 클라우드 프로바이더 설정부터

    자, 이제 실제로 코드를 작성해볼게요. 먼저 세 클라우드의 프로바이더를 한 파일에 선언하는 것부터 시작합니다.

    1단계: 프로바이더(Provider) 설정

    # providers.tf
    
    terraform {
      required_version = ">= 1.5.0"
    
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.0"
        }
        google = {
          source  = "hashicorp/google"
          version = "~> 5.0"
        }
        azurerm = {
          source  = "hashicorp/azurerm"
          version = "~> 3.0"
        }
      }
    
      # 원격 백엔드 (Remote Backend) — 팀 협업 시 필수!
      backend "s3" {
        bucket         = "my-terraform-state-bucket"
        key            = "multi-cloud/terraform.tfstate"
        region         = "ap-northeast-2"
        encrypt        = true
        dynamodb_table = "terraform-state-lock"  # 상태 잠금(State Locking)용
      }
    }
    
    # AWS 프로바이더
    provider "aws" {
      region = var.aws_region
    
      default_tags {
        tags = {
          ManagedBy   = "Terraform"
          Environment = var.environment
          Project     = var.project_name
        }
      }
    }
    
    # GCP 프로바이더
    provider "google" {
      project = var.gcp_project_id
      region  = var.gcp_region
    }
    
    # Azure 프로바이더
    provider "azurerm" {
      features {}
      subscription_id = var.azure_subscription_id
    }
    

    여기서 중요한 포인트! 원격 백엔드(Remote Backend)는 팀으로 작업할 때 절대 빠지면 안 됩니다. 상태 파일(State File)을 로컬에 두면 팀원끼리 충돌이 생기거든요. 저도 초반에 이걸 몰라서 상태 파일 날려먹은 적이 있습니다.

    2단계: 변수 파일 설정

    # variables.tf
    
    variable "environment" {
      description = "배포 환경 (dev, staging, prod)"
      type        = string
      default     = "dev"
    }
    
    variable "project_name" {
      description = "프로젝트 이름"
      type        = string
    }
    
    # AWS 관련 변수
    variable "aws_region" {
      description = "AWS 리전"
      type        = string
      default     = "ap-northeast-2"  # 서울 리전
    }
    
    # GCP 관련 변수
    variable "gcp_project_id" {
      description = "GCP 프로젝트 ID"
      type        = string
    }
    
    variable "gcp_region" {
      description = "GCP 리전"
      type        = string
      default     = "asia-northeast3"  # 서울 리전
    }
    
    # Azure 관련 변수
    variable "azure_subscription_id" {
      description = "Azure 구독 ID"
      type        = string
      sensitive   = true  # 민감 정보 마스킹
    }
    
    variable "azure_location" {
      description = "Azure 지역"
      type        = string
      default     = "Korea Central"
    }
    
    Terraform 멀티 클라우드 프로바이더 설정 코드 — AWS GCP Azure 동시 구성

    ▲ 세 클라우드의 프로바이더를 하나의 코드베이스에서 관리하는 실제 구성 예시

    3단계: AWS VPC 모듈 작성

    # modules/aws/vpc/main.tf
    
    resource "aws_vpc" "main" {
      cidr_block           = var.vpc_cidr
      enable_dns_hostnames = true
      enable_dns_support   = true
    
      tags = {
        Name = "${var.project_name}-${var.environment}-vpc"
      }
    }
    
    resource "aws_subnet" "public" {
      count             = length(var.public_subnet_cidrs)
      vpc_id            = aws_vpc.main.id
      cidr_block        = var.public_subnet_cidrs[count.index]
      availability_zone = var.availability_zones[count.index]
    
      map_public_ip_on_launch = true
    
      tags = {
        Name = "${var.project_name}-public-subnet-${count.index + 1}"
        Type = "Public"
      }
    }
    
    resource "aws_internet_gateway" "main" {
      vpc_id = aws_vpc.main.id
    
      tags = {
        Name = "${var.project_name}-igw"
      }
    }
    

    4단계: GCP VPC 네트워크 모듈

    # modules/gcp/vpc/main.tf
    
    resource "google_compute_network" "main" {
      name                    = "${var.project_name}-${var.environment}-vpc"
      auto_create_subnetworks = false  # 커스텀 서브넷 사용
      project                 = var.project_id
    }
    
    resource "google_compute_subnetwork" "main" {
      name          = "${var.project_name}-subnet"
      ip_cidr_range = var.subnet_cidr
      region        = var.region
      network       = google_compute_network.main.id
      project       = var.project_id
    
      # Private Google Access — GCP 내부 서비스 접근용
      private_ip_google_access = true
    }
    
    # 방화벽 규칙 (Firewall Rule)
    resource "google_compute_firewall" "allow_internal" {
      name    = "${var.project_name}-allow-internal"
      network = google_compute_network.main.name
      project = var.project_id
    
      allow {
        protocol = "tcp"
        ports    = ["0-65535"]
      }
    
      allow {
        protocol = "udp"
        ports    = ["0-65535"]
      }
    
      allow {
        protocol = "icmp"
      }
    
      source_ranges = [var.subnet_cidr]
    }
    

    5단계: Azure VNet 모듈

    # modules/azure/vnet/main.tf
    
    resource "azurerm_resource_group" "main" {
      name     = "${var.project_name}-${var.environment}-rg"
      location = var.location
    
      tags = {
        ManagedBy   = "Terraform"
        Environment = var.environment
      }
    }
    
    resource "azurerm_virtual_network" "main" {
      name                = "${var.project_name}-vnet"
      address_space       = [var.vnet_cidr]
      location            = azurerm_resource_group.main.location
      resource_group_name = azurerm_resource_group.main.name
    }
    
    resource "azurerm_subnet" "main" {
      name                 = "${var.project_name}-subnet"
      resource_group_name  = azurerm_resource_group.main.name
      virtual_network_name = azurerm_virtual_network.main.name
      address_prefixes     = [var.subnet_cidr]
    }
    
    # NSG (Network Security Group, 네트워크 보안 그룹)
    resource "azurerm_network_security_group" "main" {
      name                = "${var.project_name}-nsg"
      location            = azurerm_resource_group.main.location
      resource_group_name = azurerm_resource_group.main.name
    }
    

    6단계: 환경별 메인 파일에서 모듈 호출

    # environments/dev/main.tf
    
    # AWS 모듈 호출
    module "aws_vpc" {
      source = "../../modules/aws/vpc"
    
      project_name         = var.project_name
      environment          = var.environment
      vpc_cidr             = "10.0.0.0/16"
      public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
      availability_zones   = ["ap-northeast-2a", "ap-northeast-2c"]
    }
    
    # GCP 모듈 호출
    module "gcp_vpc" {
      source = "../../modules/gcp/vpc"
    
      project_name = var.project_name
      environment  = var.environment
      project_id   = var.gcp_project_id
      region       = var.gcp_region
      subnet_cidr  = "10.1.0.0/16"
    }
    
    # Azure 모듈 호출
    module "azure_vnet" {
      source = "../../modules/azure/vnet"
    
      project_name = var.project_name
      environment  = var.environment
      location     = var.azure_location
      vnet_cidr    = "10.2.0.0/16"
      subnet_cidr  = "10.2.1.0/24"
    }
    

    ⚠️ 삽질 포인트 — 이것만 조심하세요

    제가 멀티 클라우드 Terraform 구축하면서 진짜 고생했던 부분들을 공유드릴게요. 여러분은 같은 실수 안 하셨으면 해서요.

    문제 1: 인증 정보 관리

    각 클라우드마다 인증 방식이 달라서 처음엔 환경변수를 어디다 어떻게 설정해야 하는지 헷갈렸습니다. 절대 코드에 크리덴셜(Credential, 인증 정보)을 하드코딩하지 마세요!

    # AWS 인증 — AWS CLI 프로파일 사용 권장
    export AWS_PROFILE=my-profile
    # 또는 환경변수로
    export AWS_ACCESS_KEY_ID="your-access-key"
    export AWS_SECRET_ACCESS_KEY="your-secret-key"
    
    # GCP 인증 — 서비스 계정 키 파일 사용
    export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
    # 또는 gcloud CLI 인증
    gcloud auth application-default login
    
    # Azure 인증 — Service Principal 사용
    export ARM_CLIENT_ID="your-client-id"
    export ARM_CLIENT_SECRET="your-client-secret"
    export ARM_TENANT_ID="your-tenant-id"
    export ARM_SUBSCRIPTION_ID="your-subscription-id"
    

    💡 팁: CI/CD 파이프라인에서는 각 클라우드의 OIDC(OpenID Connect) 방식 인증을 사용하면 시크릿 관리가 훨씬 깔끔해집니다. GitHub Actions랑 연동하면 특히 좋아요.

    문제 2: 상태 파일(State File) 충돌

    팀원이 동시에 terraform apply 돌리면 상태 파일이 꼬입니다. 이게 진짜 무서운 상황이에요. DynamoDB 테이블로 상태 잠금(State Locking)을 반드시 설정하세요.

    # 상태 잠금용 DynamoDB 테이블 생성
    resource "aws_dynamodb_table" "terraform_state_lock" {
      name           = "terraform-state-lock"
      billing_mode   = "PAY_PER_REQUEST"
      hash_key       = "LockID"
    
      attribute {
        name = "LockID"
        type = "S"
      }
    
      tags = {
        Name = "Terraform State Lock Table"
      }
    }
    

    문제 3: 프로바이더 버전 충돌

    이거 진짜 골치 아팠는데요. required_providers에 버전 범위를 명확히 지정하지 않으면 팀원마다 다른 버전이 설치돼서 동작이 달라지는 일이 생겨요. 반드시 .terraform.lock.hcl 파일을 Git에 커밋하세요. 이게 npm의 package-lock.json 같은 역할을 합니다.

    # 프로바이더 초기화 및 잠금 파일 생성
    terraform init
    
    # 잠금 파일 확인
    cat .terraform.lock.hcl
    
    # 잠금 파일을 Git에 커밋
    git add .terraform.lock.hcl
    git commit -m "chore: update terraform provider lock file"
    

    배포 및 결과 검증

    드디어 실제 배포 단계입니다! Terraform의 기본 워크플로우는 init → plan → apply 순서로 진행됩니다.

    # 1. 초기화 (프로바이더 다운로드)
    terraform init
    
    # 2. 플랜 확인 — 실제로 뭐가 만들어질지 미리 보기
    terraform plan -var-file="terraform.tfvars" -out=tfplan
    
    # 3. 플랜 파일 내용 사람이 읽을 수 있게 출력
    terraform show -json tfplan | jq '.'
    
    # 4. 실제 적용!
    terraform apply tfplan
    
    # 5. 결과 확인
    terraform output
    
    # 6. 상태 파일에서 특정 리소스 확인
    terraform state list
    terraform state show module.aws_vpc.aws_vpc.main
    

    🎉 apply가 성공하면 이런 출력이 나옵니다:

    Apply complete! Resources: 15 added, 0 changed, 0 destroyed.
    
    Outputs:
    
    aws_vpc_id = "vpc-0a1b2c3d4e5f67890"
    aws_public_subnet_ids = [
      "subnet-0a1b2c3d4e5f67891",
      "subnet-0a1b2c3d4e5f67892",
    ]
    gcp_network_id = "projects/my-project/global/networks/myproject-dev-vpc"
    azure_vnet_id = "/subscriptions/.../resourceGroups/myproject-dev-rg/providers/Microsoft.Network/virtualNetworks/myproject-vnet"
    
    Terraform apply 성공 결과 화면 — 멀티 클라우드 리소스 동시 프로비저닝 완료

    ▲ terraform apply 성공 시 세 클라우드에 동시 프로비저닝된 결과 화면

    Terraform 멀티 클라우드 자동화 — 한 단계 더 나아가기

    여기까지 기본 구조를 잡았다면, 이제 실제 팀 환경에서 쓸 수 있는 수준으로 올려야죠. 제가 실제로 도입해서 효과 본 것들을 공유드릴게요.

    Terragrunt로 반복 코드 줄이기

    Terragrunt는 Terraform의 래퍼(Wrapper) 도구인데요. 환경별로 반복되는 backend 설정이나 공통 변수를 DRY(Don’t Repeat Yourself, 반복하지 않기) 원칙에 맞게 관리할 수 있게 해줍니다. 규모가 커지면 진짜 필요해져요.

    CI/CD 파이프라인 연동

    GitHub Actions나 GitLab CI와 연동해서 PR(Pull Request) 올릴 때 자동으로 terraform plan 결과를 코멘트로 달아주는 워크플로우를 구성하면, 코드 리뷰 단계에서 인프라 변경 사항을 팀 전체가 확인할 수 있어요. 이거 도입하고 나서 팀 내 사고가 확 줄었습니다.

    # .github/workflows/terraform.yml
    name: Terraform CI/CD
    
    on:
      pull_request:
        branches: [main]
      push:
        branches: [main]
    
    jobs:
      terraform:
        name: Terraform Plan & Apply
        runs-on: ubuntu-latest
        
        permissions:
          id-token: write   # OIDC 인증용
          contents: read
          pull-requests: write
    
        steps:
          - name: Checkout
            uses: actions/checkout@v4
    
          - name: Setup Terraform
            uses: hashicorp/setup-terraform@v3
            with:
              terraform_version: "1.6.0"
    
          - name: Configure AWS Credentials (OIDC)
            uses: aws-actions/configure-aws-credentials@v4
            with:
              role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
              aws-region: ap-northeast-2
    
          - name: Terraform Init
            run: terraform init
            working-directory: environments/dev
    
          - name: Terraform Plan
            id: plan
            run: terraform plan -no-color
            working-directory: environments/dev
    
          - name: Comment PR with Plan
            uses: actions/github-script@v7
            if: github.event_name == 'pull_request'
            with:
              script: |
                const output = `#### Terraform Plan 결과 🗺️
                \`\`\`\n${{ steps.plan.outputs.stdout }}\n\`\`\`
                `;
                github.rest.issues.createComment({
                  issue_number: context.issue.number,
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  body: output
                })
    
          - name: Terraform Apply
            if: github.ref == 'refs/heads/main'
            run: terraform apply -auto-approve
            working-directory: environments/dev
    
    Terraform 멀티 클라우드 IaC 자동화 베스트 프랙티스 — 코드에서 배포까지 워크플로우 요약

    ▲ Terraform 멀티 클라우드 IaC 자동화 베스트 프랙티스 요약 — 코드부터 배포까지의 전체 워크플로우

    자주 묻는 질문 (FAQ)

    Q. Terraform Cloud를 써야 하나요, 아니면 자체 백엔드로 충분한가요?

    소규모 팀이라면 S3 + DynamoDB 조합의 자체 백엔드로 충분합니다. 팀이 커지거나 RBAC(역할 기반 접근 제어), 정책 관리가 필요해지면 HCP Terraform(구 Terraform Cloud) 유료 플랜을 고려해볼 만해요.

    Q. 각 클라우드 인증 정보를 어떻게 안전하게 관리하나요?

    CI/CD 환경에서는 각 클라우드의 OIDC 연동을 추천드립니다. 로컬 개발 환경에서는 각 클라우드 CLI 도구의 프로파일 기능을 활용하고, 절대 코드나 tfvars 파일에 시크릿을 하드코딩하지 마세요.

    Q. 멀티 클라우드에서 네트워크를 연결하려면 어떻게 하나요?

    VPN Gateway나 클라우드 간 피어링 서비스를 사용해야 하는데, 이 부분은 별도 글에서 자세히 다룰 예정이에요. 각 클라우드의 VPN 리소스도 Terraform으로 코드화할 수 있습니다.

    마무리 — Terraform 멀티 클라우드, 이제 시작해보세요

    처음 멀티 클라우드 Terraform 구조를 잡을 때는 진짜 막막했는데, 이제 돌아보면 이게 없던 시절로 돌아가기 싫을 만큼 편해졌습니다. 코드로 모든 인프라가 관리되니까 감사 추적(Audit Trail)도 되고, 실수로 뭔가 지워도 코드로 복구할 수 있고, 팀 온보딩도 훨씬 수월해졌어요.

    오늘 다룬 내용을 정리하면:

    • ✅ 프로젝트 구조: 클라우드별, 환경별 디렉토리 분리
    • ✅ 프로바이더 설정: AWS, GCP, Azure를 단일 코드베이스에서 선언
    • ✅ 모듈화: 재사용 가능한 모듈로 반복 코드 제거
    • ✅ 원격 백엔드: 상태 파일 중앙화 및 잠금 설정
    • ✅ CI/CD 연동: GitHub Actions로 자동화 파이프라인 구성

    다음 글에서는 Terraform 모듈 레지스트리 활용법과 실제 프로덕션 환경에서 쓰는 보안 설정들을 다뤄볼 예정이에요. 이전 글에서 Kubernetes 클러스터 구성을 다뤘으니 함께 참고하시면 더 좋을 것 같습니다.

    궁금한 점이나 다른 삽질 경험 있으시면 댓글로 공유해주세요. 같이 고민해봐요! 🎉

  • [Cloud] Terraform 멀티 클라우드 관리 가이드 – AWS/GCP/Azure 한 번에 관리하기

    [Cloud] Terraform 멀티 클라우드 관리 가이드 – AWS/GCP/Azure 한 번에 관리하기

    멀티 클라우드, 이제는 선택이 아니라 현실입니다

    혹시 이런 상황 겪어보신 적 있으신가요? 개발팀에서는 AWS를 쓰고, 데이터 분석팀에서는 GCP BigQuery를 쓰고, 어느 날 갑자기 Azure AD 연동도 필요하다고 하고… 처음엔 각 팀이 알아서 하면 되겠지 싶었는데, 인프라 엔지니어 입장에서 이걸 다 관리하려니 머리가 터질 것 같더라고요.

    저도 딱 그 상황이었습니다. 몇 년 전에 회사에서 AWS만 쓰다가 GCP 프로젝트가 하나 붙고, 또 Azure가 붙고… 각 클라우드마다 콘솔 따로 열고, CLI 따로 설치하고, 권한 관리도 따로 하다 보니 진짜 정신없었거든요. 그때 Terraform으로 멀티 클라우드 환경을 구성하면서 삶이 좀 편해졌습니다 ㅎㅎ

    이 글에서는 제가 직접 구성해서 운영 중인 Terraform으로 AWS, GCP, Azure를 동시에 관리하는 멀티 클라우드 IaC(Infrastructure as Code, 코드로 인프라 관리) 환경을 단계별로 풀어드릴게요. 처음 접하시는 분도 따라오실 수 있게 최대한 친절하게 써볼게요.

    Terraform multi-cloud architecture diagram showing AWS, GCP, and Azure connected through a central Terraform control plane with arrows indicating resource provisioning flow

    ▲ Terraform 단일 코드베이스에서 AWS, GCP, Azure 세 클라우드를 동시에 관리하는 전체 아키텍처 개요도

    Terraform 멀티 클라우드란? 한 번에 여러 클라우드를 관리하는 방법

    Terraform은 HashiCorp에서 만든 오픈소스 IaC 도구입니다. 쉽게 말해, 클라우드 인프라를 코드로 정의하고 실행하면 실제 리소스가 만들어지는 방식이에요. 여러 클라우드를 한 번에 관리할 수 있다는 게 가장 큰 장점입니다.

    여기서 핵심은 Provider(프로바이더)라는 개념입니다. Terraform은 각 클라우드사에서 제공하는 Provider 플러그인을 통해 해당 클라우드와 통신하거든요. AWS Provider, Google Provider, AzureRM Provider를 동시에 선언하면 하나의 Terraform 코드로 여러 클라우드를 한 번에 관리할 수 있어요.

    구분 AWS GCP Azure
    Terraform Provider hashicorp/aws hashicorp/google hashicorp/azurerm
    인증 방식 Access Key / IAM Role Service Account JSON / ADC Service Principal / Managed Identity
    상태 저장소 S3 + DynamoDB GCS Bucket Azure Blob Storage
    주요 IaC 용어 Region, VPC, EC2 Project, VPC, Compute Subscription, VNet, VM

    저도 처음엔 각 클라우드마다 용어가 달라서 헷갈렸는데요, Terraform을 쓰면 이 차이를 Provider가 알아서 추상화해줘서 훨씬 편하더라고요.

    디렉터리 구조 설계 — 이게 제일 중요합니다

    멀티 클라우드 Terraform 프로젝트에서 제가 가장 많이 고민한 부분이 바로 디렉터리 구조였어요. 처음에 그냥 막 만들었다가 나중에 전부 갈아엎은 쓴 경험이 있거든요. 그래서 여러분은 처음부터 제대로 잡으셨으면 해서 제가 실제로 쓰는 구조를 공유합니다.

    multi-cloud-infra/
    ├── providers.tf          # 모든 Provider 선언
    ├── versions.tf           # Terraform 버전 및 Provider 버전 고정
    ├── variables.tf          # 공통 변수
    ├── outputs.tf            # 공통 출력값
    │
    ├── modules/              # 재사용 가능한 모듈
    │   ├── aws/
    │   │   ├── vpc/
    │   │   ├── ec2/
    │   │   └── s3/
    │   ├── gcp/
    │   │   ├── vpc/
    │   │   ├── gce/
    │   │   └── gcs/
    │   └── azure/
    │       ├── vnet/
    │       ├── vm/
    │       └── storage/
    │
    └── environments/         # 환경별 실제 구성
        ├── dev/
        │   ├── main.tf
        │   ├── variables.tf
        │   └── terraform.tfvars
        ├── staging/
        └── prod/
    

    💡 핵심 포인트: 환경(dev/staging/prod)을 디렉터리로 분리하고, 각 클라우드별 모듈을 따로 관리하면 나중에 유지보수할 때 훨씬 편합니다. 처음엔 귀찮아 보여도, 실제로 운영하다 보면 정말 감사하게 되더라고요.

    실전 구현 1단계 — Provider 및 버전 설정

    자, 이제 실제 코드로 들어가 볼게요. 먼저 versions.tf에서 Terraform 버전과 각 Provider 버전을 고정해줍니다. 버전 고정 안 하면 나중에 Provider 업데이트되면서 갑자기 코드가 안 돌아가는 참사가 생기거든요. 저 이거로 한 번 크게 당했습니다 ㅎㅎ

    # versions.tf
    terraform {
      required_version = ">= 1.5.0"
    
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.0"
        }
        google = {
          source  = "hashicorp/google"
          version = "~> 5.0"
        }
        azurerm = {
          source  = "hashicorp/azurerm"
          version = "~> 3.0"
        }
      }
    
      # 원격 State 백엔드 — AWS S3 예시
      backend "s3" {
        bucket         = "my-terraform-state-bucket"
        key            = "multi-cloud/terraform.tfstate"
        region         = "ap-northeast-2"
        dynamodb_table = "terraform-state-lock"
        encrypt        = true
      }
    }
    

    다음은 providers.tf입니다. 각 클라우드 인증 정보를 여기서 설정해요.

    # providers.tf
    
    # AWS Provider
    provider "aws" {
      region = var.aws_region
      # 로컬에선 ~/.aws/credentials 사용
      # CI/CD에선 환경변수 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 사용
    }
    
    # GCP Provider
    provider "google" {
      project = var.gcp_project_id
      region  = var.gcp_region
      # GOOGLE_APPLICATION_CREDENTIALS 환경변수로 Service Account JSON 경로 지정
    }
    
    # Azure Provider
    provider "azurerm" {
      features {}
      subscription_id = var.azure_subscription_id
      # ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID 환경변수 사용
    }
    

    ⚠️ 경고: 절대로 Access Key나 Secret Key를 코드에 하드코딩하면 안 됩니다! 환경변수나 Vault 같은 시크릿 관리 도구를 꼭 사용하세요. GitHub에 실수로 올렸다가 과금 폭탄 맞은 사례 정말 많거든요.

    실전 구현 2단계 — 각 클라우드 리소스 모듈 만들기

    이제 각 클라우드별로 기본 네트워크 리소스를 만들어볼게요. 실제로 제가 쓰는 패턴입니다.

    Split-screen diagram showing Terraform code on the left and resulting cloud resources on AWS, GCP, and Azure on the right, with color-coded resource blocks

    ▲ 하나의 Terraform 코드가 각 클라우드 플랫폼의 실제 리소스로 프로비저닝되는 과정을 보여주는 구성 다이어그램

    AWS VPC 모듈로 가상 네트워크 구성하기

    # modules/aws/vpc/main.tf
    resource "aws_vpc" "main" {
      cidr_block           = var.cidr_block
      enable_dns_hostnames = true
      enable_dns_support   = true
    
      tags = merge(var.common_tags, {
        Name = "${var.env}-vpc"
        Cloud = "AWS"
      })
    }
    
    resource "aws_subnet" "public" {
      count             = length(var.public_subnet_cidrs)
      vpc_id            = aws_vpc.main.id
      cidr_block        = var.public_subnet_cidrs[count.index]
      availability_zone = var.availability_zones[count.index]
    
      tags = merge(var.common_tags, {
        Name = "${var.env}-public-subnet-${count.index + 1}"
      })
    }
    
    resource "aws_internet_gateway" "main" {
      vpc_id = aws_vpc.main.id
    
      tags = var.common_tags
    }
    

    GCP VPC 모듈로 가상 네트워크 구성하기

    # modules/gcp/vpc/main.tf
    resource "google_compute_network" "main" {
      name                    = "${var.env}-vpc"
      auto_create_subnetworks = false
      project                 = var.project_id
    }
    
    resource "google_compute_subnetwork" "main" {
      name          = "${var.env}-subnet"
      ip_cidr_range = var.cidr_block
      region        = var.region
      network       = google_compute_network.main.id
      project       = var.project_id
    
      private_ip_google_access = true
    }
    

    Azure VNet 모듈로 가상 네트워크 구성하기

    # modules/azure/vnet/main.tf
    resource "azurerm_resource_group" "main" {
      name     = "${var.env}-rg"
      location = var.location
    
      tags = var.common_tags
    }
    
    resource "azurerm_virtual_network" "main" {
      name                = "${var.env}-vnet"
      address_space       = [var.cidr_block]
      location            = azurerm_resource_group.main.location
      resource_group_name = azurerm_resource_group.main.name
    
      tags = var.common_tags
    }
    
    resource "azurerm_subnet" "main" {
      name                 = "${var.env}-subnet"
      resource_group_name  = azurerm_resource_group.main.name
      virtual_network_name = azurerm_virtual_network.main.name
      address_prefixes     = [var.subnet_cidr]
    }
    

    환경별 실제 구성 파일로 모듈 조합하기

    # environments/dev/main.tf
    
    # AWS 네트워크
    module "aws_network" {
      source = "../../modules/aws/vpc"
    
      env                  = "dev"
      cidr_block           = "10.0.0.0/16"
      public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
      availability_zones   = ["ap-northeast-2a", "ap-northeast-2c"]
      common_tags = {
        Environment = "dev"
        ManagedBy   = "Terraform"
      }
    }
    
    # GCP 네트워크
    module "gcp_network" {
      source = "../../modules/gcp/vpc"
    
      env        = "dev"
      project_id = var.gcp_project_id
      region     = "asia-northeast1"
      cidr_block = "10.1.0.0/20"
    }
    
    # Azure 네트워크
    module "azure_network" {
      source = "../../modules/azure/vnet"
    
      env        = "dev"
      location   = "koreacentral"
      cidr_block = "10.2.0.0/16"
      subnet_cidr = "10.2.1.0/24"
      common_tags = {
        Environment = "dev"
        ManagedBy   = "Terraform"
      }
    }
    

    실전 구현 3단계 — 멀티 클라우드 배포 실행하기

    이제 준비가 끝났습니다. 실제로 배포를 해볼게요. 명령어는 어느 클라우드나 동일합니다. 이게 Terraform의 진짜 강점이에요.

    # 1. Terraform 초기화 (Provider 다운로드)
    terraform init
    
    # 2. 실행 계획 확인 (dry-run)
    terraform plan -out=tfplan
    
    # 3. 실제 배포 (AWS, GCP, Azure 동시에 리소스 생성)
    terraform apply tfplan
    
    # 4. 배포된 리소스 확인
    terraform show
    
    # 5. 특정 출력값 확인
    terraform output
    

    명령어 하나로 AWS VPC, GCP VPC, Azure VNet이 동시에 만들어집니다. 정말 신기하더라고요. 각 클라우드 콘솔에 들어가서 확인해보면 실제로 리소스가 생성되어 있을 거예요.

    멀티 클라우드 운영할 때 꼭 알아야 할 팁

    1. State 파일 관리는 신중하게

    Terraform State 파일은 현재 인프라 상태를 기록한 중요한 파일입니다. 절대로 로컬에만 저장하면 안 됩니다. S3, GCS, Azure Blob Storage 같은 원격 백엔드에 저장하고, DynamoDB나 Blob Storage 잠금 기능으로 동시 접근을 방지하세요. 여러 팀이 동시에 배포하면서 State 파일이 꼬이는 참사를 막아야 거든요.

    2. 환경 분리는 필수

    dev, staging, prod를 완전히 분리된 디렉터리로 관리하세요. 실수로 prod 환경을 destroy하는 일을 막기 위해 terraform destroy는 스크립트로 자동화하지 말고 수동으로만 실행하는 게 좋습니다.

    3. 변수 파일은 .gitignore에 추가

    terraform.tfvars 파일에는 민감한 정보(API 키, 구독 ID 등)가 들어갑니다. 절대로 Git에 커밋하면 안 됩니다. .gitignore에 추가하고, CI/CD 파이프라인에서 환경변수로 주입하세요.

    4. 비용 모니터링을 자동화하세요

    멀티 클라우드는 각 클라우드의 비용이 따로 집계되기 때문에 전체 비용을 파악하기 어렵습니다. Terraform Cloud나 Infracost 같은 도구로 배포 전에 비용을 예측하는 게 좋습니다.

    자주 하는 실수와 해결 방법

    Q: 특정 클라우드 리소스만 삭제하고 싶은데 어떻게 하나요?

    A: terraform destroy -target=module.aws_network 명령으로 특정 모듈만 삭제할 수 있습니다. 다만 이 방법은 State 파일 불일치를 초래할 수 있으니 주의하세요.

    Q: Provider 버전을 업그레이드하면 기존 리소스가 삭제되나요?

    A: 보통은 아닙니다. 다만 terraform plan으로 변경사항을 꼭 확인한 후 업그레이드하세요. 간혹 주요 버전 업그레이드 시 리소스 구조가 바뀔 수 있거든요.

    Q: 기존 클라우드 리소스를 Terraform으로 관리하려면?

    A: terraform import 명령을 사용합니다. 예: terraform import aws_vpc.main vpc-12345678. 기존 리소스를 State 파일에 등록한 후 코드를 작성하면 됩니다.

    마치며: Terraform 멀티 클라우드의 미래

    저는 지난 3년간 Terraform으로 AWS, GCP, Azure를 관리해오면서 정말 많은 이점을 느꼈습니다. 각 클라우드의 콘솔을 오갈 필요 없이 코드 한 곳에서 모든 걸 관리할 수 있다는 게 최고의 장점이에요. 팀 규모가 커져도 일관된 방식으로 인프라를 관리할 수 있고, 재해 복구나 환경 복제도 정말 쉬워집니다.

    처음에는 디렉터리 구조를 잡는 데 시간이 걸릴 수 있지만, 한 번 제대로 설계해놓으면 정말 오랫동안 써먹을 수 있는 자산이 됩니다. 여러분도 이 글을 참고해서 Terraform 멀티 클라우드 환경을 구축해보세요. 분명 개발과 운영이 훨씬 편해질 거예요!