관리 메뉴

근묵자흑

프로덕션 수준의 테라폼 코드 본문

IaC/terraform

프로덕션 수준의 테라폼 코드

Luuuuu 2025. 1. 19. 19:50

1. 프로덕션 수준 인프라 구축에 오랜 시간이 걸리는 이유

DevOps 산업의 성숙도

프로덕션 수준 인프라 구축이 오래 걸리는 첫 번째 이유는 DevOps 산업이 아직 초기 단계이기 때문입니다:

  • 클라우드 컴퓨팅, IaC, DevOps 등의 용어는 2000년대 중후반에 등장
  • 테라폼, 도커, 패커, 쿠버네티스는 2010년 중후반에 출시
  • 기술이 아직 충분히 성숙하지 않았고, 경험 많은 전문가가 부족

DevOps 산업의 현재 상태 (2025)

  • 시스템 복잡도 증가
    • 마이크로서비스 아키텍처의 보편화
    • 멀티클라우드/하이브리드 환경 요구 증가
    • 보안 요구사항의 지속적 강화
    • 규제 요구사항의 복잡화
  • 높아진 기대치
    • 제로 다운타임에 대한 요구
    • 즉각적인 확장성 기대
    • 완벽한 보안과 규정 준수
    • 비용 최적화 요구
  • 통합의 어려움
    • 레거시 시스템과의 연동
    • 다양한 도구들 간의 통합
    • 조직간 협업 필요성

"야크 털 깎기" 현상

두 번째 이유는 "야크 털 깎기" 현상입니다:

  • 원래 하고자 했던 작업을 수행하기 전에 해야 하는 하찮고, 겉보기에 관련성이 적어 보이는 사전 작업들
  • 예시: 새 서비스 배포 → VPC 수정 → 네트워크 정책 변경 → 보안 검토 필요

복잡성의 두 가지 유형

  1. 본질적 복잡성(Essential Complexity)
    • 문제 자체가 가진 근본적인 복잡성
    • 피할 수 없는 복잡성
    • 예: 고가용성, 보안 요구사항, 성능 최적화
  2. 우발적 복잡성(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 소형 모듈

대형 모듈의 단점:

  1. 속도가 느림(plan에만 5분 이상)
  2. 안전하지 않음(관리자 권한 필요)
  3. 위험성이 높음
  4. 이해하기 어려움
  5. 리뷰하기 어려움
  6. 테스트하기 어려움

소형 모듈 예시:

# 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는 이렇게 말했습니다:

"한 가지 일을 잘 하는 프로그램을 작성하라. 프로그램이 함께 동작하도록 작성하라."

 

이 철학을 테라폼 모듈에 적용하면, 각 모듈은:

  1. 단일 책임을 가져야 함
  2. 다른 모듈과 쉽게 조합될 수 있어야 함
  3. 재사용이 가능해야 함

함수형 프로그래밍의 영향

모듈 설계에서 중요한 또 다른 원칙은 함수형 프로그래밍의 '부작용 최소화' 원칙입니다. 간단한 예를 들어보겠습니다:

# 단순한 함수들
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
  }
}

모듈 조합의 장점

이렇게 모듈을 조합하여 인프라를 구축하면 다음과 같은 장점이 있습니다:

  1. 재사용성
    • 각 모듈은 독립적으로 사용 가능
    • 다른 프로젝트에서도 재사용 가능
  2. 환경 분리
    • 환경 변수를 통한 손쉬운 환경 구분
    • prod, stage, dev 환경에서 동일한 코드 사용
  3. 유지보수 용이성
    • 각 모듈은 단일 책임만 가짐
    • 문제 발생 시 해당 모듈만 수정
  4. 구성 유연성
    • 필요한 설정만 선택적으로 적용
    • 기본값 제공으로 최소 설정으로도 작동

실제 사용 방법

이 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
  }
}

결론

프로덕션 수준의 인프라를 구축할 때는:

  1. 체크리스트를 만들어 필요한 작업 파악
  2. 작고 재사용 가능한 모듈로 분리
  3. 테스트 자동화
  4. 버전 관리와 문서화
  5. 다양한 도구의 적절한 활용

이러한 방식으로 접근하면 안정적이고 확장 가능한 인프라를 구축할 수 있습니다.

'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