resource "aws_vpc" "main" {
cidr_block = var.base_cidr_block
}
<BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
# Block body
<IDENTIFIER> = <EXPRESSION> # Argument
}
Terraform은 HCL 언어로 정의하며 기본 문법은 위와 같다. {}로 감싸진 하나의 단위를 블록이라고 하며, <BLOCK TYPE> <BLOCK LABEL> 로 정의한다. 블록은 Argument로 이루어져 있으며, 각 Argument는 IDENTIFIER = EXPRESSION 형태로 정의하며 key-value 형태이다. 본 글은 Terraform에서 사용하는 BLOCK TYPE에 대해 다룬다.
terraform
terraform {
required_providers {
aws = {
version = ">= 2.7.0"
source = "hashicorp/aws"
}
}
}
Terraform이 Infrastructure resource가 아닌 Terraform 자체 설정들을 정의하는 블록이다. Terraform의 state를 저장하는 backend, Terraform version 설정이나 위의 예시와 같이 현재 Terraform 모듈에서 요구되는 provider version 과 같은 설정들을 정의한다.
provider
provider "aws" {
region = "us-east-1"
}
Terraform을 통해 관리할 Cloud 혹은 SaaS 등을 설정하가 위해 사용한다. 위의 예시는 AWS를 사용하기 위한 provider이며, region 을 지정해주었다.
access key, secret key
$ export AWS_ACCESS_KEY_ID="XXXXXXXXXXXXX"
$ export AWS_SECRET_ACCESS_KEY="XXXXXXXXXXXXX"
$ terraform apply
access key와 secret key를 provider에서 설정해줄 수 있지만, Terraform code를 공유하게 되면 해당 access key와 secret key가 외부에 노출될 수 있다. 그래서 공식 문서를 보면 환경 변수 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY를 설정하면 provider에서 인식하여 실행해준다. 이 외에도 profile과 region도 환경 변수로 설정이 가능하다.
resource
resource "aws_instance" "web" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
}
Terraform에서 Infrastructure를 선언하기 위해 사용한다. resource "<RESOURCE TYPE>" "<RESOURCE NAME>" 형태로 정의하며, provider에 따라 사용할 수 있는 resource type이 다르다. 해당 resource 블록 외의 다른 블록에서 사용하려면 <RESOURCE TYPE>.<RESOURCE NAME>.<ATTRIBUTE NAME> 으로 참조한다. 위의 예시로는 aws_instance.web.ami 를 통해 생성한 instance의 ami를 참조할 수 있다.
Argument
블록 안의 ami, instance_type과 같은 데이터들을 arguments라고 하며, resource type에 따라 사용할 수 있는 arguments가 다르다.
Attribute
생성한 resource의 속성 값들이다. terraform registry 문서를 보면 resource 문서들은 Argument Reference와 Attribute Reference로 이루어져 있다.
Attribute는 위와 같이 Argument를 포함하여 export한다.
Meta-Argument
모든 resource에서 공통으로 사용할 수 있는 argument를 Meta-Argument라고 한다.
depends_on | resource의 argument에 포함되지 않아 Terraform이 감지할 수 없는 resource 간의 dependency가 필요한 경우 사용 e.g. EC2 에서 사용되는 S3가 필요한 경우 EC2 resource에 depends_on 추가 |
||
count | 하나의 resource block을 통해 count 개수 만큼 resource instance를 생성 | ||
for_each | count는 숫자 값을 통해 instance를 생성한다면, for_each는 map 또는 set을 통해 instance 생성 | ||
provider | default provider가 아닌 alternate provider를 사용할 경우 명시하기 위해 사용 | ||
lifecycle | resource의 생명 주기를 명시할 경우 사용. create_before_destroy, prevent_destroy, ignore_changes, replace_triggered_by 가 있다. 자세한 사항은 공식 문서 참조 |
data
data "aws_ami" "web" {
filter {
name = "state"
values = ["available"]
}
filter {
name = "tag:Component"
values = ["web"]
}
most_recent = true
}
현재 작업하는 Terraform 코드 밖에서 정의된 데이터들을 가져올 수 있는 기능이다. 위의 예시는 AWS의 AMI를 가져오는 데이터 블록이며, filter를 통해 state 는 available, tag의 Component는 web인 AMI 중 가장 최신 버전을 가져오게 된다.
resource "aws_instance" "web" {
ami = data.aws_ami.web.id
instance_type = "t1.micro"
}
data 블록으로 가져온 AMI를 위와 같이 사용 가능하다.
이 외에도 data 블록은 로컬에서 파일을 읽어 제공하는 기능인 local_file 이나 aws의 IAM policy json을 렌더링 하기 위한 aws_iam_policy_document 기능이 있다. 그 외의 기능은 deprecated 되거나 권장하지 않는 방식이라 언급하지 않았다.
module
module "consul_cluster" {
source = "./modules/aws-consul-cluster"
version = "0.1.0"
vpc_id = module.network.vpc_id
subnet_ids = module.network.subnet_ids
}
module은 terraform에서 resource 설정을 패키징하고 재사용하는 단위이다. library라기 보다는 상속에 더 가깝다.
source를 통해 Terraform 코드의 내부 혹은 Github, S3, Terraform Registry 등에서 모듈을 가져올 수 있다. 참조
Terraform Registry에 Docker Hub와 같은 다양한 샘플 Terraform 모듈들이 있다.
좋은 예제도 많고 테라폼 코드를 어떻게 작성하는 것이 Best Practice일지에 대한 도움을 많이 받았다!
Root Module
구글링을 하다보면 Root Module의 정의에 대해서 다들 다르게 적어두었는데,
Every Terraform configuration has at least one module, known as its root module, which consists of the resources defined in the .tf files in the main working directory.
애초에 공식 문서에서 정의를 애매하게 적어두었다.. 그래서 열심히 docs를 찾아본 결과
A Terraform module is a set of Terraform configuration files in a single directory. Even a simple configuration consisting of a single directory with one or more .tf files is a module. When you run Terraform commands directly from such a directory, it is considered the root module.
라는 글을 볼 수 있었다. 모듈 참조에 상관 없이 현재 Terraform command를 실행하고 있는 작업 디렉토리의 module을 root module이라고 한다. (child module도 child module 안에서 command 를 실행하면 해당 module이 root module)
Child Module
다른 module에게 불린 module을 child 모듈이라고 한다. 위 코드에선 "./modules/aws-consul-cluster"이 child module 이다.
output
output "instance_ip_addr" {
value = aws_instance.server.private_ip
}
Terraform에서 output은 프로그래밍에서의 return과 같은 역할을 한다.
module을 사용해서 Terraform 설정을 가져왔을때, 해당 module의 output에 있는 데이터들에 접근할 수 있다. web_server module에서 instance_ip_addr인 output이 있는 경우 module.web_server.instance_ip_addr을 통해서 접근이 가능하다.
variable
variable "availability_zone_names" {
type = list(string)
default = ["us-west-1a"]
}
output이 return이라면 variable은 parameter와 같은 역할을 한다. variable은 input의 역할, 공식 문서에선 input variables로 정의되어 있다. module에서 사용하는 변수 값들을 정의하며, default 값을 지정할 수 있다. string, number, bool 뿐만 아니라 list, set, map, object, tuple 등의 collection 타입도 지원한다.
$ terraform apply
var.database_password
Enter a value:
default 값을 지정하지 않으면 terraform plan, terraform apply와 같이 Terraform을 실행할 때 값을 입력할 수 있다. 하지만 실행 중엔 변경되지 않으니 주의하자. 위와 같은 형태로 Enter a value: 에 값을 넣어준다.
module "servers" {
source = "./app-cluster"
servers = 5
}
혹은 variable이 있는 module을 참조할 경우 module block에서 값을 할당할 수 있다. 위는 app-cluster module을 불러오면서 app-cluster module에 있는 server variable의 값을 할당하고 있다. 위와 같이 module을 참초할 경우 해당 module의 variable도 모두 값을 할당해주어야 한다.
locals
locals {
instance_ids = concat(aws_instance.blue.*.id, aws_instance.green.*.id)
}
locals {
common_tags = {
Service = local.service_name
Owner = local.owner
}
}
Terraform 코드에서 사용되는 지역 변수이다. 여기서 지역의 범위는 하나의 모듈 단위이다. plan, apply, destroy 등 terraform을 실행할 때 변경되지 않으므로 상수에 더 가깝다. name 과 expression의 형태이며, 위의 예시를 보면 instance_ids가 name이고 concat(aws_instance.blue.*.id, aws_instance.green.*.id)가 expression이다. terraform에서의 expression은 5, "hello"와 같은 단순한 값일 수도 있고 위와 같이 함수를 사용하는 표현식일 수도 있다. expression 종류는 공식 문서 참조
Input variables are like function arguments.
Output values are like function return values.
Local values are like a function's temporary local variables.
variable이 이름이 변수이다 보니, 변수처럼 사용하는 예시들도 종종 볼 수 있는데 위의 공식 문서를 보면 이름도 그렇고 local이 좀 더 프로그래밍에서의 변수에 가깝지 않나 생각된다. 사실 처음 봤을때 필자도 그렇게 생각했고 네이밍을 해시코프가 잘못하지 않았나 생각..