13년차의 서버실

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

[태그:] GCP

  • [Cloud] Cloud Run 마이그레이션: 온프레미스 앱 전환 전략

    [Cloud] Cloud Run 마이그레이션: 온프레미스 앱 전환 전략

    [클라우드] Cloud Run 마이그레이션으로 온프레미스 앱 전환하기

    온프레미스에서 오래 돌리던 앱을 클라우드로 옮기는 일, 말은 쉬워 보여도 막상 들어가면 손이 꽤 갑니다. 특히 Cloud Run 마이그레이션은 서버 한 대만 없앤다고 끝나는 작업이 아니거든요. 프로세스가 어떻게 뜨는지, 파일을 어디에 쓰는지, 배치 작업은 어떻게 분리할지, 외부 트래픽은 어떤 방식으로 받을지까지 다시 봐야 합니다. 저도 홈랩과 운영 환경에서 비슷한 전환을 몇 번 해보니, 잘된 사례보다 중간에 막혔던 경험에서 더 많이 배우게 되더라고요. 이번 글에서는 온프레미스 클라우드 전환을 고민하시는 분들을 위해, 실제로 점검하는 순서대로 Cloud Run 도입 전략과 컨테이너 마이그레이션 포인트를 정리해보겠습니다.

    기존 VM 기반 운영에 익숙하셨다면 Cloud Run의 요청 기반 실행 모델이 처음엔 좀 낯설 수 있습니다. 저도 처음엔 감이 잘 안 왔는데, 구조를 한 번 이해하고 나니 운영 포인트가 오히려 단순해졌습니다. 핵심은 애플리케이션을 억지로 클라우드에 끼워 맞추는 게 아니라, Cloud Run이 안정적으로 실행할 수 있는 형태로 먼저 정리한 뒤 올리는 것입니다.

    Cloud Run 마이그레이션을 위한 온프레미스 클라우드 전환 아키텍처 다이어그램

    온프레미스 애플리케이션이 컨테이너 이미지로 패키징되고 Cloud Run, 데이터베이스, 로드밸런서로 연결되는 전체 흐름을 보여주는 개요 이미지입니다.

    1. 왜 Cloud Run 마이그레이션이 현실적인 선택인지

    쉽게 말해 Cloud Run은 컨테이너만 잘 만들면 실행 환경은 서비스가 관리해주는 플랫폼입니다. 서버 패치, 런타임 노드 관리, 자동 확장 같은 운영 부담을 꽤 덜 수 있죠. 그래서 기존 온프레미스 앱 중에서도 아래 조건에 맞는 서비스는 전환 효과가 큰 편입니다.

    • HTTP 또는 gRPC 기반으로 요청을 처리하는 웹 애플리케이션
    • 상태(state)를 메모리나 로컬 디스크가 아닌 외부 저장소로 분리할 수 있는 서비스
    • 트래픽 변동이 있어 자동 확장의 이점을 볼 수 있는 서비스
    • 배포 속도와 롤백 단순화가 중요한 팀 서비스

    반대로 항상 실행 중이어야 하는 장기 백그라운드 프로세스나, 로컬 파일시스템 의존도가 높은 구조라면 그대로 옮기기 어렵습니다. 이런 경우는 웹 서비스는 Cloud Run 서비스로, 배치성 작업은 Cloud Run Jobs 같은 다른 실행 방식으로 나눠 보는 편이 낫습니다. 결국 마이그레이션에서 제일 중요한 건 클라우드 기능을 많이 아는 게 아니라, 현재 앱이 어떤 전제 위에서 돌아가는지 정확히 파악하는 것입니다.

    2. Cloud Run 마이그레이션 전에 알아둘 개념 차이

    온프레미스에서는 보통 서버를 만들고, 프로세스를 띄워두고, 로그 위치를 정하고, 방화벽을 열고, 필요하면 리버스 프록시를 붙입니다. 그런데 Cloud Run은 접근 방식이 꽤 다릅니다.

    항목 온프레미스 앱 운영 Cloud Run 운영
    실행 단위 서버/VM 위 프로세스 컨테이너
    확장 방식 수동 증설 또는 별도 오토스케일링 요청량 기반 자동 확장
    상태 저장 로컬 디스크 활용 빈번 영속 데이터는 외부 저장소 권장
    배포 서버 접속 후 배포 또는 파이프라인 이미지 배포 중심
    네트워크 공개 방화벽, 프록시, 로드밸런서 직접 구성 Ingress 정책과 IAM으로 제어

    핵심은 두 가지입니다. 첫째, 애플리케이션은 지정된 포트에서 요청을 받아야 합니다. 둘째, 인스턴스의 영속 상태에 의존하면 안 됩니다. 로그는 stdout/stderr, 설정은 환경 변수, 민감한 값은 Secret Manager 같은 외부 비밀 저장소, 파일은 Cloud Storage 같은 외부 스토리지를 쓰는 방식이 잘 맞습니다.

    여기서 하나 짚고 갈 부분이 있습니다. Cloud Run 컨테이너 파일시스템은 쓰기가 가능하긴 하지만 영구 디스크가 아니라 인메모리 기반 임시 저장소라서, 인스턴스가 내려가면 데이터가 유지되지 않습니다. 임시 파일 정도는 괜찮지만 업로드 원본이나 세션 파일을 계속 거기에 두는 구조는 금방 문제를 만들더라고요.

    3. 마이그레이션 전에 꼭 하는 사전 점검

    실전 들어가기 전에 저는 항상 아래 체크리스트부터 봅니다. 이 단계 건너뛰면 뒤에서 더 오래 걸립니다.

    1. 프로세스 시작 방식 확인
      앱이 포그라운드로 실행되는지 확인합니다. 백그라운드로 포크되면 컨테이너 환경에서 예상과 다르게 동작할 수 있습니다.
    2. 포트 바인딩 방식 확인
      앱이 환경 변수로 주입된 포트를 사용할 수 있는지 봅니다. Cloud Run은 컨테이너에 PORT 환경 변수를 주입합니다.
    3. 세션/업로드 저장 위치 확인
      세션 파일, 업로드 파일, 캐시가 로컬 디스크에 쌓이면 장애 포인트가 됩니다.
    4. 환경 변수 정리
      DB 접속 정보, API 키, 운영 모드 값을 코드나 파일에 박아뒀다면 외부 설정으로 분리합니다.
    5. 헬스체크와 기동 시간 점검
      Cloud Run에서는 시작 프로브와 라이브니스 프로브를 설정할 수 있으니, 앱 기동 시간이 긴 편이면 같이 검토합니다.
    6. 장기 실행 작업 분리
      웹 요청 처리와 오래 걸리는 배치를 분리하지 않으면 타임아웃 때문에 고생할 수 있습니다. Cloud Run 서비스의 요청 타임아웃은 기본 5분이고 최대 60분까지 설정할 수 있습니다.

    혹시 지금 운영 중인 앱이 이런 상태이신가요? “서버 켜지면 systemd로 자동 실행되고, 로그는 /var/log 아래에 쌓이고, 업로드 파일은 /data에 저장한다.” 정말 흔한 구조입니다. 저도 많이 봤고 직접 운영도 했거든요. 그런데 이 구조를 그대로 온프레미스 클라우드 전환에 들이밀면 문제가 생기기 쉽습니다. 그래서 먼저 상태 의존성을 줄이는 게 중요합니다.

    4. 실전 구현 1: 앱을 컨테이너로 감싸기

    이제 실제 작업으로 들어가 보겠습니다. 예시는 Python Flask 기준이지만 Node.js, Go, Java도 원리는 같습니다. 중요한 건 프레임워크가 아니라 컨테이너 안에서 단일 프로세스로 뜨고, 지정 포트를 리스닝하는지입니다.

    4-1. 애플리케이션 코드에서 포트 환경 변수 받기

    import os
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route("/")
    def index():
        return {"status": "ok", "message": "hello from cloud run"}
    
    if __name__ == "__main__":
        port = int(os.environ.get("PORT", "8080"))
        app.run(host="0.0.0.0", port=port)

    여기서 중요한 포인트는 127.0.0.1이 아니라 0.0.0.0으로 바인딩해야 한다는 점입니다. Cloud Run은 컨테이너가 0.0.0.0에서 요청을 받기를 기대하거든요. 이거 한 번 놓치면 배포는 성공했는데 접속이 안 되는 전형적인 상황이 나옵니다. 저도 예전에 이걸로 로그만 한참 뒤졌습니다.

    4-2. Dockerfile 작성

    FROM python:3.11-slim
    
    WORKDIR /app
    
    COPY requirements.txt ./
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY . .
    
    ENV PORT=8080
    CMD ["python", "app.py"]

    프로덕션에서는 Gunicorn 같은 WSGI 서버를 두는 구성이 더 일반적입니다. 다만 여기서는 흐름을 설명하려고 단순 예시로 적었습니다. 실제 운영에서는 애플리케이션 특성에 맞는 프로세스 모델을 고르시면 됩니다.

    4-3. 로컬에서 컨테이너 실행 확인

    docker build -t my-onprem-app:local .
    docker run --rm -p 8080:8080 my-onprem-app:local
    curl http://localhost:8080/

    로컬에서 먼저 확인하는 이유는 단순합니다. Cloud Run 문제인지, 내 컨테이너 문제인지 먼저 구분해야 하거든요. 이 단계에서 응답이 안 오면 클라우드로 올려도 똑같이 막힙니다.

    5. Cloud Run 마이그레이션 배포 절차

    컨테이너가 정상 동작하면 이제 이미지를 레지스트리에 올리고 서비스를 배포합니다. 보통은 CI/CD에서 처리하지만, 처음 구조 검증할 때는 CLI로 한 번 직접 해보는 게 좋습니다. 해보면 서비스 계정, 리전, 이미지 태그 전략이 한 번에 정리되더라고요.

    PROJECT_ID="your-gcp-project"
    REGION="asia-northeast3"
    REPO="app-images"
    SERVICE="my-onprem-app"
    IMAGE="$REGION-docker.pkg.dev/$PROJECT_ID/$REPO/$SERVICE:manual-001"
    
    gcloud auth login
    gcloud config set project "$PROJECT_ID"
    gcloud artifacts repositories create "$REPO" \
      --repository-format=docker \
      --location="$REGION"
    
    gcloud auth configure-docker "$REGION-docker.pkg.dev"
    docker build -t "$IMAGE" .
    docker push "$IMAGE"
    
    gcloud run deploy "$SERVICE" \
      --image "$IMAGE" \
      --region "$REGION" \
      --allow-unauthenticated

    사내 서비스처럼 외부 공개가 필요 없다면 --allow-unauthenticated 대신 인증된 호출만 허용하는 구성을 검토하시면 됩니다. 이때는 Ingress 정책과 IAM을 같이 봐야 합니다. 인증을 막는 설정과 네트워크 진입 경로 설정은 역할이 다르니 묶어서 보는 게 좋습니다.

    Cloud Run 도입 과정의 컨테이너 마이그레이션 배포 흐름 이미지

    컨테이너 이미지가 Artifact Registry에 저장되고 Cloud Run 서비스로 배포되며 환경 변수와 시크릿이 주입되는 흐름을 설명하는 구성 이미지입니다.

    5-1. 환경 변수와 시크릿 분리

    운영하다 보면 제일 위험한 게 자격 증명을 이미지 안에 넣어버리는 겁니다. 이건 정말 피하셔야 합니다.

    gcloud run deploy "$SERVICE" \
      --image "$IMAGE" \
      --region "$REGION" \
      --set-env-vars APP_ENV=prod,LOG_LEVEL=info \
      --set-secrets DB_PASSWORD=db-password:latest

    설정값은 환경 변수로, 비밀번호나 토큰은 시크릿으로 나누는 습관을 들이면 운영이 훨씬 편해집니다. 나중에 키 교체할 때 체감이 확 옵니다. 이거 진짜 편하더라고요.

    5-2. 데이터베이스 연결은 어떻게 할까

    온프레미스 앱이 데이터베이스를 쓰고 있다면, 가장 먼저 볼 건 연결 방식입니다. 앱과 DB를 같은 서버에 붙여 놓았던 구조라면 분리가 필요합니다. 보통은 다음 두 방향 중 하나를 검토합니다.

    • 관리형 데이터베이스로 이전해 네트워크와 자격 증명을 정리한다
    • 기존 데이터베이스를 유지하되, 안전한 네트워크 경로와 연결 풀을 재설계한다

    중요한 건 애플리케이션 인스턴스 수가 늘어나도 DB 연결 수가 급격히 폭증하지 않게 하는 것입니다. Cloud Run은 빠르게 확장될 수 있어서, 연결 풀 값을 아무 생각 없이 잡으면 DB가 먼저 버거워질 수 있습니다.

    6. 운영 관점에서 꼭 챙겨야 할 설정

    배포가 됐다고 끝은 아닙니다. 실제로는 여기서부터 운영 품질이 갈립니다.

    6-1. 동시성 이해하기

    Cloud Run은 한 인스턴스가 여러 요청을 동시에 처리할 수 있습니다. CPU 바운드 작업인지, I/O 바운드 작업인지에 따라 적정값이 달라집니다. 기본 동시성은 콘솔 배포 기준 80이고, CLI나 Terraform으로 새 서비스를 만들 때는 vCPU 수에 따라 계산되는 기본값이 적용될 수 있습니다. 그래서 그냥 숫자 하나만 외워두기보다, 실제 지연 시간과 CPU 사용률을 보고 조정하는 편이 더 안전합니다.

    6-2. 타임아웃과 장기 작업 분리

    웹 요청 하나가 오래 걸린다면, 그 작업을 직접 처리하기보다 큐나 배치로 넘기는 게 낫습니다. 예를 들어 대용량 리포트 생성, 비디오 인코딩, 대량 동기화 작업은 웹 요청 경로에서 분리하는 편이 안전합니다. 서비스 타임아웃은 기본 5분이고 최대 60분까지 늘릴 수 있지만, 길게 준다고 구조 문제가 사라지진 않더라고요.

    6-3. 로그는 파일이 아니라 표준 출력으로

    온프레미스에선 로그 파일을 tail로 보던 습관이 강하죠. 그런데 컨테이너 환경에서는 애플리케이션 로그를 stdout/stderr로 내보내는 방식이 관리가 훨씬 쉽습니다. 로그 포맷도 가능하면 JSON 같은 구조화 로그로 맞춰두면 검색이 편합니다.

    6-4. 파일 업로드는 외부 스토리지로

    앱이 업로드 파일을 로컬 경로에 저장하고 있다면 구조를 바꾸는 게 좋습니다. Cloud Run의 로컬 파일시스템은 영속 저장소가 아니라 임시 저장소에 가깝기 때문입니다. 이 부분은 컨테이너 마이그레이션에서 자주 놓치는 항목 중 하나입니다.

    7. 실제로 많이 겪는 문제와 트러블슈팅

    이 섹션은 정말 경험담 위주입니다. 문서만 읽으면 잘 안 보이는데, 실제로 해보면 여기서 많이 막힙니다.

    • 문제 1: 배포는 성공했는데 접속이 안 됨
      원인으로는 포트 바인딩 오류, 0.0.0.0 미사용, 애플리케이션 시작 실패가 많습니다. 로그에서 startup 관련 메시지를 먼저 확인합니다.
    • 문제 2: 특정 요청에서만 500 에러 발생
      로컬 파일 경로에 쓰기 시도하는 경우가 많습니다. 업로드, 임시 파일, 세션 저장 위치를 다시 확인합니다.
    • 문제 3: 예상보다 응답이 느림
      콜드 스타트, 외부 DB 연결 지연, 동시성 설정 부적합을 의심합니다.
    • 문제 4: 갑자기 DB 연결이 부족해짐
      오토스케일링과 연결 풀 값이 충돌할 수 있습니다. 인스턴스 수와 DB 허용 연결 수를 같이 봐야 합니다.
    • 문제 5: 배치 작업이 중간에 끊김
      HTTP 요청 처리 경로에 오래 걸리는 작업을 넣은 경우가 많습니다. 작업 큐나 Cloud Run Jobs 기반으로 재설계하는 편이 낫습니다.

    제가 한 번은 기존 앱이 이미지 업로드 후 썸네일까지 같은 요청 안에서 만들고, 결과 파일도 로컬 디스크에 저장하는 구조를 본 적이 있습니다. 로컬 테스트에선 멀쩡했는데 클라우드에 올리니 간헐적으로 실패하더라고요. 처음엔 네트워크 문제인 줄 알았는데, 결국 저장소 구조가 원인이었습니다. 업로드 원본과 결과물을 외부 스토리지로 빼고 나서야 안정화됐습니다. 그때 진짜 숨이 좀 놓였죠.

    8. 검증 방법: 전환 후 무엇을 확인해야 하나요?

    배포가 끝난 뒤에는 기능만 보지 말고 운영 지표도 같이 봐야 합니다. 최소한 아래 항목은 확인하시는 걸 권장드립니다.

    1. 기능 검증
      핵심 API, 로그인, 업로드, 외부 연동을 실제 시나리오로 테스트합니다.
    2. 로그 검증
      오류 로그가 구조적으로 남는지, 요청 추적이 가능한지 확인합니다.
    3. 성능 검증
      트래픽이 몰릴 때 응답 시간이 급격히 늘지 않는지 봅니다.
    4. 확장 검증
      동시 요청 상황에서 인스턴스 확장이 의도대로 이뤄지는지 확인합니다.
    5. 운영 절차 검증
      재배포, 롤백, 설정 변경, 시크릿 교체가 얼마나 단순해졌는지 봅니다.
    SERVICE_URL="https://your-service-url"
    
    curl -i "$SERVICE_URL/"
    
    for i in $(seq 1 20); do
      curl -s "$SERVICE_URL/" &
    done
    wait

    초기 검증은 이렇게 단순하게 시작해도 충분합니다. 중요한 건 숫자 자체보다, 어떤 조건에서 앱이 흔들리는지 빨리 파악하는 것입니다. 운영은 결국 재현성과 관찰 가능성 싸움이거든요. 관련해서 이전 글의 컨테이너 이미지 최적화 내용이 있다면 여기와 함께 내부 링크로 연결해두는 것도 SEO와 체류 시간 측면에서 꽤 도움이 됩니다.

    Cloud Run 마이그레이션 이후 검증을 위한 운영 대시보드 이미지

    배포 이후 요청 성공률, 응답 시간, 로그 확인 과정을 한눈에 볼 수 있는 운영 검증용 대시보드 이미지입니다.

    9. 결과적으로 무엇이 좋아졌나

    모든 앱이 Cloud Run에 맞는 건 아닙니다. 이건 분명합니다. 하지만 웹 API, 내부 업무 서비스, 이벤트 기반 백엔드처럼 구조만 잘 맞추면 얻는 장점이 꽤 큽니다.

    • 배포 단위가 서버가 아니라 이미지가 되면서 재현성이 좋아집니다
    • 운영 환경 표준화가 쉬워집니다
    • 트래픽 변화 대응이 단순해집니다
    • 서버 패치와 런타임 관리 부담이 줄어듭니다
    • 개발팀과 인프라팀의 경계가 조금 더 명확해집니다
    전환 전 전환 후
    서버 접속 후 수동 점검 이미지 중심 배포와 설정 관리
    로컬 로그 파일 추적 중앙화된 로그 확인
    확장 시 서버 증설 고민 요청량 기반 확장 고려
    앱과 인프라 설정이 강하게 결합 컨테이너 기준으로 역할 분리

    물론 대가도 있습니다. 앱이 상태 비저장에 가깝게 움직이도록 구조를 손봐야 하고, 기존 운영 습관도 일부 바꿔야 합니다. 그런데 이 과정을 한 번 거치고 나면 이후 운영이 꽤 가벼워집니다. 저는 그게 가장 크게 느껴졌습니다.

    온프레미스 클라우드 전환과 Cloud Run 도입 비교 인포그래픽

    온프레미스 운영 방식과 Cloud Run 도입 이후의 배포, 확장, 로그 관리 차이를 요약한 비교 인포그래픽입니다.

    10. 마무리: 성공적인 Cloud Run 도입을 위한 정리

    Cloud Run 마이그레이션의 핵심은 기술 자체보다 애플리케이션 구조를 얼마나 잘 정리하느냐에 달려 있습니다. 서버를 옮기는 작업으로 보면 자꾸 꼬이고, 컨테이너가 요청을 안정적으로 처리하도록 재구성하는 작업으로 보면 길이 보입니다. 저도 처음엔 단순 배포 문제라고 생각했는데, 막상 해보니 설정 파일, 로그, 세션, 업로드, 배치 작업까지 전부 연결돼 있더라고요.

    정리하면 이렇습니다.

    1. 앱이 PORT 환경 변수로 실행되게 바꿉니다.
    2. 상태와 파일 저장을 외부로 분리합니다.
    3. 시크릿과 환경 변수를 이미지에서 분리합니다.
    4. 동시성, 타임아웃, DB 연결 수를 함께 봅니다.
    5. 배포보다 검증 자동화를 먼저 습관화합니다.

    혹시 지금 온프레미스 앱을 들고 계시고 어디서부터 손대야 할지 막막하셨다면, 위 순서대로만 체크해도 꽤 정리가 됩니다. 다음 글에서는 Cloud Run 앞단에 로드밸런서와 도메인, 그리고 내부 전용 서비스 노출 전략까지 이어서 다뤄보셔도 좋겠습니다. 이전 글이 있다면 여기서 자연스럽게 내부 링크를 걸어 주제 클러스터를 만드는 것도 추천드립니다.

    11. 자주 묻는 질문

    Q1. 기존 온프레미스 앱을 수정 없이 그대로 올릴 수 있나요?

    대부분은 어렵습니다. 특히 로컬 파일 저장, 백그라운드 데몬 방식, 고정 포트 전제는 수정이 필요할 가능성이 큽니다.

    Q2. Cloud Run 도입은 작은 팀에도 괜찮나요?

    오히려 작은 팀일수록 운영 단순화 효과를 크게 느끼는 경우가 많습니다. 다만 앱 구조가 맞아야 합니다.

    Q3. 모든 마이크로서비스에 적합한가요?

    아닙니다. 요청 기반 처리와 컨테이너 실행 모델에 잘 맞는 서비스부터 시작하시는 게 안전합니다.

    Q4. 마이그레이션 첫 단계는 무엇이 가장 중요할까요?

    현재 앱의 상태 의존성과 파일 저장 습관을 파악하는 겁니다. 이거 놓치면 뒤에서 계속 발목을 잡습니다.

  • [k8s] GKE WireGuard 보안 취약점 분석: 매니지드 쿠버네티스 보안 강화 전략

    [k8s] GKE WireGuard 보안 취약점 분석: 매니지드 쿠버네티스 보안 강화 전략

    GKE WireGuard 보안 취약점 분석: 매니지드 쿠버네티스 보안 강화 전략

    안녕하세요, 13년차 서버실 지킴이입니다. 요즘 클라우드 환경에서 쿠버네티스(Kubernetes)를 운영하는 분들이 정말 많으시더라고요. 저도 홈랩(Homelab)에서 이것저것 만져보면서 GKE(Google Kubernetes Engine)의 편리함에 푹 빠져 있는데, 이 편리함 뒤에는 우리가 꼭 알아야 할 보안의 그림자가 숨어있다는 걸 아시나요?

    특히 온프레미스(On-premise) 환경이나 다른 클라우드에 있는 시스템과 GKE 클러스터를 안전하게 연결할 때 WireGuard(와이어가드) 같은 VPN(Virtual Private Network)을 많이 쓰는데요. 오늘은 이 GKE와 WireGuard 조합에서 발생할 수 있는 잠재적인 보안 취약점들을 어떻게 분석하고 효과적으로 보안을 강화할 수 있을지 저의 삽질 경험을 바탕으로 솔직하게 풀어보려 합니다. 매니지드 쿠버네티스(Managed Kubernetes)의 보안, 과연 어디까지가 우리의 책임일까요? 함께 파헤쳐 봅시다! 💡

    GKE와 WireGuard, 왜 함께 쓸까요?

    먼저, GKE와 WireGuard가 무엇이고 왜 이 둘을 함께 쓰는지 간단히 짚고 넘어갈게요. 이미 잘 아시는 분들도 계시겠지만, 쉽게 말해드릴게요.

    • GKE (Google Kubernetes Engine): 구글에서 제공하는 매니지드 쿠버네티스 서비스예요. 복잡한 쿠버네티스 클러스터(Cluster) 구축과 운영을 구글이 대신 해주니까, 우리는 애플리케이션 개발에만 집중할 수 있죠. 노드(Node) 관리, 업그레이드, 확장성 등 많은 부분이 자동화되어 있어서 정말 편합니다.
    • WireGuard (와이어가드): 최근 각광받는 오픈소스 VPN 프로토콜이에요. 기존 VPN(예: OpenVPN, IPSec)보다 훨씬 가볍고 빠르면서도 강력한 암호화를 제공하거든요. 설정도 간단해서 저도 홈랩에서 자주 써먹고 있습니다.

    그럼 이 둘을 왜 같이 쓸까요? 주로 이런 시나리오에서 빛을 발합니다:

    • 하이브리드 클라우드(Hybrid Cloud) 환경 구축: 온프레미스 데이터센터나 다른 클라우드에 있는 시스템이 GKE 클러스터 내부의 서비스와 안전하게 통신해야 할 때요.
    • 개발자/운영자 원격 접속: 관리자들이 집이나 외부에서 GKE 클러스터의 프라이빗(Private) 네트워크에 안전하게 접속하여 kubectl(쿠버네티스 커맨드라인 툴) 명령어를 실행하거나 내부 서비스에 접근해야 할 때죠.
    • 보안 강화: GKE 클러스터의 외부 노출을 최소화하고, 특정 허용된 경로(VPN)를 통해서만 접근을 허용하여 전반적인 보안 수준을 높일 때입니다.

    이렇게 들으면 정말 만능 조합 같죠? 근데 여기서부터 우리의 보안 취약점 분석이 시작됩니다.

    GKE와 WireGuard를 연동하여 안전한 접근 경로를 확보하는 일반적인 아키텍처 다이어그램

    GKE와 WireGuard를 연동하여 안전한 접근 경로를 확보하는 일반적인 아키텍처 다이어그램입니다.

    “취약점 분석”의 의미: 잠재적 위험 요소 파악

    제목에 “GKE WireGuard 보안 취약점 분석”이라고 했더니, 혹시 특정 버그(Bug)나 제로데이(Zero-day) 취약점을 떠올리셨나요? 사실 오늘 다룰 내용은 특정 소프트웨어의 버그보다는, GKE와 WireGuard를 연동하는 과정에서 발생할 수 있는 설계적, 설정적 미흡함으로 인한 잠재적 보안 문제들에 대한 분석입니다. 이걸 저는 넓은 의미의 ‘취약점’으로 보고 접근해요.

    매니지드 서비스라고 마냥 안심할 수 없는 이유는 바로 공동 책임 모델 (Shared Responsibility Model) 때문입니다. GKE 자체의 인프라(Infrastructure), 즉 쿠버네티스 컨트롤 플레인(Control Plane)이나 노드 운영체제(Operating System) 등은 구글이 관리하지만, 그 위에서 돌아가는 우리의 워크로드(Workload), 네트워크 구성, 데이터 보안 등은 전적으로 사용자의 책임이거든요. WireGuard 설정 역시 마찬가지예요. 이 경계를 명확히 이해하는 것이 보안 강화의 첫걸음입니다. ⚠️

    WireGuard 구성 시 고려해야 할 보안 취약점

    제가 실제로 GKE에 WireGuard를 연결하면서 “아차!” 했던 부분들을 중심으로 잠재적 취약점들을 짚어볼게요.

    1. 키 관리 (Key Management)의 허술함

    WireGuard는 공개키 암호화(Public-key cryptography)를 사용해요. 각 피어(Peer)마다 고유한 프라이빗 키(Private Key)와 퍼블릭 키(Public Key)를 가지죠. 이 프라이빗 키가 유출된다면 어떻게 될까요? 해당 키를 가진 누구나 VPN을 통해 우리 GKE 네트워크에 접근할 수 있게 됩니다. 홈랩에서는 제가 직접 관리하니 괜찮지만, 여러 팀원이나 시스템이 사용하는 프로덕션(Production) 환경에서는 정말 치명적입니다.

    • 문제점: 프라이빗 키를 코드 저장소에 올리거나, 관리 서버에 평문(Plaintext)으로 저장하는 경우가 있어요.
    • 강화 전략: Google Secret Manager(시크릿 매니저) 같은 안전한 키 관리 서비스(KMS, Key Management Service)를 활용하여 키를 안전하게 보관하고, 필요한 인스턴스(Instance)나 서비스 계정(Service Account)에만 최소한의 권한으로 접근을 허용해야 합니다.

    2. 과도한 접근 제어 (Access Control)

    WireGuard 서버를 설정할 때, `AllowedIPs` 옵션으로 허용할 IP 대역을 지정하죠. “일단 다 열어두고 나중에 좁혀야지” 하는 생각으로 0.0.0.0/0이나 너무 넓은 대역을 설정하면 곤란합니다. GKE 내부의 민감한 서비스까지 접근이 허용될 수 있거든요.

    • 문제점: WireGuard 피어가 GKE 클러스터의 모든 서브넷(Subnet)에 접근 가능하게 설정되는 경우예요.
    • 강화 전략: 최소 권한의 원칙 (Principle of Least Privilege)을 적용해야 해요. 특정 WireGuard 피어는 필요한 GKE 내부 서비스의 IP 대역이나 특정 파드(Pod)의 IP 대역에만 접근할 수 있도록 `AllowedIPs`를 정확하게 지정해야 하니까요.

    3. 네트워크 분리 (Network Segmentation)의 부재

    WireGuard VPN으로 GKE VPC(Virtual Private Cloud)에 연결했다고 해서 모든 것이 끝나는 게 아닙니다. VPN 터널(Tunnel)은 단지 ‘길’을 만들어줄 뿐, 그 ‘길’을 통해 어디까지 갈 수 있는지는 GKE와 GCP(Google Cloud Platform)의 네트워크 규칙이 결정하거든요.

    • 문제점: WireGuard 서버가 위치한 서브넷과 GKE 클러스터 서브넷 간의 방화벽 규칙(Firewall Rule)이 너무 느슨하거나, GKE 내부의 네트워크 정책(Network Policy)이 없는 경우를 말해요.
    • 강화 전략: Shared VPC(공유 VPC)를 활용하여 네트워크를 중앙에서 관리하고, GCP 방화벽 규칙을 통해 WireGuard 서버에서 GKE 클러스터로 들어오는 트래픽을 세밀하게 제어해야 합니다. 예를 들어, 특정 포트(Port)와 프로토콜(Protocol)만 허용하는 식이죠. 또한, GKE 내부에서는 쿠버네티스 네트워크 정책(Kubernetes Network Policies)을 사용하여 파드 간 통신을 제어해야 한답니다.

    4. 모니터링 및 로깅 (Monitoring & Logging)의 부족

    아무리 보안을 강화해도 사고는 발생할 수 있어요. 중요한 건 사고 발생 시 얼마나 빠르게 인지하고 대응하느냐죠. WireGuard 트래픽이나 GKE 내부 네트워크 트래픽에 대한 가시성(Visibility)이 없다면 문제가 생겨도 알 수가 없습니다.

    • 문제점: WireGuard 서버의 접속 로그(Log)나 GKE의 감사 로깅(Audit Logging)을 제대로 설정하지 않거나, 모니터링 시스템과 연동하지 않는 경우가 많아요.
    • 강화 전략: WireGuard 서버의 접속 및 트래픽 로그를 Cloud Logging(클라우드 로깅)으로 연동하고, GKE의 Cloud Audit Logs(클라우드 감사 로그)를 통해 클러스터 내의 모든 활동을 기록해야 해요. Cloud Monitoring(클라우드 모니터링)으로 관련 지표를 대시보드(Dashboard)에 띄우고, 비정상적인 활동에 대한 알림(Alert)을 설정하는 것이 필수입니다.

    GKE 환경에서 WireGuard 보안 강화 전략 (실전 팁)

    그럼 이제 위에서 언급한 취약점들을 보완하면서 실제로 어떻게 GKE와 WireGuard의 보안을 강화할 수 있는지 제가 사용해본 몇 가지 실전 팁을 공유해드릴게요.

    1. 프라이빗 GKE 클러스터 (Private GKE Cluster) 활용

    GKE 클러스터의 마스터(Master)와 노드(Node) 모두 프라이빗 IP 주소만 가지도록 설정하여 인터넷에 노출되지 않게 하는 것이 가장 강력한 보안 조치 중 하나예요. WireGuard VPN을 통해서만 접근 가능하게 만들 수 있거든요.

    
    gcloud container clusters create my-private-gke \
        --region=asia-east1 \
        --enable-private-nodes \
        --enable-private-endpoint \
        --master-ipv4-cidr=172.16.0.0/28 \
        --create-subnetwork name=my-gke-subnet,range=10.10.0.0/20
    

    이렇게 하면 외부에서는 프라이빗 IP로만 접근할 수 있기 때문에, WireGuard VPN을 통해 VPC 네트워크에 연결된 경우에만 GKE API 서버에 접근할 수 있어요. 🔒

    2. Shared VPC(공유 VPC)와 세분화된 방화벽 규칙

    WireGuard 서버는 별도의 서브넷에 두거나, 아예 다른 GCP 프로젝트(Project)의 Shared VPC에 위치시켜 GKE 클러스터와 네트워크를 분리하는 것을 권장합니다. 그리고 반드시 필요한 트래픽만 허용하는 방화벽 규칙을 적용해야 해요.

    
    gcloud compute firewall-rules create allow-wireguard-to-gke-api \
        --network=my-shared-vpc \
        --allow=tcp:443 \
        --source-ranges=YOUR_WIREGUARD_SERVER_IP/32 \
        --target-tags=gke-master-api-endpoint
    
    gcloud compute firewall-rules create allow-wireguard-to-gke-nodes \
        --network=my-shared-vpc \
        --allow=tcp:10250,tcp:30000-32767 \
        --source-ranges=YOUR_WIREGUARD_SERVER_IP/32 \
        --target-tags=gke-node
    

    위 예시처럼 WireGuard 서버 IP에서 GKE 마스터 API(tcp:443)와 노드(tcp:10250, NodePort 범위)로만 접근을 허용하는 방화벽 규칙을 만들 수 있어요. `YOUR_WIREGUARD_SERVER_IP` 부분은 실제 WireGuard 서버의 IP 주소로 바꿔주세요.

    GCP 콘솔에서 GKE와 WireGuard 간의 통신을 제어하는 방화벽 규칙을 설정하는 화면 예시

    GCP 콘솔에서 GKE와 WireGuard 간의 통신을 제어하는 방화벽 규칙을 설정하는 화면 예시입니다.

    3. Kubernetes Network Policies(네트워크 정책) 활용

    WireGuard VPN을 통해 GKE 내부 네트워크에 접근하더라도, 클러스터 내부의 파드 간 통신은 네트워크 정책으로 다시 한번 제어해야 합니다. 특정 네임스페이스(Namespace)나 파드에만 접근을 허용하는 식으로 말이에요. 이건 GKE 내부 보안의 핵심이거든요.

    
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: allow-wireguard-access-to-backend
      namespace: default
    spec:
      podSelector:
        matchLabels:
          app: backend-service
      policyTypes:
        - Ingress
      ingress:
        - from:
            - ipBlock:
                cidr: 10.128.0.0/14  # GKE 클러스터의 Pod CIDR 범위 (WireGuard 서버에서 접근할 수 있는 IP 대역)
            - namespaceSelector:
                matchLabels:
                  name: wireguard-client-namespace # WireGuard 클라이언트 Pod가 있다면 해당 네임스페이스
          ports:
            - protocol: TCP
              port: 8080
    

    이 정책은 `backend-service` 레이블을 가진 파드가 특정 IP 대역(WireGuard를 통해 접근하는 클라이언트 IP 범위) 또는 특정 네임스페이스의 파드로부터 8080 포트로만 트래픽을 허용하도록 한답니다.

    WireGuard 키를 안전하게 생성하고 Secret Manager를 통해 관리하는 프로세스 다이어그램

    WireGuard 키를 안전하게 생성하고 Secret Manager를 통해 관리하는 프로세스 다이어그램입니다.

    삽질 경험: “나는 괜찮겠지” 하다가 겪은 일

    저도 처음엔 “GKE는 구글이 관리해주니까 보안은 기본으로 되겠지? WireGuard는 빠르고 가볍다니까 그냥 연결만 하면 되겠지!” 하는 안일한 생각으로 시작했었어요. 홈랩에서 대충 WireGuard 서버 하나 띄워서 GKE VPC에 연결하고 `AllowedIPs = 0.0.0.0/0` 해놓고 썼습니다. 처음엔 잘 되더라고요. 🎉

    근데 어느 날, 개발자 한 분이 “특정 마이크로서비스(Microservice)에만 접속해서 디버깅(Debugging)해야 하는데, 뭔가 불안해요”라는 피드백을 주시더라고요. 그때 정신이 번쩍 들었습니다. WireGuard VPN을 통해 들어오면 마치 데이터센터 안에 있는 것처럼 GKE 클러스터의 모든 파드와 서비스에 접근이 가능한 상태였던 거죠. 😱

    이거 진짜 삽질 좀 했습니다 ㅎㅎ. 처음엔 방화벽 규칙을 좁혔는데, GKE 노드들이 서로 통신을 못 해서 클러스터가 깨지는 일도 있었고요. Network Policy를 적용하는 과정에서도 파드 셀렉터(Pod Selector)를 잘못 지정해서 멀쩡한 서비스가 통신 불가가 되는 상황도 겪었죠. 결국은 GCP 방화벽 규칙과 K8s 네트워크 정책을 촘촘하게, 그리고 단계적으로 적용하면서 테스트하고 또 테스트하는 과정을 거쳤어요. 이 과정에서 최소 권한의 원칙과 네트워크 분리의 중요성을 뼈저리게 느꼈습니다. 💡

    결과 및 검증

    위에 설명드린 보안 강화 전략들을 적용하고 나면, 반드시 검증 과정을 거쳐야 해요. 저는 주로 다음과 같은 방법으로 확인했습니다.

    • 접근 테스트: WireGuard VPN을 통해 접속한 후, 허용되지 않은 IP 대역이나 포트로 접근을 시도하여 차단되는지 확인합니다.
    • GCP Security Command Center(보안 명령 센터): GKE Security Posture(보안 상태) 대시보드를 통해 클러스터의 전반적인 보안 상태를 점검하고, 취약점이 없는지 확인했어요.
    • 로깅 확인: Cloud Logging에서 WireGuard 서버 로그와 GKE 감사 로그를 확인하여 비정상적인 접근 시도나 차단 기록이 잘 남는지 확인해요.

    이 과정을 통해 “아, 이제야 좀 안심하고 쓸 수 있겠구나” 하는 생각이 들더라고요. ✅

    GCP Security Command Center의 GKE Security Posture 대시보드를 통해 클러스터의 보안 상태를 점검하는 화면 예시

    GCP Security Command Center의 GKE Security Posture 대시보드를 통해 클러스터의 보안 상태를 점검하는 화면 예시입니다.

    마무리: 결국 보안은 지속적인 관리입니다

    오늘은 GKE와 WireGuard를 연동할 때 발생할 수 있는 잠재적인 보안 취약점들을 분석하고, 이를 강화하기 위한 전략들을 저의 경험을 바탕으로 이야기해봤습니다.

    핵심은 이거예요. 매니지드 서비스라고 해도 ‘내 책임’ 영역은 분명히 존재한다는 점, 그리고 그 영역에서 우리는 ‘최소 권한의 원칙’과 ‘네트워크 분리’를 철저히 지켜야 한다는 것. WireGuard 키 관리부터 시작해서 GCP 방화벽, K8s 네트워크 정책까지, 겹겹이 보안을 쌓아 올리는 것이 중요합니다.

    보안은 한 번 설정해두면 끝나는 게 아니라, 지속적인 관심과 관리가 필요해요. 정기적인 구성 감사, 취약점 스캔, 그리고 최신 보안 패치 적용을 게을리하지 마세요. 우리 서버실의 평화를 위해서 말이에요! 🛡️

    다음번엔 GKE 보안 모범 사례를 좀 더 깊게 파고들어볼까 합니다. 기대해주세요!

    GKE WireGuard 보안 강화 전략의 주요 내용을 요약한 인포그래픽입니다.

  • [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 클러스터 구성을 다뤘으니 함께 참고하시면 더 좋을 것 같습니다.

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