근묵자흑
프로덕션 수준의 테라폼 코드 본문
1. 프로덕션 수준 인프라 구축에 오랜 시간이 걸리는 이유
DevOps 산업의 성숙도
프로덕션 수준 인프라 구축이 오래 걸리는 첫 번째 이유는 DevOps 산업이 아직 초기 단계이기 때문입니다:
클라우드 컴퓨팅, IaC, DevOps 등의 용어는 2000년대 중후반에 등장테라폼, 도커, 패커, 쿠버네티스는 2010년 중후반에 출시기술이 아직 충분히 성숙하지 않았고, 경험 많은 전문가가 부족
DevOps 산업의 현재 상태 (2025)
- 시스템 복잡도 증가
- 마이크로서비스 아키텍처의 보편화
- 멀티클라우드/하이브리드 환경 요구 증가
- 보안 요구사항의 지속적 강화
- 규제 요구사항의 복잡화
- 높아진 기대치
- 제로 다운타임에 대한 요구
- 즉각적인 확장성 기대
- 완벽한 보안과 규정 준수
- 비용 최적화 요구
- 통합의 어려움
- 레거시 시스템과의 연동
- 다양한 도구들 간의 통합
- 조직간 협업 필요성
"야크 털 깎기" 현상
두 번째 이유는 "야크 털 깎기" 현상입니다:
- 원래 하고자 했던 작업을 수행하기 전에 해야 하는 하찮고, 겉보기에 관련성이 적어 보이는 사전 작업들
- 예시: 새 서비스 배포 → VPC 수정 → 네트워크 정책 변경 → 보안 검토 필요
복잡성의 두 가지 유형
- 본질적 복잡성(Essential Complexity)
- 문제 자체가 가진 근본적인 복잡성
- 피할 수 없는 복잡성
- 예: 고가용성, 보안 요구사항, 성능 최적화
- 우발적 복잡성(Accidental Complexity)
- 특정 도구나 프로세스에 의해 발생하는 복잡성
- 예: 도구 버전 충돌, API 제한, 환경 차이
DevOps의 광범위한 책임 영역
마지막으로, DevOps가 다루는 영역이 매우 광범위합니다:
- 빌드부터 배포
- 보안과 규정 준수
- 모니터링과 로깅
- 비용 최적화
- 장애 대응
2. 프로덕션 수준 인프라 체크리스트
작업 | 설명 | 도구 예시 |
---|---|---|
설치 (Install) | 소프트웨어 바이너리 및 모든 종속성 설치 | • Bash • Ansible • Docker • Packer |
구성 (Configure) | • 런타임에서의 소프트웨어 구성 • 포트 설정 • TLS 인증서 • 서비스 디스커버리 • 리더/팔로워 구성 • 복제 설정 | • Chef • Ansible • Kubernetes |
프로비저닝 (Provision) | • 인프라 프로비저닝 • 서버, 로드밸런서 구성 • 네트워크 구성 • 방화벽 설정 • IAM 권한 관리 | • Terraform • CloudFormation |
배포 (Deploy) | • 인프라 위 서비스 배포 • 무중단 업데이트 롤아웃 • 블루-그린, 롤링, 카나리 배포 | • ASG • Kubernetes • ECS |
고가용성 (High Availability) | 개별 프로세스, 서버, 서비스, 데이터센터, 리전의 장애 대응 | • 멀티 데이터센터 • 멀티 리전 |
확장성 (Scalability) | • 부하에 따른 확장/축소 • 수평적 확장 (서버 수 증가) • 수직적 확장 (서버 사양 증가) | • Auto Scaling • Replication |
성능 (Performance) | • CPU, 메모리, 디스크, 네트워크, GPU 사용 최적화 • 쿼리 튜닝 • 벤치마킹 • 부하 테스트 • 프로파일링 | • Dynatrace • Valgrind • VisualVM |
네트워킹 (Networking) | • 정적/동적 IP 구성 • 포트 관리 • 서비스 디스커버리 • 방화벽 설정 • DNS 관리 • SSH/VPN 접근 | • VPC • 방화벽 • Route 53 |
보안 (Security) | • 전송 중 암호화 (TLS) • 저장 데이터 암호화 • 인증 및 인가 • 비밀 관리 • 서버 보안 강화 | • ACM • Let's Encrypt • KMS • Vault |
메트릭 (Metrics) | • 가용성 메트릭 • 비즈니스 메트릭 • 앱/서버 메트릭 • 이벤트 • 관찰 가능성 • 추적 • 경보 | • CloudWatch • Datadog |
로그 (Logs) | • 디스크 로그 순환 • 중앙 집중식 로그 수집 | • Elastic Stack • Sumo Logic |
데이터 백업 (Backup) | • DB, 캐시, 기타 데이터의 정기적 백업 • 별도 리전/계정으로 복제 | • AWS Backup • RDS 스냅샷 |
비용 최적화 (Cost) | • 적절한 인스턴스 유형 선택 • 스팟/예약 인스턴스 활용 • 오토스케일링 활용 • 미사용 리소스 정리 | • Auto Scaling • Infracost |
문서화 (Documentation) | • 코드 문서화 • 아키텍처 문서화 • 모범 사례 문서화 • 장애 대응 플레이북 작성 | • README • Wiki • Slack • IaC |
테스트 (Tests) | • 인프라 코드의 자동화된 테스트 작성 • 커밋 후 테스트 실행 • 야간 테스트 실행 | • Terratest • tflint • OPA • InSpec |
3. 프로덕션 수준 인프라 모듈
3.1 소형 모듈
대형 모듈의 단점:
- 속도가 느림(plan에만 5분 이상)
- 안전하지 않음(관리자 권한 필요)
- 위험성이 높음
- 이해하기 어려움
- 리뷰하기 어려움
- 테스트하기 어려움
소형 모듈 예시:
# networking/main.tf
module "vpc" {
source = "../modules/vpc"
vpc_cidr = "10.0.0.0/16"
environment = "production"
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
}
# security/main.tf
module "security_groups" {
source = "../modules/security-groups"
vpc_id = module.vpc.vpc_id
environment = "production"
}
3.2 합성 가능한 모듈
Unix 철학과 모듈 설계
테라폼 모듈 설계의 기본 원칙은 Unix 철학에서 시작합니다. Unix의 창시자 중 한 명인 Doug McIlroy는 이렇게 말했습니다:
"한 가지 일을 잘 하는 프로그램을 작성하라. 프로그램이 함께 동작하도록 작성하라."
이 철학을 테라폼 모듈에 적용하면, 각 모듈은:
- 단일 책임을 가져야 함
- 다른 모듈과 쉽게 조합될 수 있어야 함
- 재사용이 가능해야 함
함수형 프로그래밍의 영향
모듈 설계에서 중요한 또 다른 원칙은 함수형 프로그래밍의 '부작용 최소화' 원칙입니다. 간단한 예를 들어보겠습니다:
# 단순한 함수들
def add(x, y)
return x + y
end
def multiply(x, y)
return x * y
end
# 함수 합성
def calculate(x, y)
return multiply(add(x, y), x)
end
이처럼 작은 함수들을 조합하여 복잡한 연산을 수행할 수 있습니다. 테라폼 모듈도 이와 같은 원칙을 따릅니다.
실전: 합성 가능한 테라폼 모듈 만들기
Hello World 웹 애플리케이션을 예시로, 작은 모듈들을 어떻게 조합하여 완전한 인프라를 구축하는지 알아보겠습니다.
기본 아키텍처
구축할 인프라는 다음과 같습니다:
- Auto Scaling Group(ASG)로 관리되는 EC2 인스턴스들
- Application Load Balancer(ALB)를 통한 부하 분산
- 데이터베이스 연결 설정
- 다중 환경(prod, stage, dev) 지원
모듈 구성하기
1. ASG(Auto Scaling Group) 모듈
먼저 서버 인스턴스들을 관리할 ASG 모듈을 설정합니다:
module "asg" {
source = "../../cluster/asg-rolling-deploy"
# 환경별 이름 설정
cluster_name = "hello-world-${var.environment}"
# 인스턴스 설정
ami = var.ami
instance_type = var.instance_type
# 시작 스크립트 설정
user_data = templatefile("${path.module}/user-data.sh", {
server_port = var.server_port
db_address = data.terraform_remote_state.db.outputs.address
db_port = data.terraform_remote_state.db.outputs.port
server_text = var.server_text
})
# 오토스케일링 설정
min_size = var.min_size
max_size = var.max_size
enable_autoscaling = var.enable_autoscaling
# 네트워크 설정
subnet_ids = data.aws_subnets.default.ids
target_group_arns = [aws_lb_target_group.asg.arn]
health_check_type = "ELB"
}
2. ALB(Application Load Balancer) 모듈
다음으로 로드 밸런서 모듈을 설정합니다:
module "alb" {
source = "../../networking/alb"
alb_name = "hello-world-${var.environment}"
subnet_ids = data.aws_subnets.default.ids
}
3. Target Group 설정
ALB가 트래픽을 전달할 대상 그룹을 설정합니다:
resource "aws_lb_target_group" "asg" {
name = "hello-world-${var.environment}"
port = var.server_port
protocol = "HTTP"
vpc_id = data.aws_vpc.default.id
health_check {
path = "/"
protocol = "HTTP"
matcher = "200"
interval = 15
timeout = 3
healthy_threshold = 2
unhealthy_threshold = 2
}
}
4. Listener Rule 설정
마지막으로 ALB의 리스너 규칙을 설정합니다:
resource "aws_lb_listener_rule" "asg" {
listener_arn = module.alb.alb_http_listener_arn
priority = 100
condition {
path_pattern {
values = ["*"]
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.asg.arn
}
}
모듈 조합의 장점
이렇게 모듈을 조합하여 인프라를 구축하면 다음과 같은 장점이 있습니다:
- 재사용성
- 각 모듈은 독립적으로 사용 가능
- 다른 프로젝트에서도 재사용 가능
- 환경 분리
- 환경 변수를 통한 손쉬운 환경 구분
- prod, stage, dev 환경에서 동일한 코드 사용
- 유지보수 용이성
- 각 모듈은 단일 책임만 가짐
- 문제 발생 시 해당 모듈만 수정
- 구성 유연성
- 필요한 설정만 선택적으로 적용
- 기본값 제공으로 최소 설정으로도 작동
실제 사용 방법
이 Hello World 앱 모듈을 사용하려면:
module "hello_world_app" {
source = "./modules/services/hello-world-app"
environment = "prod"
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
min_size = 2
max_size = 4
enable_autoscaling = true
}
3.3 테스트 가능한 모듈
폴더 구조 :
alb-test/
├── go.mod
├── go.sum
├── examples/
│ └── alb/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── test/
└── alb_test.go
수행 명령어 :
cd test
go test -v -timeout 30m
테스트 코드 :
# test/alb_test.go
package test
import (
"fmt"
"testing"
"time"
http_helper "github.com/gruntwork-io/terratest/modules/http-helper"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/require"
)
func TestAlbExample(t *testing.T) {
t.Parallel()
// 고유한 ALB 이름 생성
uniqueID := random.UniqueId()
albName := fmt.Sprintf("test-alb-%s", uniqueID)
terraformOptions := &terraform.Options{
// 테라폼 코드가 있는 디렉토리 경로
TerraformDir: "../examples/alb",
// 테라폼 변수 설정
Vars: map[string]interface{}{
"alb_name": albName,
"environment": "test",
"vpc_cidr": "10.0.0.0/16",
"public_subnets": []string{"10.0.1.0/24", "10.0.2.0/24"},
"private_subnets": []string{"10.0.10.0/24", "10.0.11.0/24"},
"azs": []string{"us-west-2a", "us-west-2b"},
},
// 테라폼 명령어 재시도 설정
MaxRetries: 3,
TimeBetweenRetries: 5 * time.Second,
RetryableTerraformErrors: map[string]string{
"RequestError": "Failed to request AWS",
"ValidationError": "Failed to validate AWS resources",
},
}
// 테스트 종료 시 리소스 정리
defer terraform.Destroy(t, terraformOptions)
// 테라폼 초기화 및 적용
terraform.InitAndApply(t, terraformOptions)
// ALB DNS 이름 가져오기
albDnsName := terraform.Output(t, terraformOptions, "alb_dns_name")
require.NotEmpty(t, albDnsName, "ALB DNS name should not be empty")
// ALB 엔드포인트 URL
url := fmt.Sprintf("http://%s", albDnsName)
// ALB 상태 확인 설정
expectedStatus := 404
expectedBody := "404: page not found"
maxRetries := 30
timeBetweenRetries := 20 * time.Second
// ALB 상태 확인
http_helper.HttpGetWithRetry(
t,
url,
nil, // tls skip verification = false
expectedStatus,
expectedBody,
maxRetries,
timeBetweenRetries,
)
// ALB 태그 확인
tags := terraform.OutputMap(t, terraformOptions, "alb_tags")
require.Equal(t, "test", tags["Environment"], "ALB should have correct environment tag")
require.Equal(t, albName, tags["Name"], "ALB should have correct name tag")
}
3.4 릴리스 가능한 모듈
버전 관리 예시:
# 태그 생성
git tag -a "v0.1.0" -m "Initial release"
git push --follow-tags
# 모듈 사용
module "vpc" {
source = "git::https://github.com/org/modules.git//networking?ref=v0.1.0"
}
3.5 테라폼 모듈 외의 것들
프로비저너 사용
테라폼을 실행할 때 부트스트랩, 구성관리 또는 정리작업을 수행하기 위해 로컬 시스템이나 원격 시스템에서 스크립트를 실행하는데 사용된다.
- local-exec - 로컬 시스에서 스크립트를 수행
- remote-exec - 원격 리소스에서 스크립트를 수행
- chef - 원격 리소스에서 셰프 클리언트 실행
- file - 원격 리소스로 파일 복사
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx"
]
}
}
null_resource 활용
프로비저너는 리소스 내에서만 정의할 수 있지만, 틀정 리소스에 연결하지 않고 프로비저너를 실행하려 할 수 있습니다.
아무것도 생성하지 않는다는 점을 제외하면 일반 테라폼 리소소와 같은 기능을 합니다.
resource "null_resource" "setup" {
depends_on = [aws_instance.web]
provisioner "local-exec" {
command = "./scripts/setup.sh ${aws_instance.web.public_ip}"
}
triggers = {
instance_id = aws_instance.web.id
}
}
결론
프로덕션 수준의 인프라를 구축할 때는:
- 체크리스트를 만들어 필요한 작업 파악
- 작고 재사용 가능한 모듈로 분리
- 테스트 자동화
- 버전 관리와 문서화
- 다양한 도구의 적절한 활용
이러한 방식으로 접근하면 안정적이고 확장 가능한 인프라를 구축할 수 있습니다.
'IaC > terraform' 카테고리의 다른 글
Terraform 코드 테스트 (8) | 2025.02.02 |
---|---|
Terraform Mocks (4) | 2025.02.02 |
테라폼 팁과 요령: 반복문, if문, 배포 및 주의사항 (3) | 2025.01.12 |
Terraform Module (8) | 2025.01.05 |
Terraform State (3) | 2024.12.29 |