🌐 Terraform 멀티클라우드 전략
문서 목적
실무에서 사용하는 멀티클라우드 패턴과 Terraform으로 구현하는 방법을 제공합니다. AWS, GCP, Azure를 조합하여 각 클라우드의 강점을 활용하는 실전 예제 모음입니다.
📑 목차
1. 왜 멀티클라우드인가?
💡 실무 사례별 이유
🎯 Case 1: 각 클라우드의 특화 서비스 활용
실제 시나리오:
"우리 회사는 AWS에서 메인 인프라를 운영하지만,
데이터 분석은 GCP BigQuery가 훨씬 저렴하고 빠릅니다."
결정:
AWS (Compute, Storage) + GCP (BigQuery, Data Analytics)
클라우드별 강점:
| 클라우드 | 특화 서비스 | 선택 이유 |
|---|---|---|
| AWS | EC2, Lambda, S3, RDS | 가장 많은 서비스, 성숙한 생태계 |
| GCP | BigQuery, GKE, AI/ML | 데이터 분석 최강, K8s 네이티브 |
| Azure | Active Directory, Office 365 | 엔터프라이즈 통합, MS 생태계 |
🛡️ Case 2: 재해 복구 (Disaster Recovery)
실제 시나리오:
"2021년 AWS 서울 리전 장애로 서비스가 6시간 중단됐습니다.
이제 백업 리전을 다른 클라우드에 둡니다."
결정:
Primary: AWS (서울)
DR: GCP (도쿄) 또는 Azure (한국 중부)
🔓 Case 3: 벤더 락인 방지
실제 시나리오:
"AWS만 쓰다가 가격이 올라도 선택지가 없었습니다.
이제 워크로드를 다른 클라우드로 옮길 수 있는 구조를 만듭니다."
결정:
컨테이너 기반 (Docker/K8s) + Terraform IaC
→ 언제든 마이그레이션 가능
💰 Case 4: 비용 최적화
실제 시나리오:
"같은 워크로드인데 GCP가 30% 저렴합니다.
하지만 기존 AWS 인프라와 통합이 필요합니다."
결정:
비용 민감 워크로드 → GCP
기존 서비스와 통합 필요 → AWS
2. 멀티클라우드 패턴
📊 Pattern 1: 하이브리드 (Hybrid)
구조:
AWS (메인 인프라)
├── EC2 (웹 서버)
├── RDS (데이터베이스)
└── S3 (파일 저장)
↓ 데이터 전송
GCP (데이터 분석)
└── BigQuery (데이터 웨어하우스)
장점:
- ✅ 각 클라우드의 강점 활용
- ✅ 비용 최적화
단점:
- ❌ 복잡도 증가
- ❌ 네트워크 레이턴시
- ❌ 데이터 전송 비용
📊 Pattern 2: 재해 복구 (DR)
구조:
Primary: AWS (서울)
├── 실시간 트래픽 100%
└── State: Active
DR: GCP (도쿄)
├── 대기 상태 (Standby)
└── 주기적 데이터 동기화
장애 발생 시:
→ DNS 전환 (Route 53 → Cloud DNS)
→ GCP가 Primary로 승격
장점:
- ✅ 리전 장애 대응
- ✅ 벤더 장애 대응
단점:
- ❌ DR 환경 유지 비용
- ❌ 데이터 동기화 복잡도
📊 Pattern 3: 멀티 리전 액티브-액티브
구조:
AWS (서울) ←→ GCP (도쿄) ←→ Azure (싱가포르)
↓ ↓ ↓
50% 트래픽 30% 트래픽 20% 트래픽
Global Load Balancer (Cloudflare, Akamai)
→ 지역별 최적 클라우드로 라우팅
장점:
- ✅ 글로벌 최적 성능
- ✅ 높은 가용성
단점:
- ❌ 매우 높은 복잡도
- ❌ 데이터 일관성 문제
3. AWS + GCP 통합 실전 예제
🎯 시나리오: AWS 인프라 + GCP BigQuery 데이터 분석
아키텍처:
┌─────────────────────────────────────────────────────┐
│ AWS │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ EC2 Web │───→│ RDS │───→│ S3 Logs │ │
│ └──────────┘ └──────────┘ └─────┬────┘ │
│ │ │
└─────────────────────────────────────────┼──────────┘
│
데이터 전송 (매일 1회)
↓
┌─────────────────────────────────────────┼──────────┐
│ GCP │ │
│ ┌─────────────┐ ┌──────────────────▼───────┐ │
│ │ Data Studio │←───│ BigQuery │ │
│ │ (대시보드) │ │ (데이터 분석) │ │
│ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────┘
📄 main.tf
# 📊 AWS Provider
provider "aws" {
region = "ap-northeast-2"
}
# 📊 GCP Provider
provider "google" {
project = "my-gcp-project"
region = "asia-northeast3"
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# AWS 리소스
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 📊 S3 버킷 (로그 저장)
resource "aws_s3_bucket" "logs" {
bucket = "my-app-logs-${random_string.suffix.result}"
tags = {
Purpose = "Application Logs"
Cloud = "AWS"
}
}
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
# 📊 S3 버킷 라이프사이클 (30일 후 삭제)
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
rule {
id = "delete-old-logs"
status = "Enabled"
expiration {
days = 30
}
}
}
# 📊 IAM User for BigQuery Data Transfer
resource "aws_iam_user" "bigquery_transfer" {
name = "bigquery-data-transfer"
tags = {
Purpose = "GCP BigQuery Data Transfer"
}
}
resource "aws_iam_access_key" "bigquery_transfer" {
user = aws_iam_user.bigquery_transfer.name
}
# 📊 S3 Read-Only Policy
resource "aws_iam_user_policy" "bigquery_s3_read" {
name = "s3-read-only"
user = aws_iam_user.bigquery_transfer.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = [
aws_s3_bucket.logs.arn,
"${aws_s3_bucket.logs.arn}/*"
]
}
]
})
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# GCP 리소스
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 📊 BigQuery Dataset
resource "google_bigquery_dataset" "logs" {
dataset_id = "application_logs"
location = "asia-northeast3"
description = "Application logs from AWS S3"
labels = {
purpose = "analytics"
source = "aws-s3"
}
}
# 📊 BigQuery Table
resource "google_bigquery_table" "access_logs" {
dataset_id = google_bigquery_dataset.logs.dataset_id
table_id = "access_logs"
schema = jsonencode([
{
name = "timestamp"
type = "TIMESTAMP"
mode = "REQUIRED"
},
{
name = "user_id"
type = "STRING"
mode = "NULLABLE"
},
{
name = "action"
type = "STRING"
mode = "REQUIRED"
},
{
name = "status_code"
type = "INTEGER"
mode = "REQUIRED"
},
{
name = "response_time_ms"
type = "INTEGER"
mode = "NULLABLE"
}
])
time_partitioning {
type = "DAY"
field = "timestamp"
}
labels = {
source = "aws-s3"
}
}
# 📊 BigQuery Data Transfer (S3 → BigQuery)
resource "google_bigquery_data_transfer_config" "s3_transfer" {
display_name = "AWS S3 to BigQuery Transfer"
location = "asia-northeast3"
data_source_id = "amazon_s3"
schedule = "every day 03:00"
destination_dataset_id = google_bigquery_dataset.logs.dataset_id
params = {
data_path_template = "s3://${aws_s3_bucket.logs.bucket}/logs/*"
destination_table_name = google_bigquery_table.access_logs.table_id
file_format = "JSON"
max_bad_records = "100"
access_key_id = aws_iam_access_key.bigquery_transfer.id
secret_access_key = aws_iam_access_key.bigquery_transfer.secret
}
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 크로스 클라우드 모니터링
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 📊 CloudWatch Metric for S3 Object Count
resource "aws_cloudwatch_metric_alarm" "s3_object_count" {
alarm_name = "s3-logs-count"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "NumberOfObjects"
namespace = "AWS/S3"
period = 86400
statistic = "Average"
threshold = 10000
dimensions = {
BucketName = aws_s3_bucket.logs.bucket
StorageType = "AllStorageTypes"
}
alarm_description = "S3 버킷에 로그가 너무 많이 쌓였습니다"
}📄 outputs.tf
# 📤 AWS 출력
output "aws_s3_bucket_name" {
description = "AWS S3 버킷 이름"
value = aws_s3_bucket.logs.bucket
}
output "aws_iam_access_key_id" {
description = "BigQuery 전송용 IAM Access Key ID"
value = aws_iam_access_key.bigquery_transfer.id
}
output "aws_iam_secret_access_key" {
description = "BigQuery 전송용 IAM Secret Key"
value = aws_iam_access_key.bigquery_transfer.secret
sensitive = true
}
# 📤 GCP 출력
output "gcp_bigquery_dataset_id" {
description = "GCP BigQuery Dataset ID"
value = google_bigquery_dataset.logs.dataset_id
}
output "gcp_bigquery_table_id" {
description = "GCP BigQuery Table ID"
value = google_bigquery_table.access_logs.table_id
}
output "bigquery_query_url" {
description = "BigQuery 콘솔 쿼리 URL"
value = "https://console.cloud.google.com/bigquery?project=${var.gcp_project_id}&d=${google_bigquery_dataset.logs.dataset_id}&t=${google_bigquery_table.access_logs.table_id}"
}🚀 실행 방법
# 1. 초기화
terraform init
# 2. 실행 계획
terraform plan
# 3. 배포
terraform apply
# 4. AWS S3에 샘플 로그 업로드
echo '{"timestamp":"2025-12-31T10:00:00Z","user_id":"user123","action":"login","status_code":200,"response_time_ms":150}' > sample.json
aws s3 cp sample.json s3://$(terraform output -raw aws_s3_bucket_name)/logs/
# 5. 다음날 03:00에 자동으로 BigQuery로 전송됨
# 또는 수동 실행:
# gcloud transfer jobs run JOB_NAME
# 6. BigQuery에서 쿼리
# https://console.cloud.google.com/bigquery
# SELECT * FROM application_logs.access_logs LIMIT 1004. AWS + Azure 통합 실전 예제
🎯 시나리오: AWS 인프라 + Azure Active Directory
아키텍처:
┌────────────────────────────────────────────────────┐
│ Azure │
│ ┌──────────────────────────────────────────────┐ │
│ │ Azure Active Directory (AAD) │ │
│ │ - 사용자 관리 │ │
│ │ - SSO (Single Sign-On) │ │
│ └────────────────┬─────────────────────────────┘ │
└───────────────────┼────────────────────────────────┘
│
SAML 2.0 / OIDC
↓
┌───────────────────┼────────────────────────────────┐
│ AWS │ │
│ ┌───────────────▼──────────────────┐ │ │
│ │ IAM Identity Provider │ │ │
│ │ (Azure AD 연동) │ │ │
│ └───────────────┬──────────────────┘ │ │
│ │ │ │
│ ┌───────────────▼──────────────────┐ │ │
│ │ IAM Role │ │ │
│ │ (Azure AD 사용자에게 권한 부여) │ │ │
│ └───────────────┬──────────────────┘ │ │
│ │ │ │
│ ┌───────────────▼──────────────────┐ │ │
│ │ EC2, S3, RDS 등 │ │ │
│ │ (리소스 접근) │ │ │
│ └──────────────────────────────────┘ │ │
└─────────────────────────────────────────┴──────────┘
📄 main.tf
# 📊 AWS Provider
provider "aws" {
region = "ap-northeast-2"
}
# 📊 Azure Provider
provider "azurerm" {
features {}
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Azure AD 설정
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 📊 Azure AD Application
resource "azuread_application" "aws_sso" {
display_name = "AWS SSO Integration"
web {
redirect_uris = [
"https://signin.aws.amazon.com/saml"
]
}
api {
requested_access_token_version = 2
}
}
# 📊 Service Principal
resource "azuread_service_principal" "aws_sso" {
application_id = azuread_application.aws_sso.application_id
}
# 📊 Azure AD Group for AWS Admin
resource "azuread_group" "aws_admins" {
display_name = "AWS Administrators"
security_enabled = true
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# AWS IAM 설정
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 📊 AWS IAM SAML Provider (Azure AD)
resource "aws_iam_saml_provider" "azure_ad" {
name = "AzureAD"
saml_metadata_document = file("azure-ad-metadata.xml")
}
# 📊 IAM Role for Azure AD Users
resource "aws_iam_role" "azure_ad_admin" {
name = "AzureAD-Admin"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_saml_provider.azure_ad.arn
}
Action = "sts:AssumeRoleWithSAML"
Condition = {
StringEquals = {
"SAML:aud" = "https://signin.aws.amazon.com/saml"
}
}
}
]
})
tags = {
IntegratedWith = "AzureAD"
}
}
# 📊 Admin 권한 부여
resource "aws_iam_role_policy_attachment" "azure_ad_admin" {
role = aws_iam_role.azure_ad_admin.name
policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}
# 📊 읽기 전용 Role
resource "aws_iam_role" "azure_ad_readonly" {
name = "AzureAD-ReadOnly"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_saml_provider.azure_ad.arn
}
Action = "sts:AssumeRoleWithSAML"
Condition = {
StringEquals = {
"SAML:aud" = "https://signin.aws.amazon.com/saml"
}
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "azure_ad_readonly" {
role = aws_iam_role.azure_ad_readonly.name
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}5. 3개 클라우드 동시 사용 (Tri-Cloud)
🎯 시나리오: 글로벌 재해 복구
아키텍처:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ AWS │ │ GCP │ │ Azure │
│ 서울 │ │ 도쿄 │ │ 싱가포르 │
│ │ │ │ │ │
│ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │
│ │ EC2 Web │ │ │ │ GCE Web │ │ │ │ VM Web │ │
│ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │
│ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │
│ │ RDS │◄┼──────┼─│Cloud SQL │◄┼──────┼─│Azure SQL │ │
│ └──────────┘ │ 복제 │ └──────────┘ │ 복제 │ └──────────┘ │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└─────────────────────┴─────────────────────┘
Global Load Balancer
(Cloudflare)
📄 main.tf
# 📊 Providers
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
provider "aws" {
region = "ap-northeast-2" # 서울
}
provider "google" {
project = var.gcp_project_id
region = "asia-northeast1" # 도쿄
}
provider "azurerm" {
features {}
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 공통 변수
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
variable "app_name" {
default = "global-app"
}
variable "container_image" {
description = "Docker 이미지 (모든 클라우드에서 동일)"
default = "nginx:latest"
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# AWS (서울) - Primary
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
module "aws_infrastructure" {
source = "./modules/aws-web-server"
app_name = "${var.app_name}-aws"
instance_count = 2
instance_type = "t3.micro"
region = "ap-northeast-2"
tags = {
Cloud = "AWS"
Location = "Seoul"
Role = "Primary"
}
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# GCP (도쿄) - Secondary
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
module "gcp_infrastructure" {
source = "./modules/gcp-web-server"
app_name = "${var.app_name}-gcp"
instance_count = 2
machine_type = "e2-micro"
region = "asia-northeast1"
labels = {
cloud = "gcp"
location = "tokyo"
role = "secondary"
}
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Azure (싱가포르) - Tertiary
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
resource "azurerm_resource_group" "main" {
name = "${var.app_name}-rg"
location = "Southeast Asia" # 싱가포르
tags = {
Cloud = "Azure"
Location = "Singapore"
Role = "Tertiary"
}
}
module "azure_infrastructure" {
source = "./modules/azure-web-server"
app_name = "${var.app_name}-azure"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
instance_count = 2
vm_size = "Standard_B1s"
tags = {
Cloud = "Azure"
Location = "Singapore"
Role = "Tertiary"
}
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 출력 - 글로벌 엔드포인트
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
output "endpoints" {
description = "모든 클라우드 엔드포인트"
value = {
aws = {
region = "Seoul (ap-northeast-2)"
public_ip = module.aws_infrastructure.public_ip
status = "Primary"
health_url = "http://${module.aws_infrastructure.public_ip}/health"
}
gcp = {
region = "Tokyo (asia-northeast1)"
public_ip = module.gcp_infrastructure.public_ip
status = "Secondary (DR)"
health_url = "http://${module.gcp_infrastructure.public_ip}/health"
}
azure = {
region = "Singapore (Southeast Asia)"
public_ip = module.azure_infrastructure.public_ip
status = "Tertiary (DR)"
health_url = "http://${module.azure_infrastructure.public_ip}/health"
}
}
}
output "global_dns_configuration" {
description = "Cloudflare 등 Global LB 설정 정보"
value = {
primary_origin = module.aws_infrastructure.public_ip
secondary_origin = module.gcp_infrastructure.public_ip
tertiary_origin = module.azure_infrastructure.public_ip
recommended_weights = {
aws = 50
gcp = 30
azure = 20
}
}
}📄 modules/aws-web-server/main.tf (모듈 예시)
variable "app_name" {}
variable "instance_count" {}
variable "instance_type" {}
variable "region" {}
variable "tags" {}
# 📊 Security Group
resource "aws_security_group" "web" {
name = "${var.app_name}-sg"
description = "Allow HTTP and HTTPS"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = var.tags
}
# 📊 EC2 Instances
resource "aws_instance" "web" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y nginx
CLOUD="AWS"
REGION="${var.region}"
INSTANCE_ID=$(ec2-metadata --instance-id | cut -d " " -f 2)
cat > /var/www/html/index.html <<HTML
<!DOCTYPE html>
<html>
<head><title>Multi-Cloud App</title></head>
<body>
<h1>Hello from $CLOUD</h1>
<p>Region: $REGION</p>
<p>Instance: $INSTANCE_ID</p>
</body>
</html>
HTML
systemctl start nginx
EOF
tags = merge(var.tags, {
Name = "${var.app_name}-${count.index}"
})
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
# 📊 Elastic IP
resource "aws_eip" "web" {
count = 1
instance = aws_instance.web[0].id
domain = "vpc"
tags = var.tags
}
output "public_ip" {
value = aws_eip.web[0].public_ip
}6. 네트워크 연결
🌉 클라우드 간 VPN 연결
AWS ↔ GCP VPN
# 📊 AWS Customer Gateway
resource "aws_customer_gateway" "gcp" {
bgp_asn = 65000
ip_address = google_compute_address.vpn_gateway.address
type = "ipsec.1"
tags = {
Name = "GCP-VPN-Gateway"
}
}
# 📊 AWS VPN Gateway
resource "aws_vpn_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "AWS-VPN-Gateway"
}
}
# 📊 AWS VPN Connection
resource "aws_vpn_connection" "gcp" {
vpn_gateway_id = aws_vpn_gateway.main.id
customer_gateway_id = aws_customer_gateway.gcp.id
type = "ipsec.1"
static_routes_only = false
tags = {
Name = "AWS-to-GCP-VPN"
}
}
# 📊 GCP VPN Gateway
resource "google_compute_vpn_gateway" "aws" {
name = "aws-vpn-gateway"
network = google_compute_network.main.id
region = "asia-northeast3"
}
# 📊 GCP External IP for VPN
resource "google_compute_address" "vpn_gateway" {
name = "vpn-gateway-ip"
region = "asia-northeast3"
}
# 📊 GCP VPN Tunnel
resource "google_compute_vpn_tunnel" "aws" {
name = "aws-vpn-tunnel"
peer_ip = aws_vpn_connection.gcp.tunnel1_address
shared_secret = aws_vpn_connection.gcp.tunnel1_preshared_key
target_vpn_gateway = google_compute_vpn_gateway.aws.id
local_traffic_selector = ["10.0.0.0/16"]
remote_traffic_selector = ["172.16.0.0/16"]
}7. 주의사항 및 트레이드오프
⚠️ 복잡도 증가
| 항목 | 단일 클라우드 | 멀티 클라우드 |
|---|---|---|
| 학습 곡선 | 1개 클라우드 학습 | 2-3개 클라우드 학습 |
| 관리 포인트 | 1개 콘솔 | 3개 콘솔 |
| 비용 추적 | 간단 | 복잡 (통합 필요) |
| 네트워크 | 단순 | VPN/Peering 필요 |
| 보안 | 1개 정책 | 3개 정책 통합 |
💰 비용 함정
멀티클라우드 숨은 비용:
1. 데이터 전송 비용 (Egress)
- AWS → GCP: $0.09/GB
- 월 1TB 전송 시: $92.16
2. VPN 연결 비용
- AWS VPN: $0.05/시간 = $36/월
- GCP VPN: $0.05/시간 = $36/월
- 총: $72/월
3. 관리 시간 비용
- 엔지니어 학습 시간: 주 5시간
- 3개 클라우드 모니터링: 주 3시간
🔒 보안 고려사항
# 📊 네트워크 격리
# AWS VPC: 10.0.0.0/16
# GCP VPC: 10.1.0.0/16
# Azure VNet: 10.2.0.0/16
# 📊 CIDR 충돌 방지 필수!
# 📊 통합 인증/인가
# - Azure AD (Primary IdP)
# - AWS IAM (Federated)
# - GCP IAM (Federated)
# 📊 암호화
# - 전송 중: VPN/TLS
# - 저장: KMS (각 클라우드별)📊 데이터 주권 (Data Sovereignty)
규제 고려사항:
1. GDPR (유럽): 유럽 리전 필수
2. 개인정보보호법 (한국): 국내 리전 권장
3. 중국: 중국 내 리전 필수
멀티클라우드 시:
- 데이터 위치 명확히 추적
- 리전 간 복제 규제 확인
- 로그/감사 통합
💡 베스트 프랙티스
✅ Do
- 컨테이너 사용: Docker/K8s로 클라우드 간 이동성 확보
- 모듈화: 클라우드별 모듈 분리
- 통합 모니터링: Datadog, New Relic 등 통합 도구
- IaC 필수: Terraform으로 모든 인프라 코드화
- 비용 알람: 각 클라우드별 예산 설정
❌ Don’t
- 과도한 복잡도: 필요 없으면 단일 클라우드 사용
- 수동 관리: 콘솔 클릭 금지, 모두 코드로
- CIDR 충돌: 네트워크 계획 없이 시작 금지
- 보안 간과: 각 클라우드별 보안 정책 필수
- 비용 무시: 데이터 전송 비용 사전 계산
🎯 의사결정 가이드
”멀티클라우드를 해야 할까?”
✅ YES (멀티클라우드 추천):
- 글로벌 서비스 (저지연 필요)
- 고가용성 필수 (99.99% 이상)
- 각 클라우드 특화 서비스 필요 (BigQuery, Azure AD 등)
- 벤더 락인 우려
❌ NO (단일 클라우드 권장):
- 스타트업 초기 (리소스 부족)
- 팀 규모 5명 이하
- 국내만 서비스
- 복잡도 관리 어려움
📚 참고 자료
Created: 2025-12-31 Tags: terraform multi-cloud aws gcp azure hybrid Category: Terraform/멀티클라우드