본 시리즈는 가시다님의 T101(테라폼으로 시작하는 IaC) 3기 진행 내용입니다. (가시다님 노션)
도서 정보
https://www.yes24.com/Product/Goods/119179333
실습 코드
https://github.com/terraform101
목차
1. 기본 사용 - 조건문, 함수, 프로비저너
2. 기본 사용 - null_resource와 terraform_data , moved 블록, CLI를 위한 시스템 환경 변수
3. 프로바이더
1. 조건문
테라폼에서의 조건식은 3항 연산자 형태를 갖는다. 조건은 true 또는 false로 확인되는 모든 표현식을 사용할 수 있다 - 링크
- 일반적으로 비교, 논리 연산자를 사용해 조건을 확인한다.
- 조건식은 ? 기호를 기준으로 왼쪽은 조건이며, 오른쪽은 : 기호를 기준으로 왼쪽이 조건에 대해 true가 반환되는 경우이고 오른쪽이 false가 반환되는 경우다.
- 다음의 예에서 var.a가 빈 문자열이 아니라면 var.a를 나타내지만, 비어 있을 때는 “default-a”를 반환한다
# <조건 정의> ? <옳은 경우> : <틀린 경우>
var.a != "" ? var.a : "default-a"
- 조건식의 각 조건은 비교 대상의 형태가 다르면 테라폼 실행 시 조건 비교를 위해 형태를 추론하여 자동으로 변환하는데, 명시적인 형태 작성을 권장
# 조건식 형태 권장 사항
var.example ? 12 : "hello" # 비권장
var.example ? "12" : "hello" # 권장
var.example ? tostring(12) : "hello" # 권장
- 조건식은 단순히 특정 속성에 대한 정의, 로컬 변수에 대한 재정의, 출력 값에 대한 조건 정의 뿐만 아니라 리소스 생성 여부에 응용할 수 있다. count에 조건식을 결합한 경우 다음과 같이 특정 조건에 따라 리소스 생성 여부를 선택할 수 있다.
- main.tf 파일 내용
variable "enable_file" {
default = true
}
resource "local_file" "foo" {
count = var.enable_file ? 1 : 0
content = "foo!"
filename = "${path.module}/foo.bar"
}
output "content" {
value = var.enable_file ? local_file.foo[0].content : ""
}
- 실행
# 변수 우선순위3 : 환경 변수 (TF_VAR 변수 이름)
export TF_VAR_enable_file=false
export | grep TF_VAR_enable_file
#
terraform init && terraform plan && terraform apply -auto-approve
terraform state list
# 환경 변수 삭제
unset TF_VAR_enable_file
export | grep TF_VAR_enable_file
# 재실행
terraform plan && terraform apply -auto-approve
terraform state list
#
echo "local_file.foo[0]" | terraform console
echo "local_file.foo[0].content" | terraform console
TF_VAR_enable_file 을 false로 지정한 경우
content값이 없는 것 확인
TF_VAR_enable_file 값 초기화 후 재실행
- 이렇게되면 main.tf에서 정의한 입력 변수의 기본 값인 true로 동작하게 된다.
content값이 foo! 로 잘 들어간 것 확인
2. 함수
- 테라폼은 프로그래밍 언어적인 특성을 가지고 있어서, 값의 유형을 변경하거나 조합할 수 있는 내장 함수를 사용 할 수 있다 - 링크
- 단, 내장된 함수 외에 사용자가 구현하는 별도의 사용자 정의 함수를 지원하지는 않는다.
- 함수 종류에는 숫자, 문자열, 컬렉션, 인코딩, 파일 시스템, 날짜/시간, 해시/암호화, IP 네트워크, 유형 변환이 있다.
- 테라폼 코드에 함수를 적용하면 변수, 리소스 속성, 데이터 소스 속성, 출력 값 표현 시 작업을 동적이고 효과적으로 수행할 수 있다.
- 실습을 위해서 3.11 디렉터리를 신규 생성 후 열기 → main.tf 파일 생성
resource "local_file" "foo" {
content = upper("foo! bar!")
filename = "${path.module}/foo.bar"
}
-
- 실행
#
terraform init && terraform plan && terraform apply -auto-approve
cat foo.bar ; echo
# 내장 함수 간단 사용
terraform console
>
-----------------
upper("foo!")
max(5, 12, 9)
lower(local_file.foo.content)
upper(local_file.foo.content)
cidrnetmask("172.16.0.0/12")
exit
-----------------
[추천실습] count + 함수 cidrhost 로 EC2의 ENI 에 10개의 ip를 장착
cidrhost 함수 설명 - 링크
- 주어진 cidr 블럭에서 인덱스에 해당하는 IP 주소를 반환
EC2 중에 ENI를 10개 장착할 수 있는 인스턴스 유형을 찾자
우선 여기에 해당되는 인스턴스 유형을 쓰게되면 기본적으로 CPU 사이즈까지 높기 때문에 Instance Limit에 걸리게 된다.(따로 늘리지 않았다면)
EC2 인스턴스 유형 - 하드웨어 사양 (링크)
가장 적합한 인스턴스 유형은 32vCPU이며, 8 ENI 할당을 제공하는 m5.8xlarge이 선정되었다.
기본적으로 1개의 ENI는 생성되기에 aws_network_interface는 7개로 하여 총 8개의 ENI를 할당하는 것으로 한다.
자 그럼 코드를 작성해보면 아래와 같이 대략 만들어진다.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "primary" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
resource "aws_instance" "netcloudy_ec2" {
instance_type = "m5.8xlarge"
ami = "ami-0c9c942bd7bf113a2"
subnet_id = aws_subnet.primary.id
}
resource "aws_network_interface" "netcloudy_eni" {
count = 7
subnet_id = aws_subnet.primary.id
private_ips = [
cidrhost("10.0.1.0/24", (count.index))
]
attachment {
instance = aws_instance.netcloudy_ec2.id
device_index = count.index + 1
}
}
instance_type은 m5.8xlarge
eni count는 7로 지정
이러면 될까?
안된다면 이유는?
기본적으로 cidrhost에 첫 인자는 CIDR 범위이고 두 번째 인자는 그 CIDR 범위에서 설정할 IP 주소를 반환하게 된다.
이말이 뭐냐하면 위에 예시를 기준으로 한다면 아래와 같다.
count[0] = 10.0.1.0
count[1] = 10.0.1.1
count[2] = 10.0.1.2
count[3] = 10.0.1.3
이게 과연 만들어질까?
당연히 안만들어진다.
AWS에서는 VPC와 Subnet을 생성하면 기본적으로 3번까지의 IP를 내부적으로 사용한다.
뒷 자리 255도 마찬가지(브로드캐스트)
그렇기에 아래와 같이 수정해야 한다.
기존
이러면 4번 ip부터 생성하기 때문에 정상적으로 생성된다.
다시 수정한 코드 전체 내용
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "primary" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
resource "aws_instance" "netcloudy_ec2" {
instance_type = "m5.8xlarge"
ami = "ami-0c9c942bd7bf113a2"
subnet_id = aws_subnet.primary.id
}
resource "aws_network_interface" "netcloudy_eni" {
count = 7
subnet_id = aws_subnet.primary.id
private_ips = [
cidrhost("10.0.1.0/24", (count.index) + 4)
]
attachment {
instance = aws_instance.netcloudy_ec2.id
device_index = count.index + 1
}
}
terraform count로 만들어진 eni를 확인해보자
우선 EC2에 기본으로 생성되는 Private IP
다음으로 따로 생성하여 attach한 eni들
3. 프로비저너
프로비저너는 프로바이더와 비슷하게 ‘제공자’로 해석되나, 프로바이더로 실행되지 않는 커맨드와 파일 복사 같은 역할을 수행 - 링크
- 예를 들어 AWS EC2 생성 후 특정 패키지를 설치해야 하거나 파일을 생성해야 하는 경우, 이것들은 테라폼의 구성과 별개로 동작해야 한다.
- 프로비저너로 실행된 결과는 테라폼의 상태 파일과 동기화되지 않으므로 프로비저닝에 대한 결과가 항상 같다고 보장할 수 없다 ⇒ 선언적 보장 안됨
- 따라서 프로비저너 사용을 최소화하는 것이 좋다. 프로비저너의 종류에는 파일 복사와 명령어 실행을 위한 file, local-exec, remote-exec가 있다.
- 테라폼 코드 userdata 사용, cloud-init 사용, Packer 활용, Provisiner Connections 활용, 별도의 설정 관리 툴 사용(Chef, Habitat, Puppet 등) ⇒ 이전에는 local-exec provisioners를 통해서 ansible과 연동하여 인프라 배포 후 구성관리를 많이하였으나, 최근에 이러한 부분을 개선하기 위해 terraform-provider-ansible이 제공된다고 합니다
- https://registry.terraform.io/providers/ansible/ansible/latest/docs
- https://github.com/ansible/terraform-provider-ansible/tree/main
- 프로비저너의 경우 리소스 프로비저닝 이후 동작하도록 구성할 수 있다. 예를 들어 AWS EC2 생성 후 CLI를 통해 별도 작업 수행 상황을 가정
- 실습을 위해서 3.12 디렉터리를 신규 생성 후 열기 → main.tf 파일 생성
variable "sensitive_content" {
default = "secret"
#sensitive = true
}
resource "local_file" "foo" {
content = upper(var.sensitive_content)
filename = "${path.module}/foo.bar"
provisioner "local-exec" {
command = "echo The content is ${self.content}"
}
provisioner "local-exec" {
command = "abc"
on_failure = continue
}
provisioner "local-exec" {
when = destroy
command = "echo The deleting filename is ${self.filename}"
}
}
- 실행
# 코드 내용 복붙 잘 안되면 그냥 위 코드를 직접 입력하고 아래 init, plan 할 것
terraform init && terraform plan
#
terraform apply -auto-approve
...
Plan: 1 to add, 0 to change, 0 to destroy.
local_file.foo: Creating...
local_file.foo: Provisioning with 'local-exec'...
local_file.foo (local-exec): Executing: ["/bin/sh" "-c" "echo The content is SECRET"]
local_file.foo (local-exec): The content is SECRET
local_file.foo: Provisioning with 'local-exec'...
local_file.foo (local-exec): Executing: ["/bin/sh" "-c" "abc"]
local_file.foo (local-exec): /bin/sh: abc: command not found
local_file.foo: Creation complete after 0s [id=3c3b274d119ff5a5ec6c1e215c1cb794d9973ac1]
# 테라폼 상태에 프로비저너 정보(실행 및 결과)가 없다
terraform state list
terraform state show local_file.foo
cat foo.bar ; echo
cat terraform.tfstate | jq
# graph 확인 : 프로비저너 정보(의존성)이 없다
terraform graph > graph.dot
# 삭제
terraform destroy -auto-approve
...
Plan: 0 to add, 0 to change, 1 to destroy.
local_file.foo: Destroying... [id=3c3b274d119ff5a5ec6c1e215c1cb794d9973ac1]
local_file.foo: Provisioning with 'local-exec'...
local_file.foo (local-exec): Executing: ["/bin/sh" "-c" "echo The deleting filename is ./foo.bar"]
local_file.foo (local-exec): The deleting filename is ./foo.bar
local_file.foo: Destruction complete after 0s
- main.tf 내용 수정
variable "sensitive_content" {
default = "secret"
sensitive = true
}
resource "local_file" "foo" {
content = upper(var.sensitive_content)
filename = "${path.module}/foo.bar"
provisioner "local-exec" {
command = "echo The content is ${self.content}"
}
provisioner "local-exec" {
command = "abc"
#on_failure = continue
}
provisioner "local-exec" {
when = destroy
command = "echo The deleting filename is ${self.filename}"
}
}
- 다시 실행
# 민감 정보 참조 부분의 실행 및 결과 내용은 출력 안됨
# 실행 실패 시 에러 발생되면 중지
terraform apply -auto-approve
...
Plan: 1 to add, 0 to change, 0 to destroy.
local_file.foo: Creating...
local_file.foo: Provisioning with 'local-exec'...
local_file.foo (local-exec): (output suppressed due to sensitive value in config)
local_file.foo (local-exec): (output suppressed due to sensitive value in config)
local_file.foo: Provisioning with 'local-exec'...
local_file.foo (local-exec): Executing: ["/bin/sh" "-c" "abc"]
local_file.foo (local-exec): /bin/sh: abc: command not found
╷
│ Error: local-exec provisioner error
│
│ with local_file.foo,
│ on main.tf line 14, in resource "local_file" "foo":
│ 14: provisioner "local-exec" {
│
│ Error running command 'abc': exit status 127. Output: /bin/sh: abc: command not found
│
나는 WSL2 환경이라 그런지 abc가 Permission denied가 났지만 여튼 원하는 결과가 나오긴 했다.
(실패하면 멈추는 것)
local-exec 프로비저너: 테라폼이 실행되는 환경에서 수행할 커맨드를 정의 - 링크
- 리눅스나 윈도우등 테라폼을 실행하는 환경에 맞게 커맨드를 정의, 아래 사용하는 인수 값
- command(필수) : 실행할 명령줄을 입력하며 << 연산자를 통해 여러 줄의 커맨드 입력 가능
- working_dir(선택) : command의 명령을 실행할 디렉터리를 지정해야 하고 상대/절대 경로로 설정
- interpreter(선택) : 명령을 실행하는 데 필요한 인터프리터를 지정하며, 첫 번째 인수로 인터프리터 이름이고 두 번째부터는 인터프리터 인수 값
- environment(선택) : 실행 시 환경 변수 는 실행 환경의 값을 상속받으면, 추가 또는 재할당하려는 경우 해당 인수에 key = value 형태로 설정
- 예시 코드
Unix/Linux/macOS | Windows |
resource "null_resource" "example1" {
provisioner "local-exec" {
command = <<EOF
echo Hello!! > file.txt
echo $ENV >> file.txt
EOF
interpreter = [ "bash" , "-c" ]
working_dir = "/tmp"
environment = {
ENV = "world!!"
}
}
}
|
resource "null_resource" "example1" {
provisioner "local-exec" {
command = <<EOF
Hello!! > file.txt
Get-ChildItem Env:ENV >> file.txt
EOF
interpreter = [ "PowerShell" , "-Command" ]
working_dir = "C:\\windows\temp"
environment = {
ENV = "world!!"
}
}
}
|
- command의 << 연산자를 통해 다중 라인의 명령을 수행하여 각 환경에 맞는 인터프리터를 지정해 해당 명령을 수행한다.
- 실제로 실무에서도 많이 사용하고 있는 방식이다.
- Apply 수행 시 이 명령의 실행 위치를 working_dir를 사용해 지정하고 command에서 사용하는 환경 변수에 대해 environment에서 지정한다.
- main.tf 파일 내용 수정 : macOS/Linux 경우 → 윈도우는 수정해서 사용할것!
resource "null_resource" "example1" {
provisioner "local-exec" {
command = <<EOF
echo Hello!! > file.txt
echo $ENV >> file.txt
EOF
interpreter = [ "bash" , "-c" ]
working_dir = "/tmp"
environment = {
ENV = "world!!"
}
}
}
- 실행
#
terraform init -upgrade
#
terraform plan && terraform apply -auto-approve
...
null_resource.example1: Creating...
null_resource.example1: Provisioning with 'local-exec'...
null_resource.example1 (local-exec): Executing: ["bash" "-c" " echo Hello!! > file.txt\n echo $ENV >> file.txt\n"]
...
#
terraform state list
terraform state show null_resource.example1
cat /tmp/file.txt
EOF 방식으로 잘 반영된 모습 확인할 수 있다.
원격지 연결 - 링크
- remote-exec와 file 프로비저너를 사용하기 위해 원격지에 연결할 SSH, WinRM 연결 정의가 필요하다
- connection 블록 리소스 선언 시, 해당 리소스 내에 구성된 프로비저너에 대해 공통으로 선언되고, 프로비저너 내에 선언되는 경우, 해당 프로비저너에서만 적용된다.
# connection 블록으로 원격지 연결 정의
resource "null_resource" "example1" {
connection {
type = "ssh"
user = "root"
password = var.root_password
host = var.host
}
provisioner "file" {
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"
}
provisioner "file" {
source = "conf/myapp.conf"
destination = "C:/App/myapp.conf"
connection {
type = "winrm"
user = "Administrator"
password = var.admin_password
host = var.host
}
}
}
- connection 적용 인수와 설명 - 링크
- 원격 연결이 요구되는 프로비저너의 경우 스크립트 파일을 원격 시스템에 업로드해 해당 시스템의 기본 쉘에서 실행하도록 하므로 script_path의 경우 적절한 위치를 지정하도록 한다. 경로는 난수인 %RAND% 경로가 포함되어 생성된다.
Unix/Linux/macOS : /tmp/terraform_%RAND%.sh
Windows(cmd) : C:/windows/temp/terraform_%RAND%.cmd
Windows(PowerShell) : C:/windows/temp/terraform_%RAND%.ps1
- 베스천 호스트를 통해 연결하는 경우 관련 인수를 지원한다 - 링크
file 프로비저너 : 테라폼을 실행하는 시스템에서 연결 대상으로 파일 또는 디렉터리를 복사하는 데 사용 - 링크
- 테라폼을 실행하는 시스템에서 연결 대상으로 파일 또는 디렉터리를 복사하는 데 사용
- 사용되는 인수
- source : 소스 파일 또는 디렉터리로, 현재 작업 중인 디렉터리에 대한 상태 경로 또는 절대 경로로 지정할 수 있다. content와 함께 사용할 수 없다.
- content : 연결 대상에 복사할 내용을 정의하며 대상이 디렉터리인 경우 tf-file-content 파일이 생성되고, 파일인 경우 해당 파일에 내용이 기록된다. source와 함께 사용할 수 없다.
- destination : 필수 항목으로 항상 절대 경로로 지정되어야 하며, 파일 또는 디렉터리다.
- destination 지정 시 주의해야 할 점은 ssh 연결의 경우 대상 디렉터리가 존재해야 하며, winrm 연결은 디렉터리가 없는 경우 자동으로 생성함
- 디렉터리를 대상으로 하는 경우에는 source 경로 형태에 따라 동작에 차이가 생긴다.
- destination이 /tmp인 경우 source가 디렉터리로 /foo 처럼 마지막에 /가 없는 경우 대상 디렉터리에 지정한 디렉터리가 업로드되어 연결된 시스템에 /tmp/foo 디렉터리가 업로드된다.
- source가 디렉터리로 /foo/ 처럼 마지막에 /가 포함되는 경우 source 디렉터리 내의 파이란 /tmp 디렉터리에 업로드된다.
- file 프로비저너 구성 예
resource "null_resource" "foo" {
# myapp.conf 파일이 /etc/myapp.conf 로 업로드
provisioner "file" {
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"
}
# content의 내용이 /tmp/file.log 파일로 생성
provisioner "file" {
content = "ami used: ${self.ami}"
destination = "/tmp/file.log"
}
# configs.d 디렉터리가 /etc/configs.d 로 업로드
provisioner "file" {
source = "conf/configs.d"
destination = "/etc"
}
# apps/app1 디렉터리 내의 파일들만 D:/IIS/webapp1 디렉터리 내에 업로드
provisioner "file" {
source = "apps/app1/"
destination = "D:/IIS/webapp1"
}
}
remote-exec 프로비저너 : 원격지 환경에서 실행할 커맨드와 스크립트를 정의 - 링크
- 예를 들면 AWS의 EC2 인스턴스를 생성하고 해당 VM에서 명령을 실행하고 패키지를 설치하는 등의 동작을 의미한다.
- 사용하는 인수는 다음과 같고 각 인수는 서로 배타적이다.
- inline : 명령에 대한 목록으로 [ ] 블록 내에 “ “로 묶인 다수의 명령을 , 로 구분해 구성한다.
- script : 로컬의 스크립트 경로를 넣고 원격에 복사해 실행한다.
- scripts : 로컬의 스크립트 경로의 목록으로 [ ] 블록 내에 “ “로 묶인 다수의 스크립트 경로를 , 로 구분해 구성한다
- script 또는 scripts의 대상 스크립트 실행에 필요한 인수는 관련 구성에서 선언할 수 없으므로 필요할 때 file 프로바이더로 해당 스크립트를 업로드하고 inline 인수를 활용해 스크립트에 인수를 추가한다.
- 구성 예
resource "aws_instance" "web" {
# ...
# Establishes connection to be used by all
# generic remote provisioners (i.e. file/remote-exec)
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",
]
}
}
결론
조건문, 함수, 프로비저너 까지 모두 살펴보았다.
테라폼에서 제공하는 함수가 정말 많은 편의성을 제공한다는 걸 알게 되었다.테라폼의 상태 파일과 동기화되지 않는 프로비저너의 역할 역시 잘 알게되었다.나는 프로비저너를 통해 간단한 명령어나 앤서블 코드 실행하는 정도로 사용하게 될 것 같다.
'Tech > Terraform' 카테고리의 다른 글
[T101_3기] 4주차 - State & 모듈 & 협업 (1/3) - State (0) | 2023.09.10 |
---|---|
[T101_3기] 3주차 - 기본 사용 및 프로바이더 (2/3) - 기본 사용(2/2) (0) | 2023.09.10 |
[T101_3기] 2주차 - 기본 사용 (3/3) - 출력(output) (0) | 2023.09.09 |
[T101_3기] 2주차 - 기본 사용 (2/3) - 입력 변수(Variable) 및 지역값(local) (0) | 2023.09.09 |
[T101_3기] 2주차 - 기본 사용 (1/3) - 데이터 소스 및 반복문 (0) | 2023.09.09 |
댓글