1. 반복문
테라폼으로 정의 되지 않은 외부 리소스 혹은 저장된 정보를 테라폼 내에서 참조해야 될때 사용합니다.
Copy data "local_file" "abc" { // 데이터 소스 블록은 data 로 시작,
// 데이터 소스 유형은 `프로바이더이름_리소스 유형`
filename = "${path.module}/abc.txt" // 구성 인수들은 { } 안에 선언
}
A. for each
for each
는 반복을 할때 타입 값을 하나씩 object로 접근하는 의미입니다. count 의 불편한 부분을 보완하고 있습니다.
for each
는 map, set
타입만 허용합니다.
each
데이터 타입은 key, value 와 같은 값을 가지고 있습니다.
Copy resource "local_file" "abc" {
for_each = {
a = "content a"
b = "content b"
}
/// for_each = toset(["Todd", "James", "Alice", "Dottie"]) -> set 일때
/// for_each = tomap({ -> map 일때
// a_group = "eastus"
// another_group = "westus2"
// })
content = each.value
filename = "${path.module}/${each.key}.txt"
}
다음과 같이 for_each 에 값을 할당하고 사용할 수 있습니다.
다음과 같이 console에서 값을 확인 할 수 있습니다.
다음과 같이 데이터 형태가 map 혹은 set이어야 합니다. 리스트 일때 다음과 같은 오류가 생성됩니다.
Copy resource "aws_iam_user" "the-accounts" {
for_each = ["Todd", "James", "Alice", "Dottie"]
name = each.key
}
B. for each vs count 비교
count는 index를 통해서 삭제시에 그전 인덱스들도 삭제되는 문제가 있는데 for each는 그런 문제로부터 자유로운것을 확인 할 수 있습니다.
또한, index로 몇번에 어떤 내용이 있는지 알 수 없습니다.
for_each는 리소스를 맵으로 처리해서 중간의 항목을 안전하게 처리할 수 있어서 count 보다 안전합니다.
count로 리소스를 생성 했을때, index 값을 확인 할 수 있습니다.
Copy resource "local_file" "abc" {
count = 3
content = "This is filename abc${count.index}.txt"
filename = "${path.module}/abc${count.index}.txt"
}
다음과 같이 index_key가 0으로 생성이 됩니다.
하지만 그와 다르게 for_each로 생성하게 되면 key값을 user_id로 가지는것을 확인할 수 있습니다.
Copy provider "aws" {
region = "ap-northeast-2"
}
resource "aws_iam_user" "myiam" {
for_each = toset(var.user_names)
name = each.value
}
variable "user_names" {
description = "Create IAM users with these names"
type = list(string)
default = ["gasida", "akbun", "ssoon"]
}
output "all_users" {
value = aws_iam_user.myiam
}
중간에 값을 추가 했을때, 값을 확인 할 수 있습니다.
C. For Expressions
forexpression 은 동적으로 variable을 참조해서 변환 작업을 진행합니다.
for 을 사용하는 방법은 여러 방법이 있습니다.
Copy variable "names" {
default = ["a", "b", "c"]
}
resource "local_file" "abc" {
content = jsonencode(var.names) # 결과 : ["a", "b", "c"]
filename = "${path.module}/abc.txt"
}
output "file_content" {
value = local_file.abc.content
}
콘솔에 들어가 다음과 같은 방식으로 iteration을 할 수 있습니다.
다음과 같이 간단하게 값들을 iteration 할 수 있습니다.
Copy variable "names" {
type = list(string)
default = ["a", "b"]
}
output "A_upper_value" {
value = [for v in var.names : upper(v)] // value 값만 iteratoin
}
output "B_index_and_value" {
value = [for i, v in var.names : "${i} is ${v}"] // index 와 value 값을 iteration
}
output "C_make_object" {
value = { for v in var.names : v => upper(v) } // value 값을 iteration 후에 object로
}
output "D_with_filter" {
value = [for v in var.names : upper(v) if v != "a"] // value 값을 iteration 하고 if 문으로 필터터
}
해당 access 방법들에 대해서 결과값은 다음과 같다.
실제로 이용되는 for expression을 사용해보자
aws 에서 storage 값을 다음과 같이 postfix를 사용해서 buekct을 생성할 수 있다. (악분님 유튜브)
Copy variable "fruits" {
type = set(string)
default = ["apple", "banana"]
description = "fruit example"
}
variable "postfix" {
type = string
default = "test"
description = "postfix"
}
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_s3_bucket" "mys3bucket" {
for_each = toset([for fruit in var.fruits : format("%s-%s", fruit, var.postfix)])
bucket = "akbun-t101study-${each.key}"
}
다음과 같은 s3 버킷들이 잘 생기는 것을 확인 할 수 있다.
삭제도 하나만 삭제하면 정상적으로 잘되니 한번 실습해보시는것도 좋을 것 같다.
D. dynamic
dynamic 리소스 내부 블록을 동적인 블록으로 생성
어떤 리소스에 대해서 동적으로 리소스를 생성하는 방법으로 동일 리소스를 한번에 생성하는 역할을 한다.
다음과 같이 생성하기 보다 dynamic 으로 생성하면 손쉽게 생성할 수 있다.
resource "provider_resource" "name"
{ name = "some_resource" some_setting { key = a_value } some_setting { key = b_value } some_setting { key = c_value } some_setting { key = d_value } }
resource "provider_resource" "name"
{
name = "some_resource" dynamic "some_setting"
{ for_each = {
a_key = a_value
b_key = b_value
c_key = c_value
d_key = d_value
} content
{ key = some_setting.value } }
}
Copy variable "names" {
default = {
a = "hello a"
b = "hello b"
c = "hello c"
}
}
data "archive_file" "dotfiles" {
type = "zip"
output_path = "${path.module}/dotfiles.zip"
dynamic "source" {
for_each = var.names
content {
content = source.value
filename = "${path.module}/${source.key}.txt"
}
}
}
이런 코드를 응집도가 높은것인데 하나의 프로비저닝 로직이 하나의 블럭에 들어가서 코드의 가시성이 높아진다.
2. 조건문
A. Conditional Expression
Copy # <조건 정의> ? <옳은 경우> : <틀린 경우>
var.a != "" ? var.a : "default-a"
Copy variable "enable_file" {
default = true
}
resource "local_file" "foo" {
count = var.enable_file ? 1 : 0 // 1개가 만들어지면 된다
content = "foo!"
filename = "${path.module}/foo.bar"
}
output "content" {
value = var.enable_file ? local_file.foo[0].content : ""
}
enable_file 값이 true로 파일이 생성되면 된다.
그럼 한번 enable_file을 false로 설정하고 실행해보자
3. 함수
A. 내장 함수
해당 링크로 가보면 다음과 같은 내장 함수들을 제공하고 있다.
Copy terraform console
-----------------
> upper("foo!") // 대문자 변환
"FOO!"
> max(5, 12, 9) // 가장 큰값
12
> lower(local_file.foo.content)
"foo! bar!"
> upper(local_file.foo.content)
"FOO! BAR!"
>
> cidrnetmask("172.16.0.0/12") // 해당 네트워크 관련 마스킹을 뽑아내준다.
"255.240.0.0"
>
> cidrsubnet("1.1.1.0/24", 1, 0)
"1.1.1.0/25"
> cidrsubnet("1.1.1.0/24", 1, 1) // 첫번째 서브넷에서 제일 첫번째 IP를 지정할 수 있다.
"1.1.1.128/25"
> cidrsubnet("1.1.1.0/24", 2, 2)
"1.1.1.128/26"
>
> cidrsubnet("1.1.1.0/24", 2, 0)
"1.1.1.0/26"
> cidrsubnet("1.1.1.0/24", 2, 1)
"1.1.1.64/26"
> cidrsubnet("1.1.1.0/24", 2, 2)
"1.1.1.128/26"
> cidrsubnet("1.1.1.0/24", 2, 3)
"1.1.1.192/26"
>
> cidrsubnets("10.1.0.0/16", 4, 4, 8, 4)
tolist([
"10.1.0.0/20",
"10.1.16.0/20",
"10.1.32.0/24",
"10.1.48.0/20",
])
>
4. 프로비저너
프로비저너는 커맨드와 파일 복사와 같은 역할을 하는데 docker file의 command와 동일하다고 생각하면 된다.
마음이 아프게도 프로비저너로 실행된 결과는 tfstate 파일에 동기화되지 않아서 선언적 보장이 되지 않는다.
만약에 프로비져너로 인한 웹서버가 실행에 실패해도 전혀 알 수 없다.
이때문에 terraform-provider-ansible을 사용해서 ansible을 사용해서 진행하는 것을 권장 한다.
A. local-exec 프로비저너
테라폼이 실행되는 환경에서 수행할 커맨드를 명시합니다.
윈도우와 리눅스 둘다 환경에 맞게 설정해주면 됩니다.
Copy resource "null_resource" "example1" {
provisioner "local-exec" {
command = <<EOF // 필수 값
echo Hello!! > file.txt
echo $ENV >> file.txt
EOF
interpreter = [ "bash" , "-c" ] // Interpreter 지정을 할 수 있습니다.
working_dir = "/tmp" // 커맨드가 돌아가는 지정 폴더 입니다.
environment = {
ENV = "world!!"
}
}
}
다음과 같이 실행하고 file.txt 값을 생성합니다.
B. 원격지 연결
원격지를 정의하고 해당 원격지에서 수행할 커맨드를 명시합니다.
Copy resource "null_resource" "example1" {
connection {
type = "ssh" // 어떤 connection을 사용할지
user = "root" // 어떤 user 로 로그인 할지
password = var.root_password
host = var.host // 대상 호스트
}
provisioner "file" {
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"
}
provisioner "file" { // 해당 프로비저너에서만으로 connection을 설정될 수 있습니다.
source = "conf/myapp.conf"
destination = "C:/App/myapp.conf"
connection {
type = "winrm" // winrm도 사용이 가능합니다.
user = "Administrator"
password = var.admin_password
host = var.host
}
}
}
연결 인수에대해서는 다음 링크에서 확인할 수 있습니다.
C. file 프로비저너
테라폼을 실행하는 시스템에서 연결대상으로 파일 또는 디렉토리를 복사하는데 사용합니다.
Copy resource "null_resource" "example1" {
provisioner "file" {
source = "conf/myapp.conf" // terraform 을 실행하는 host의 파일 위치
destination = "/etc/myapp.conf" // provisioning 되는 호스트의 파일을 전달합니다.
}
}
주의점
destination 지정 시 주의해야 할 점은 ssh 연결의 경우 대상 디렉터리가 존재해야 한다.
디렉터리를 대상으로 하는 경우에는 source 경로 형태에 따라 동작에 차이가 있습니다.
Copy provisioner "file" {
content = "ami used: ${self.ami}"
destination = "/tmp/file.log" // content의 내용이 /tmp/file.log 파일로 생성
}
provisioner "file" {
source = "conf/configs.d"
destination = "/etc" // configs.d 디렉터리가 /etc/configs.d 로 업로드
}
provisioner "file" {
source = "apps/app1/"
destination = "D:/IIS/webapp1" //apps/app1 디렉터리 내의 파일들만 D:/IIS/webapp1 디렉터리 내에 업로드
}
D. remote-exec 프로비저너
원격지에서 실행할 커맨드와 스크립틀르 명시합니다.
Copy resource "aws_instance" "web" {
connection {
type = "ssh"
user = "root"
password = var.root_password
host = self.public_ip
}
provisioner "file" {
source = "script.sh"
destination = "/tmp/script.sh"
}
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/script.sh",
"/tmp/script.sh args",
]
}
}
다음과 같이 remote-exec 을 통해 스크립트를 실행할 수 있습니다.
null resource는 아무 작업도 하지 않는 리소스로 의도적으로 선행 리소스를 먼저 프로비저닝을 하고 사후 작업을 지정할 때 사용한다.
A. null_resource
유즈케이스:
모듈, 반복문, 데이터 소스, 로컬 변수 사용
예시 케이스:
웹 서비스는 노출이 필요한 aws_epi가 필요하다.
Copy provider "aws" {
region = "ap-northeast-2"
}
resource "aws_security_group" "instance" {
name = "t101sg"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "example" {
ami = "ami-0c9c942bd7bf113a2"
instance_type = "t2.micro"
subnet_id = "subnet-dbc571b0"
private_ip = "172.31.1.100"
vpc_security_group_ids = [aws_security_group.instance.id]
user_data = <<-EOF
#!/bin/bash
echo "Hello, T101 Study" > index.html
nohup busybox httpd -f -p 80 &
EOF
tags = {
Name = "Single-WebSrv"
}
provisioner "remote-exec" {
inline = [
"echo ${aws_eip.myeip.public_ip}"
]
}
}
resource "aws_eip" "myeip" {
#vpc = true
instance = aws_instance.example.id
associate_with_private_ip = "172.31.1.100"
}
output "public_ip" {
value = aws_instance.example.public_ip
description = "The public IP of the Instance"
}
실행을 시키면 다음과 같은 에러를 마주하게 됩니다.
서로 프로비져닝을 하기위해서 서로의 값이 필요한 cycle이 생성이 되고 이때 null_resource를 통해 극복할 수 있다.
Copy
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_security_group" "instance" {
name = "t101sg"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "example" {
ami = "ami-0c9c942bd7bf113a2"
instance_type = "t2.micro"
subnet_id = "subnet-" // 삭제처리리
private_ip = "172.31.0.100"
key_name = "my-eks.pem"
vpc_security_group_ids = [aws_security_group.instance.id]
user_data = <<-EOF
#!/bin/bash
echo "Hello, T101 Study" > index.html
nohup busybox httpd -f -p 80 &
EOF
tags = {
Name = "Single-WebSrv"
}
}
resource "aws_eip" "myeip" {
#vpc = true
instance = aws_instance.example.id
associate_with_private_ip = "172.31.0.100"
}
resource "null_resource" "echomyeip" {
provisioner "remote-exec" {
connection {
host = aws_eip.myeip.public_ip
type = "ssh"
user = "ubuntu"
private_key = file("/home/simon/my-eks.pem")
#password = "qwe123"
}
inline = [
"echo ${aws_eip.myeip.public_ip}"
]
}
}
output "public_ip" {
value = aws_instance.example.public_ip
description = "The public IP of the Instance"
}
output "eip" {
value = aws_eip.myeip.public_ip
description = "The EIP of the Instance"
}
```
다음과 같이 변경하면 잘 실행되는것을 확인할 수 있다.
다음과 같이 트리거를 생성해서 사용할 수 있다.
Copy resource "null_resource" "foo" {
triggers = {
ec2_id = aws_instance.bar.id // instance의 id가 변경되는 경우 재실행
}
}
null resource가 별도의 프로바이더 구성이 필요하지만 terraform_data는 기본 수명 주기 관리자가 제공되어서 비슷한 유즈케이스에 활용 될 수 있다.
사용 방법
강제 재실행을 위한 trigger replace
상태저장을 위한 input 인수와 저장된 값을 출력하는 output 속성을 제공합니다.
Copy resource "terraform_data" "bootstrap" {
triggers_replace = [
aws_instance.web.id, // null resource 프로바이더 없이 replace시 바로 사용 가능하다.
aws_instance.database.id
]
provisioner "local-exec" {
command = "bootstrap-hosts.sh" // 커맨드 사용 가능하다.
}
}
6. moved 블록
테라폼의 State에 기록되는 리소스 주소의 이름이 변경되면 삭제가 된다!!
moved는 테라폼 state의 대상을 삭제하지 않고 이름을 변경하고 프로비져닝 상태를 유지시키려는 목적이다.
A. moved 블록
로컬파일을 생성하고 moved를 이용해 옮겨보자
Copy resource "local_file" "a" {
content = "foo!"
filename = "${path.module}/foo.bar"
}
output "file_content" {
value = local_file.a.content
}
Copy resource "local_file" "b" {
content = "foo!"
filename = "${path.module}/foo.bar"
}
moved {
from = local_file.a
to = local_file.b
}
output "file_content" {
value = local_file.b.content
}
다음과 같이 삭제 없이 이동만 할 수 있게 되었다.
7. CLI를 위한 시스템 환경 변수
A. 시스템 환경 변수
환경 변수로 로깅 레벨 이나 실행에 대한 옵션을 설정할 수 있습니다.
TF_LOG : 테라폼의 stderr 로그 레벨을 설정합니다.
TF_INPUT : 값을 false 또는 0으로 설정하면 테라폼 실행 시 인수에 -input=false 를 추가한 것으로 실행
TF_VAR_name : TF_VAR_<변수 이름>을 사용하면 입력 시 또는 default로 선언된 변수 값을 대체할수 있습니다. -> 위의 var_enable_file 도 해당 로직을 사용함
TF_DATA_DIR : State 저장 작업 디렉터리별 데이터를 보관하는 위치를 지정할 수 있습니다.
Copy TF_LOG=info terraform plan
// 다음과 같이 로그 레벨을 info 로 변경할 수 있습니다.
Last updated 9 months ago