• Home
  • About
    • lahuman photo

      lahuman

      열심히 사는 아저씨

    • Learn More
    • Facebook
    • LinkedIn
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

#7-2 프로덕션 수준의 테라폼 코드

04 Dec 2022

Reading time ~19 minutes

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

소개

프로덕션 수준의 인프라 By production-grade infrastructure

  • 서버, 데이터 저장소, 로드 밸런서, 보안 기능, 모니터링 및 경고 도구, 파이프라인 구축 및 비즈니스 운영에 필요한 기타 모든 기술을 의미 + 이중화 및 장애 대응 가능
  • 필자가 경험한 프로덕션 수준의 인프라를 만드는 프로젝트에 소요되는 대략적인 시간
인프라 유형 예 예상 소요 시간
관리형 서비스 Managed service Amazon RDS 1~2주
스스로 관리하는 분산 시스템 (상태 비저장)    
Self-managed distributed system (stateless) Node.js 앱이 실행되는 ASG 클러스터 2~4
스스로 관리하는 분산 시스템 (상태 저장)    
Self-managed distributed system (stateful) Amazon Elasticsearch cluster 2~4개월
전체 아키텍처 Entire architecture 애플리케이션, 데이터 저장소, 로드 밸런서, 모니터링 등 6~36개월

실제 프로덕션 수준의 인프라를 테라폼으로 구성해본 경험해보신 분들은 대략 얼마정도의 기간이 소요되었나요?

==> 제 경우는 전체 구조를 잡는데 6개월 가량 소요 되었고, 이 또한 3~4번 이상 구조를 변경하면서 진행하였습니다.

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

Why It Takes So Long to Build Production-Grade Infrastructure

DevOps 프로젝트는 다른 유형의 소프트웨어 프로젝트 보다 더 시간이 소요될 수 있다. 아래 법칙에 잘 들어맞는 예이다

  • 호프스태터의 법칙 Hofstadter’s Law : 호프스태터의 법칙을 계산에 넣어도 항상 예상한 것보다 더 오래 걸린다.
    1. 데브옵스 산업이 여전히 석기 시대에 있기 때문 the industry is still in its infancy
    • 아직 산업의 초기 단계이며, ‘Cloud Computing, IaC, DevOps, Docker, k8s’ 등 도구의 출현과 기술이 빠르게 변하고 있으면 충분히 성숙되지 않았음
  1. 데브옵스가 특히 ‘야크 털 깎기’에 취약 The second reason is that DevOps seems to be particularly susceptible to yak shaving - 링크

    https://everydayconcepts.io/yak-shaving/

    • 야크 털 깎기 : 어떤 목적을 달성하기 위해 원래 목적과 전혀 상관없는 일들을 계속해야 하며 그중 마지막 작업 - 링크
  2. 수행해야 하는 작업의 체크 리스트가 너무 많다 long checklist of tasks that you must do to prepare infrastructure for production

    • 문제는 대다수 개발자가 체크 리스트에 있는 대부분의 항목을 알지 못하기 때문에 프로젝트를 평가할 때 중요하고 시간이 많이 걸리는 세부 사항을 잊어버린다

2. 프로덕션 수준 인프라 체크 리스트

프로덕션 환경으로 전환하는 데 필요한 것이 뭐가 있죠? What are the requirements for going to production?

The Production-Grade Infrastructure Checklist

  • 예를 들어 서버나 로드밸런서가 다운되면 어떻게 될까요? 데이터 센터에 문제가 생긴다면?
  • 네트워킹 작업 역시 까다롭다. ‘VPC, VPN, Service Discovery, SSH Access’ 설정 등 모두 맻 개월이 걸릴 수도 있습니다.
  • 그러나 프로젝트 계획과 시간 예측에서 완전히 배제되는 경우가 많습니다.

3. 프로덕션 수준 인프라 모듈

Production-Grade Infrastructure Modules

작업 목록을 확인했으니, 구현하기 위해 재사용 가능한 모듈을 구축하는 모범 사례를 살표보자. 아래 다룰 주제.

  • 소형 모듈 Small modules
  • 합성 가능한 모듈 Composable modules
  • 테스트 가능한 모듈 Testable modules
  • 릴리스 가능한 모듈 Versioned modules
  • 테라폼 모듈 외의 것들 Beyond Terraform modules

3.1 소형 모듈 Small modules

대형 모듈 단점 : 상태 파일 격리에서 알아본 것처럼 ‘개발-스테이징-프로덕션’ 모든 인프라 환경을 단일 파일 또는 단일 모듈로 정의하는 것을 좋지 않을뿐더러 유해한 것으로 간주함

  • 속도가 느림 slow : 모든 인프라가 하나의 모듈에 정의되어 있으면 명령 실행 시 오래 걸림. terraform plan 시 20분 걸리기도 함
  • 안전하지 않음 insecure : 모든 인프라가 하나의 모듈에 정의되어 있으면 어떤 것을 변경하려면 모든 액세스 권한이 필요함.
    • 따라서 모든 사용자가 관리자 권한이 필요하여 최소 권한 원칙 principle of least privilege 에 위배
  • 위험성이 높음 risky : 예를 들어 스테이징 환경에서 프런트엔드 앱을 변경 시 오타나 잘못된 명령으로 프로덕션 데이터베이스를 삭제할 수 있다
  • 이해하기 어려움 understand : 한 곳에 코드가 많을수록 한 사람이 모든 것을 이해하기가 더 어려워짐.
  • 리뷰하기 어려움 review : 수집 줄의 코드로 구성된 모듈을 리뷰하는 것으 쉽지만, 수천 줄의 코드로 구성된 모듈을 리뷰하는 것은 거의 불가능하다.
    • terraform plan 실행 시 오래 거리고, plan 명령의 출력이 수천 줄이며 아무도 코드를 읽으려 하지 않는다.
    • 이 경우 데이터베이스가 삭제될 것임을 나타내는 빨간색 코드를 누구도 발견하지 못할 수도 있습니다.
  • 테스트하기 어려움 test : 인프라 코드 테스트는 어렵다. 다음 장에서 살펴볼 예정

방안 : 소형 모듈로 코드를 작성, 클린 코드 Clean Code 내용

  • 함수의 첫 번째 규칙은 작어야 한다는 것입니다. The first rule of functions is that they should be small.
  • 함수의 두 번째 규칙은 그보다 더 작아야 한다는 것입니다. The second rule of functions is that they should be smaller than that.

  • 위 아키텍처라 2만 줄의 거대한 단일 테라폼 모듈로 정의되었다면, 즉시 코드 스멜 code smell 로 취급해야 합니다.
    • 코드 스멜 : 더 큰 문제를 일으킬 수 있는 코드의 특징 → SonarQube 솔루션 활용 할 수 있음 - 링크
  • 아래 처럼 여러 개의 작은 모듈로 리팩터링하자 : ASG, ALB, App → 코드를 소형 모듈 3개로 리팩터링

Let’s refactor the code accordingly into three smaller modules:

  1. modules/cluster/asg-rolling-deploy : A generic, reusable, standalone module for deploying an ASG that can do a zero-downtime, rolling deployment.
    • 무중단 롤링 배포를 수행할 수 있으며 ASG를 배포하기 위한 재사용 가능한 일반 독립형 모듈
  2. modules/networking/alb : A generic, reusable, standalone module for deploying an ALB.
    • ALB를 배포하기 위한 재사용 가능한 일반 독립형 모듈
  3. modules/services/hello-world-app : A module specifically for deploying the “Hello, World” app, which uses the asg-rolling-deploy and alb modules under the hood.
    • “Hello, World” 앱을 배포하기 위한 모듈

코드 리팩터링

  1. 5장에서 배포한 webserver-cluster 를 terraform destroy 로 삭제 → asg-rolling-deploy 과 alb 모듈을 조립할 수 있다
  2. asg-rolling-deploy 리팩터링 modules/cluster/asg-rolling-deploy 새 폴더를 만들고 module/services/webserver-cluster/main.tf ⇒ modules/cluster/asg-rolling-deploy/main.tf 로 리소스 이동 후 수정
    • small-modules/modules/cluster/asg-rolling-deploy/main.tf - 링크

        terraform {
          required_version = ">= 1.0.0, < 2.0.0"
              
          required_providers {
            aws = {
              source  = "hashicorp/aws"
              version = "~> 4.0"
            }
          }
        }
              
        resource "aws_launch_configuration" "example" {
          image_id        = var.ami
          instance_type   = var.instance_type
          security_groups = [aws_security_group.instance.id]
          user_data       = var.user_data
              
          # Required when using a launch configuration with an auto scaling group.
          lifecycle {
            create_before_destroy = true
            precondition {
              condition     = data.aws_ec2_instance_type.instance.free_tier_eligible
              error_message = "${var.instance_type} is not part of the AWS Free Tier!"
            }
          }
        }
              
        resource "aws_autoscaling_group" "example" {
          name                 = var.cluster_name
          launch_configuration = aws_launch_configuration.example.name
              
          vpc_zone_identifier  = var.subnet_ids
              
          # Configure integrations with a load balancer
          target_group_arns    = var.target_group_arns
          health_check_type    = var.health_check_type
              
          min_size = var.min_size
          max_size = var.max_size
              
          # Use instance refresh to roll out changes to the ASG
          instance_refresh {
            strategy = "Rolling"
            preferences {
              min_healthy_percentage = 50
            }
          }
              
          tag {
            key                 = "Name"
            value               = var.cluster_name
            propagate_at_launch = true
          }
              
          dynamic "tag" {
            for_each = {
              for key, value in var.custom_tags:
              key => upper(value)
              if key != "Name"
            }
              
            content {
              key                 = tag.key
              value               = tag.value
              propagate_at_launch = true
            }
          }
              
          lifecycle {
            postcondition {
              condition     = length(self.availability_zones) > 1
              error_message = "You must use more than one AZ for high availability!"
            }
          }
              
        }
              
        resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {
          count = var.enable_autoscaling ? 1 : 0
              
          scheduled_action_name  = "${var.cluster_name}-scale-out-during-business-hours"
          min_size               = 2
          max_size               = 10
          desired_capacity       = 10
          recurrence             = "0 9 * * *"
          autoscaling_group_name = aws_autoscaling_group.example.name
        }
              
        resource "aws_autoscaling_schedule" "scale_in_at_night" {
          count = var.enable_autoscaling ? 1 : 0
              
          scheduled_action_name  = "${var.cluster_name}-scale-in-at-night"
          min_size               = 2
          max_size               = 10
          desired_capacity       = 2
          recurrence             = "0 17 * * *"
          autoscaling_group_name = aws_autoscaling_group.example.name
        }
              
        resource "aws_security_group" "instance" {
          name = "${var.cluster_name}-instance"
        }
              
        resource "aws_security_group_rule" "allow_server_http_inbound" {
          type              = "ingress"
          security_group_id = aws_security_group.instance.id
              
          from_port   = var.server_port
          to_port     = var.server_port
          protocol    = local.tcp_protocol
          cidr_blocks = local.all_ips
        }
              
        resource "aws_cloudwatch_metric_alarm" "high_cpu_utilization" {
          alarm_name  = "${var.cluster_name}-high-cpu-utilization"
          namespace   = "AWS/EC2"
          metric_name = "CPUUtilization"
              
          dimensions = {
            AutoScalingGroupName = aws_autoscaling_group.example.name
          }
              
          comparison_operator = "GreaterThanThreshold"
          evaluation_periods  = 1
          period              = 300
          statistic           = "Average"
          threshold           = 90
          unit                = "Percent"
        }
              
        resource "aws_cloudwatch_metric_alarm" "low_cpu_credit_balance" {
          count = format("%.1s", var.instance_type) == "t" ? 1 : 0
              
          alarm_name  = "${var.cluster_name}-low-cpu-credit-balance"
          namespace   = "AWS/EC2"
          metric_name = "CPUCreditBalance"
              
          dimensions = {
            AutoScalingGroupName = aws_autoscaling_group.example.name
          }
              
          comparison_operator = "LessThanThreshold"
          evaluation_periods  = 1
          period              = 300
          statistic           = "Minimum"
          threshold           = 10
          unit                = "Count"
        }
              
        data "aws_ec2_instance_type" "instance" {
          instance_type = var.instance_type
        }
              
        locals {
          tcp_protocol = "tcp"
          all_ips      = ["0.0.0.0/0"]
        }
      
  3. asg-rolling-deploy 리팩터링 이제 변수를 module/services/webserver-cluster/variables.tf ⇒ modules/cluster/asg-rolling-deploy/variables.tf 로 이동
    • small-modules/modules/cluster/asg-rolling-deploy/variables.tf - 링크

        # ---------------------------------------------------------------------------------------------------------------------
        # REQUIRED PARAMETERS
        # You must provide a value for each of these parameters.
        # ---------------------------------------------------------------------------------------------------------------------
              
        variable "cluster_name" {
          description = "The name to use for all the cluster resources"
          type        = string
        }
              
        variable "ami" {
          description = "The AMI to run in the cluster"
          type        = string
        }
              
        variable "instance_type" {
          description = "The type of EC2 Instances to run (e.g. t2.micro)"
          type        = string
              
          validation {
            condition     = contains(["t2.micro", "t3.micro"], var.instance_type)
            error_message = "Only free tier is allowed: t2.micro | t3.micro."
          }
        }
              
        variable "min_size" {
          description = "The minimum number of EC2 Instances in the ASG"
          type        = number
              
          validation {
            condition     = var.min_size > 0
            error_message = "ASGs can't be empty or we'll have an outage!"
          }
              
          validation {
            condition     = var.min_size <= 10
            error_message = "ASGs must have 10 or fewer instances to keep costs down."
          }
        }
              
        variable "max_size" {
          description = "The maximum number of EC2 Instances in the ASG"
          type        = number
              
          validation {
            condition     = var.max_size > 0
            error_message = "ASGs can't be empty or we'll have an outage!"
          }
              
          validation {
            condition     = var.max_size <= 10
            error_message = "ASGs must have 10 or fewer instances to keep costs down."
          }
        }
              
        variable "subnet_ids" {
          description = "The subnet IDs to deploy to"
          type        = list(string)
        }
              
        variable "enable_autoscaling" {
          description = "If set to true, enable auto scaling"
          type        = bool
        }
              
        # ---------------------------------------------------------------------------------------------------------------------
        # OPTIONAL PARAMETERS
        # These parameters have reasonable defaults.
        # ---------------------------------------------------------------------------------------------------------------------
              
        variable "target_group_arns" {
          description = "The ARNs of ELB target groups in which to register Instances"
          type        = list(string)
          default     = []
        }
              
        variable "health_check_type" {
          description = "The type of health check to perform. Must be one of: EC2, ELB."
          type        = string
          default     = "EC2"
              
          validation {
            condition     = contains(["EC2", "ELB"], var.health_check_type)
            error_message = "The health_check_type must be one of: EC2 | ELB."
          }
        }
              
        variable "user_data" {
          description = "The User Data script to run in each Instance at boot"
          type        = string
          default     = null
        }
              
        variable "custom_tags" {
          description = "Custom tags to set on the Instances in the ASG"
          type        = map(string)
          default     = {}
        }
              
        variable "server_port" {
          description = "The port the server will use for HTTP requests"
          type        = number
          default     = 8080
        }
      
  4. alb 리팩터링 modules/networking/alb 새 폴더 만들고, module/services/webserver-cluster/main.tf ⇒ modules/networking/alb/main.tf 로 리소스 이동 후 수정
    • small-modules/modules/networking/alb/main.tf

        terraform {
          required_version = ">= 1.0.0, < 2.0.0"
              
          required_providers {
            aws = {
              source  = "hashicorp/aws"
              version = "~> 4.0"
            }
          }
        }
              
        resource "aws_lb" "example" {
          name               = var.alb_name
          load_balancer_type = "application"
              
          subnets            = var.subnet_ids
              
          security_groups    = [aws_security_group.alb.id]
        }
              
        resource "aws_lb_listener" "http" {
          load_balancer_arn = aws_lb.example.arn
          port              = local.http_port
          protocol          = "HTTP"
              
          # By default, return a simple 404 page
          default_action {
            type = "fixed-response"
              
            fixed_response {
              content_type = "text/plain"
              message_body = "404: page not found"
              status_code  = 404
            }
          }
        }
              
        resource "aws_security_group" "alb" {
          name = var.alb_name
        }
              
        resource "aws_security_group_rule" "allow_http_inbound" {
          type              = "ingress"
          security_group_id = aws_security_group.alb.id
              
          from_port   = local.http_port
          to_port     = local.http_port
          protocol    = local.tcp_protocol
          cidr_blocks = local.all_ips
        }
              
        resource "aws_security_group_rule" "allow_all_outbound" {
          type              = "egress"
          security_group_id = aws_security_group.alb.id
              
          from_port   = local.any_port
          to_port     = local.any_port
          protocol    = local.any_protocol
          cidr_blocks = local.all_ips
        }
              
        locals {
          http_port    = 80
          any_port     = 0
          any_protocol = "-1"
          tcp_protocol = "tcp"
          all_ips      = ["0.0.0.0/0"]
        }
      
  5. alb 리팩터링 이제 변수 코드 파일 생성 modules/networking/alb/variables.tf
    • small-modules/modules/networking/alb/variables.tf

        # ---------------------------------------------------------------------------------------------------------------------
        # REQUIRED PARAMETERS
        # You must provide a value for each of these parameters.
        # ---------------------------------------------------------------------------------------------------------------------
              
        variable "alb_name" {
          description = "The name to use for this ALB"
          type        = string
        }
              
        variable "subnet_ids" {
          description = "The subnet IDs to deploy to"
          type        = list(string)
        }
      

3.2 합성 가능한 모듈 Composable modules

재사용 가능하고 합성 가능한 모듈

  • 외부에서 상태를 읽는 대신 입력 매개 변수를 통해 전달하고, 외부에 상태를 쓰는 대신 출력 매개 변수를 통해 계산 결과를 반환합니다.
  • 모든 것을 입력 변수를 통해 전달하고 모든 것을 출력 변수를 통해 반환하며 간단한 모듈들을 결합해 더 복잡한 모듈을 만들수 있다.
  • 실제 사용 시에는 더 나은 합성과 재사용을 위해 아래 실습 내용 보다 모듈을 더욱 세분화해야 할 수도 있습니다.

코드 리팩터링

  1. modules/cluster/asg-rolling-deploy/variables.tf 에 새로운 입력 변수 4개를 추가
    • small-modules/modules/cluster/asg-rolling-deploy/variables.tf - 링크

        # ---------------------------------------------------------------------------------------------------------------------
        # REQUIRED PARAMETERS
        # You must provide a value for each of these parameters.
        # ---------------------------------------------------------------------------------------------------------------------
              
        variable "cluster_name" {
          description = "The name to use for all the cluster resources"
          type        = string
        }
              
        variable "ami" {
          description = "The AMI to run in the cluster"
          type        = string
        }
              
        variable "instance_type" {
          description = "The type of EC2 Instances to run (e.g. t2.micro)"
          type        = string
              
          validation {
            condition     = contains(["t2.micro", "t3.micro"], var.instance_type)
            error_message = "Only free tier is allowed: t2.micro | t3.micro."
          }
        }
              
        variable "min_size" {
          description = "The minimum number of EC2 Instances in the ASG"
          type        = number
              
          validation {
            condition     = var.min_size > 0
            error_message = "ASGs can't be empty or we'll have an outage!"
          }
              
          validation {
            condition     = var.min_size <= 10
            error_message = "ASGs must have 10 or fewer instances to keep costs down."
          }
        }
              
        variable "max_size" {
          description = "The maximum number of EC2 Instances in the ASG"
          type        = number
              
          validation {
            condition     = var.max_size > 0
            error_message = "ASGs can't be empty or we'll have an outage!"
          }
              
          validation {
            condition     = var.max_size <= 10
            error_message = "ASGs must have 10 or fewer instances to keep costs down."
          }
        }
              
        variable "subnet_ids" {
          description = "The subnet IDs to deploy to"
          type        = list(string)
        }
              
        variable "enable_autoscaling" {
          description = "If set to true, enable auto scaling"
          type        = bool
        }
              
        # ---------------------------------------------------------------------------------------------------------------------
        # OPTIONAL PARAMETERS
        # These parameters have reasonable defaults.
        # ---------------------------------------------------------------------------------------------------------------------
              
        variable "target_group_arns" {
          description = "The ARNs of ELB target groups in which to register Instances"
          type        = list(string)
          default     = []
        }
              
        variable "health_check_type" {
          description = "The type of health check to perform. Must be one of: EC2, ELB."
          type        = string
          default     = "EC2"
              
          validation {
            condition     = contains(["EC2", "ELB"], var.health_check_type)
            error_message = "The health_check_type must be one of: EC2 | ELB."
          }
        }
              
        variable "user_data" {
          description = "The User Data script to run in each Instance at boot"
          type        = string
          default     = null
        }
              
        variable "custom_tags" {
          description = "Custom tags to set on the Instances in the ASG"
          type        = map(string)
          default     = {}
        }
              
        variable "server_port" {
          description = "The port the server will use for HTTP requests"
          type        = number
          default     = 8080
        }
      
    • subnet_ids 는 asg-rolling-deploy 모듈을 배포할 서브넷을 지정
      • webserver-cluster 모듈(5장)은 기본 VPC 및 기본 서브넷에 배포되도록 하드 코딩되어 있지만, subnet_ids 변수를 노출해 어떤 VPC나 서브넷에서도 사용할 수 있게 허용
    • target_group_arns 과 health_check_type 변수는 ASG를 로드 밸런서와 통합하는 방식을 구성
      • webserver-cluster 모듈에는 ALB가 내장되어 있지만 asg-rolling-deploy 모듈은 일반 모듈이므로 로드 밸런서 설정을 입력 변수로 노출하면 다양한 상황에서 ASG를 사용 할 수 있다.
      • 예를 들어 로드 밸런서가 없거나 하나의 ALB 또는 여러 NLB를 사용하는 상황에서 ASG를 사용할 수 있음
    • 위 3가지 입력 변수를 module/services/webserver-cluster/main.tf ⇒ modules/cluster/asg-rolling-deploy/main.tf의 aws_autoscaling_group 리소스에 전달하여 ALB와 같은 이전에 하드 코딩된 리소스 및 데이터 소스를 참조하는 설정으로 대체합니다.
      • small-modules/modules/cluster/asg-rolling-deploy/main.tf - 링크

          ...
          resource "aws_autoscaling_group" "example" {
            name                 = var.cluster_name
            launch_configuration = aws_launch_configuration.example.name
                    
            vpc_zone_identifier  = var.subnet_ids
                    
            # Configure integrations with a load balancer
            target_group_arns    = var.target_group_arns
            health_check_type    = var.health_check_type
                    
            min_size = var.min_size
            max_size = var.max_size
                    
            # Use instance refresh to roll out changes to the ASG
            instance_refresh {
              strategy = "Rolling"
              preferences {
                min_healthy_percentage = 50
              }
            }
                    
            tag {
              key                 = "Name"
              value               = var.cluster_name
              propagate_at_launch = true
            }
                    
            dynamic "tag" {
              for_each = {
                for key, value in var.custom_tags:
                key => upper(value)
                if key != "Name"
              }
                    
              content {
                key                 = tag.key
                value               = tag.value
                propagate_at_launch = true
              }
            }
                    
            lifecycle {
              postcondition {
                condition     = length(self.availability_zones) > 1
                error_message = "You must use more than one AZ for high availability!"
              }
            }
                    
          }
          ...
        
    • user_data 는 사용자 데이터 스크립트를 전달
      • webserver-cluster 모듈에 하드 코딩된 사용자 데이터 스크립크는 ‘Hello, World’ 앱 배포에만 사용할 수 있습니다.
      • 그러나 사용자 데이터 스크립트를 입력 변수로 사용하면 asg-rolling-deploy 모듈은 ASG 전체에 어떤 앱이든 배포할 수 있습니다.
      • 이 user_data 변수를 아래 aws_launch_configuration 리소스로 전달합니다.
      • small-modules/modules/cluster/asg-rolling-deploy/main.tf - 링크

          ...
          resource "aws_launch_configuration" "example" {
            image_id        = var.ami
            instance_type   = var.instance_type
            security_groups = [aws_security_group.instance.id]
            user_data       = var.user_data
                    
            # Required when using a launch configuration with an auto scaling group.
            lifecycle {
              create_before_destroy = true
              precondition {
                condition     = data.aws_ec2_instance_type.instance.free_tier_eligible
                error_message = "${var.instance_type} is not part of the AWS Free Tier!"
              }
            }
          }
          ...
        
  2. 출력 변수 추가
    • 아래 출력 변수 2개를 추가 : 아래 출력을 사용하여 보안 그룹에 사용자 정의 규칙을 추가하는 등 새로운 동작을 추가할수 있어서 asg-rolling-deploy 모듈의 재사용성을 높임
      • small-modules/modules/cluster/asg-rolling-deploy/outputs.tf

          output "**asg_name**" {
            value       = aws_autoscaling_group.example.name
            description = "The name of the Auto Scaling Group"
          }
                    
          output "instance_security_group_id" {
            value       = aws_security_group.instance.id
            description = "The ID of the EC2 Instance Security Group"
          }
        
    • 비슷하게 modules/networking/alb/outputs.tf 에도 출력 변수를 추가
      • small-modules/modules/networking/alb/outputs.tf

          output "alb_dns_name" {
            value       = aws_lb.example.dns_name
            description = "The domain name of the load balancer"
          }
                    
          output "alb_http_listener_arn" {
            value       = aws_lb_listener.http.arn
            description = "The ARN of the HTTP listener"
          }
                    
          output "alb_security_group_id" {
            value       = aws_security_group.alb.id
            description = "The ALB Security Group ID"
          }
        
  3. 마지막 단계 asg-rolling-deploy 및 alb 모듈을 사용하여 webserver-cluster 모듈을 ‘Hello, World’ 앱을 배포할 수 있는 hello-world-app 모듈로 변환하는 것
    • 이를 위해 module/services/webserver-cluster 이름을 → module/services/hello-world-app 로 변경
    • 아래 리소스와 데이터 소스만 남겨 두자
      • module/services/hello-world-app/main.tf

          ...
          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"
                      
            custom_tags = var.custom_tags
          }
                    
          module "alb" {
            source = "../../networking/alb"
                    
            alb_name   = "hello-world-${var.environment}"
            subnet_ids = data.aws_subnets.default.ids
          }
                    
          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
            }
          }
                    
          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
            }
          }
                    
          data "terraform_remote_state" "db" {
            backend = "s3"
                    
            config = {
              bucket = var.db_remote_state_bucket
              key    = var.db_remote_state_key
              region = "us-east-2"
            }
          }
                    
          data "aws_vpc" "default" {
            default = true
          }
                    
          data "aws_subnets" "default" {
            filter {
              name   = "vpc-id"
              values = [data.aws_vpc.default.id]
            }
          }
        
    • modules/services/hello-world-app/variables.tf 에 아래 변수를 추가
      • modules/services/hello-world-app/variables.tf

          # ---------------------------------------------------------------------------------------------------------------------
          # REQUIRED PARAMETERS
          # You must provide a value for each of these parameters.
          # ---------------------------------------------------------------------------------------------------------------------
                    
          variable "environment" {
            description = "The name of the environment we're deploying to"
            type        = string
          }
          ...
        
    • 그리고 이전에 작성한 asg-rolling-deploy 모듈을 hello-world-app 모듈에 추가합니다
    • 그리고 이전에 생성한 alb 모듈을 hello-world-app 모듈에 추가합니다
    • environment 입력 변수를 사용해 모든 리소스의 네임스페이스가 hello-world-stage, hello-world-prod 같은 환경을 기준으로 지정되도록 명명 규칙을 시행합니다.
    • 이 코드는 또한 이전에 추가한 새로운 subnet_ids, target_group_arns, health_check_type, and user_data 변수를 적절한 값으로 설정합니다.
    • 다음으로 이 앱에 대한 ALB 대상 그룹 및 리스너 규칙을 구성합니다.
    • name 에서 environment 를 사용하도록 module/services/hello-world-app/main.tf에서 aws_lb_target_group 리소스를 업데이트합니다.
      • module/services/hello-world-app/main.tf

          ...
          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
            }
          }
          ...
        
    • 이제 aws_lb_listener_rule 리소스의 listener_arn 매개 변수가 ALB 모듈의 alb_http_listener_arn 출력을 가리키도록 업데이트합니다.
      • module/services/hello-world-app/main.tf

          ...
                    
          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
            }
          }
          ...
        
    • 마지막으로 asg-rolling-deploy 및 alb 모듈의 중요한 출력을 hello-world-app 모듈의 출력으로 전달합니다.
      • module/services/hello-world-app/outputs.tf

          output "alb_dns_name" {
            value       = module.alb.alb_dns_name
            description = "The domain name of the load balancer"
          }
                    
          output "asg_name" {
            value       = module.asg.asg_name
            description = "The name of the Auto Scaling Group"
          }
                    
          output "instance_security_group_id" {
            value       = module.asg.instance_security_group_id
            description = "The ID of the EC2 Instance Security Group"
          }
        

3.3 테스트 가능한 모듈 Testable modules

위에서 작성한 코드 작동 확인 실습

  1. examples/asg/main.tf 작성 : asg-rolling-deploy 모듈을 사용하여 크기가 1인 ASG를 배포
    • examples/asg/main.tf

        terraform {
          required_version = ">= 1.0.0, < 2.0.0"
              
          required_providers {
            aws = {
              source  = "hashicorp/aws"
              version = "~> 4.0"
            }
          }
        }
              
        provider "aws" {
          region = "us-east-2"
        }
              
        module "asg" {
          source = "../../modules/cluster/asg-rolling-deploy"
              
          cluster_name  = var.cluster_name
              
          ami           = data.aws_ami.ubuntu.id
          instance_type = "t2.micro"
              
          min_size           = 1
          max_size           = 1
          enable_autoscaling = false
              
          subnet_ids        = data.aws_subnets.default.ids
        }
              
        data "aws_vpc" "default" {
          default = true
        }
              
        data "aws_subnets" "default" {
          filter {
            name   = "vpc-id"
            values = [data.aws_vpc.default.id]
          }
        }
              
        data "aws_ami" "ubuntu" {
          most_recent = true
          owners      = ["099720109477"] # Canonical
              
          filter {
            name   = "name"
            values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
          }
        }
      
  2. 배포 및 확인

     # [터미널1] RDS 생성 모니터링
     while true; do **aws rds describe-db-instances** --query "*[].[Endpoint.Address,Endpoint.Port,MasterUsername]" --output text  ; echo "------------------------------" ; sleep 1; done
        
     # RDS 배포
     cd ~/terraform-up-and-running-code/code/terraform/08-production-grade-infrastructure/small-modules/examples/**mysql**
        
     # 환경변수에 지정
     export TF_VAR_db_username='cloudneta'
     export TF_VAR_db_password='cloudnetaQ!'
        
     terraform init && terraform plan
     terraform apply -auto-approve
        
     # [터미널2]
     while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done
        
     # 배포
     cd ~/terraform-up-and-running-code/code/terraform/08-production-grade-infrastructure/small-modules/examples/**asg**
     terraform init
     terraform plan
     terraform apply -auto-approve
        
     # ALB 배포
     cd ~/terraform-up-and-running-code/code/terraform/08-production-grade-infrastructure/small-modules/examples/**alb**
     terraform init && terraform plan
     terraform apply -auto-approve
        
     # ALB DNS주소로 curl 접속 확인 
     ALBDNS=$(terraform output -raw alb_dns_name)
     while true; do curl --connect-timeout 1  http://$ALBDNS ; echo; echo "------------------------------"; date; sleep 1; done
     curl -s http://$ALBDNS
    
  3. 삭제

     cd ~/terraform-up-and-running-code/code/terraform/08-production-grade-infrastructure/small-modules/examples/alb
     terraform destroy -auto-approve
     cd ~/terraform-up-and-running-code/code/terraform/08-production-grade-infrastructure/small-modules/examples/asg
     terraform destroy -auto-approve
     cd ~/terraform-up-and-running-code/code/terraform/08-production-grade-infrastructure/small-modules/examples/mysql
     terraform destroy -auto-approve
    

3.4 릴리스 가능한 모듈 Versioned modules

Validations : 테라폼 0.13 validation blocks 은 입력 변수를 체크

  • 아래는 instance_type 으로 t2.micro 와 t3.micro 만 사용할 수 있게 설정

      variable "instance_type" {
        description = "The type of EC2 Instances to run (e.g. t2.micro)"
        type        = string
        
        validation {
          condition     = contains(["t2.micro", "t3.micro"], var.instance_type)
          error_message = "Only free tier is allowed: t2.micro | t3.micro."
        }
      }
    
  • 체크 검증 확인

      $ terraform apply -var instance_type="m4.large"
      │ Error: Invalid value for variable
      │
      │   on main.tf line 17:
      │    1: variable "instance_type" {
      │     ├────────────────
      │     │ var.instance_type is "m4.large"
      │
      │ Only free tier is allowed: t2.micro | t3.micro.
      │
      │ This was checked by the validation rule at main.tf:21,3-13.
    

버전 Versioned Modules : 오늘 실행하던, 3년 후에 실행하던 동일한 결과를 얻을 수 있어야 한다!

  • 두 가지 유형 버전 고려 : Versioning of the module’s dependencies , Versioning of the module itself
  • Versioning of the module’s dependencies : Terraform core, Providers, Modules
    • Terraform core : The version of the terraform binary you depend on → 테라폼 실행 파일 버전
    • Providers : The version of each provider your code depends on, such as the aws provider → 프로바이더 버전
    • Modules : The version of each module you depend on that are pulled in via module blocks → 모듈 버전
  • Terraform core 고정 : required_version 사용

      terraform {
        # Require any 1.x version of Terraform
        required_version = ">= 1.0.0, < 2.0.0"
      }
    
    • Production 환경에서는 버전을 직접 지정을 권장
      terraform {
        # Require Terraform at exactly version 1.2.3
        required_version = "1.2.3"
      }
    
  • Providers 고정 : required_providers 아래 version 사용

      terraform {
        required_version = ">= 1.0.0, < 2.0.0"
        
        required_providers {
          aws = {
            source  = "hashicorp/aws"
            version = "~> 4.0"
          }
        }
      }
    
  • Modules 고정 : 시맨틱 버전 관리와 함께 깃 태그를 사용

3.5 테라폼 모듈 외의 것들 Beyond Terraform modules

  • Provisioners
  • Provisioners with null_resource
  • External data source


terraformstudy Share Tweet +1