프로덕션 수준의 테라폼 코드
소개
프로덕션 수준의 인프라
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 : 호프스태터의 법칙을 계산에 넣어도 항상 예상한 것보다 더 오래 걸린다.
- 데브옵스 산업이 여전히 석기 시대에 있기 때문 the industry is still in its infancy
- 아직 산업의 초기 단계이며, ‘Cloud Computing, IaC, DevOps, Docker, k8s’ 등 도구의 출현과 기술이 빠르게 변하고 있으면 충분히 성숙되지 않았음
-
데브옵스가 특히 ‘야크 털 깎기’에 취약 The second reason is that DevOps seems to be particularly susceptible to yak shaving - 링크
https://everydayconcepts.io/yak-shaving/
- 야크 털 깎기 : 어떤 목적을 달성하기 위해 원래 목적과 전혀 상관없는 일들을 계속해야 하며 그중 마지막 작업 - 링크
-
수행해야 하는 작업의 체크 리스트가 너무 많다 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:
- modules/cluster/asg-rolling-deploy : A generic, reusable, standalone module for deploying an ASG that can do a zero-downtime, rolling deployment.
- 무중단 롤링 배포를 수행할 수 있으며 ASG를 배포하기 위한 재사용 가능한 일반 독립형 모듈
- modules/networking/alb : A generic, reusable, standalone module for deploying an ALB.
- ALB를 배포하기 위한 재사용 가능한 일반 독립형 모듈
- 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” 앱을 배포하기 위한 모듈
코드 리팩터링
- 5장에서 배포한 webserver-cluster 를 terraform destroy 로 삭제 → asg-rolling-deploy 과 alb 모듈을 조립할 수 있다
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"] }
-
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 }
-
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"] }
-
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
재사용 가능하고 합성 가능한 모듈
- 외부에서 상태를 읽는 대신 입력 매개 변수를 통해 전달하고, 외부에 상태를 쓰는 대신 출력 매개 변수를 통해 계산 결과를 반환합니다.
- 모든 것을 입력 변수를 통해 전달하고 모든 것을 출력 변수를 통해 반환하며 간단한 모듈들을 결합해 더 복잡한 모듈을 만들수 있다.
- 실제 사용 시에는 더 나은 합성과 재사용을 위해 아래 실습 내용 보다 모듈을 더욱 세분화해야 할 수도 있습니다.
코드 리팩터링
- 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개를 추가 : 아래 출력을 사용하여 보안 그룹에 사용자 정의 규칙을 추가하는 등 새로운 동작을 추가할수 있어서 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" }
-
- 아래 출력 변수 2개를 추가 : 아래 출력을 사용하여 보안 그룹에 사용자 정의 규칙을 추가하는 등 새로운 동작을 추가할수 있어서 asg-rolling-deploy 모듈의 재사용성을 높임
마지막 단계
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
위에서 작성한 코드 작동 확인 실습
- 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-*"] } }
-
-
배포 및 확인
# [터미널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
-
삭제
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