[2주차] 기본 사용 2/3
CloudNet@ 가시다님이 진행하는 Terraform 101 Study 4기 스터디 내용 참고.
스터디 교재 : ‘테라폼으로 시작하는 IaC’ (한빛 미디어, 김민수 외 지금) 책의 내용 참고
-. https://www.aladin.co.kr/m/mproduct.aspx?ItemId=317519064
1. 데이터 소스
데이터 소스는 테라폼으로 정의되지 않은 외부 리소스 또는 저장된 정보를 테라폼 내에서 참조할 때 사용한다
- 데이터 소스 블록은 data 로 시작, 이후 ‘데이터 소스 유형’을 정의 ← Resource 블록 정의와 유사
- 데이터 소스 유형은 첫 번째 _를 기준으로 앞은 프로바이더 이름, 뒤는 프로바이더에서 제공하는 리소스 유형을 의미한다.
- 데이터 소스 유형을 선언한 뒤에는 고유한 이름을 붙인다. 리소스의 이름과 마찬가지로 이름은 동일한 유형에 대한 식별자 역할을 하므로 중복될 수 없다.
- 이름 뒤에는 데이터 소스 유형에 대한 구성 인수들은 { } 안에 선언한다. 인수가 필요하지 않은 유형도 있지만, 그때에도 { } 는 입력한다
- 데이터 소스 속성 참조
- 데이터 소스로 읽은 대상을 참조하는 방식은 리소스와 구별되게 data가 앞에 붙는다. 속성 값은 다음과 같이 접근할 수 있다.
## 선언형태 ##
# Terraform Code
data "<리소스 유형>" "<이름>" {
<인수> = <값>
}
# 데이터 소스 참조
data.<리소스 유형>.<이름>.<속성>
## 예시 ##
# Declare the data source
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_subnet" "primary" {
availability_zone = data.aws_availability_zones.available.names[0]
# e.g. ap-northeast-2a
}
resource "aws_subnet" "secondary" {
availability_zone = data.aws_availability_zones.available.names[1]
# e.g. ap-northeast-2b
}
예제)
main.tf
data "aws_availability_zones" "seoul" {
state = "available"
}
terraform init -upgrade && terraform plan && terraform apply -auto-approve
terraform console 명령으로 자원 확인 가능
2. 입력변수
입력 변수는 인프라를 구성하는 데 필요한 속성 값을 정의해 코드의 변경 없이 여러 인프라를 생성하는 데 목적이 있다.
테라폼에서는 이것을 입력 변수 Input Variables 로 정의한다.
- 변수는 variable로 시작되는 블록으로 구성된다. 변수 블록 뒤의 이름 값은 동일 모듈 내 모든 변수 선언에서 고유해야 하며, 이 이름으로 다른 코드 내에서 참조된다.
# variable 블록 선언의 예
variable "<이름>" {
<인수> = <값>
}
variable "image_id" {
type = string
}
테라폼 예약 변수 이름으로 사용 불가능 : source, version, providers, count, for_each, lifecycle, depends_on, locals
- 변수 정의 시 사용 가능한 메타인수
- default : 변수 값을 전달하는 여러 가지 방법을 지정하지 않으면 기본값이 전달됨, 기본값이 없으면 대화식으로 사용자에게 변수에 대한 정보를 물어봄
- type : 변수에 허용되는 값 유형 정의, string number bool list map set object tuple 와 유형을 지정하지 않으면 any 유형으로 간주
- description : 입력 변수의 설명
- validation : 변수 선언의 제약조건을 추가해 유효성 검사 규칙을 정의 - 링크
- sensitive : 민감한 변수 값임을 알리고 테라폼의 출력문에서 값 노출을 제한 (암호 등 민감 데이터의 경우) - 링크
- nullable : 변수에 값이 없어도 됨을 지정 - Link
- 변수 정의 시 사용 가능한 메타인수
- default : 변수 값을 전달하는 여러 가지 방법을 지정하지 않으면 기본값이 전달됨, 기본값이 없으면 대화식으로 사용자에게 변수에 대한 정보를 물어봄
- type : 변수에 허용되는 값 유형 정의, string number bool list map set object tuple 와 유형을 지정하지 않으면 any 유형으로 간주
- description : 입력 변수의 설명
- validation : 변수 선언의 제약조건을 추가해 유효성 검사 규칙을 정의 - 링크
- sensitive : 민감한 변수 값임을 알리고 테라폼의 출력문에서 값 노출을 제한 (암호 등 민감 데이터의 경우) - 링크
- nullable : 변수에 값이 없어도 됨을 지정 - Link
Input Variables - Configuration Language | Terraform | HashiCorp Developer
Input variables allow you to customize modules without altering their source code. Learn how to declare, define, and reference variables in configurations.
developer.hashicorp.com
변수 선언 예제
main.tf
variable "string" {
type = string
description = "var String"
default = "myString"
}
variable "number" {
type = number
default = 123
}
variable "boolean" {
default = true
}
variable "list" {
default = [
"google",
"vmware",
"amazon",
"microsoft"
]
}
output "list_index_0" {
value = var.list.0
}
output "list_all" {
value = [
for name in var.list : upper(name)
]
}
variable "map" { # Sorting
default = {
aws = "amazon",
azure = "microsoft",
gcp = "google"
}
}
variable "set" { # Sorting
type = set(string)
default = [
"google",
"vmware",
"amazon",
"microsoft"
]
}
variable "object" {
type = object({ name = string, age = number })
default = {
name = "abc"
age = 12
}
}
variable "tuple" {
type = tuple([string, number, bool])
default = ["abc", 123, true]
}
variable "ingress_rules" { # optional ( >= terraform 1.3.0)
type = list(object({
port = number,
description = optional(string),
protocol = optional(string, "tcp"),
}))
default = [
{ port = 80, description = "web" },
{ port = 53, protocol = "udp" }]
}
## 실행 명령어 ##
terraform init && terraform plan && terraform apply -auto-approve
변수 참조
main.tf
variable "my_password" {}
resource "local_file" "abc" {
content = var.my_password
filename = "${path.module}/abc.txt"
}
terraform init -upgrade && terraform apply -auto-approve
실습
- VPC / Subnet / Security Group / EC2 생성 배포 진행
- default VPC 대신 직접 VPC를 만들고, 해당 VPC내에 EC2 1대를 배포 하는 코드 입니다.
# 신규 디렉터리 생성
mkdir my-vpc-ec2
cd my-vpc-ec2
touch vpc.tf
vpc.tf
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_vpc" "haru_vpc" {
cidr_block = "10.10.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "t101-study"
}
}
resource "aws_subnet" "haru_subnet1" {
vpc_id = aws_vpc.haru_vpc.id
cidr_block = "10.10.1.0/24"
availability_zone = "ap-northeast-2a"
tags = {
Name = "t101-subnet1"
}
}
resource "aws_subnet" "haru_subnet2" {
vpc_id = aws_vpc.haru_vpc.id
cidr_block = "10.10.2.0/24"
availability_zone = "ap-northeast-2c"
tags = {
Name = "t101-subnet2"
}
}
resource "aws_internet_gateway" "haru_igw" {
vpc_id = aws_vpc.haru_vpc.id
tags = {
Name = "t101-igw"
}
}
resource "aws_route_table" "haru_rt" {
vpc_id = aws_vpc.haru_vpc.id
tags = {
Name = "t101-rt"
}
}
resource "aws_route_table_association" "haru_rtassociation1" {
subnet_id = aws_subnet.haru_subnet1.id
route_table_id = aws_route_table.haru_rt.id
}
resource "aws_route_table_association" "haru_rtassociation2" {
subnet_id = aws_subnet.haru_subnet2.id
route_table_id = aws_route_table.haru_rt.id
}
resource "aws_route" "haru_defaultroute" {
route_table_id = aws_route_table.haru_rt.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.haru_igw.id
}
output "aws_vpc_id" {
value = aws_vpc.haru_vpc.id
}
sg.tf
resource "aws_security_group" "haru_sg" {
vpc_id = aws_vpc.myvpc.id
name = "T101 SG"
description = "T101 Study SG"
}
resource "aws_security_group_rule" "haru_sginbound" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.haru_sg.id
}
resource "aws_security_group_rule" "haru_sgoutbound" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.haru_sg.id
}
output "aws_security_group_id" {
value = aws_security_group.mysg.id
}
ec2.tf
data "aws_ami" "haru_amazonlinux2" {
most_recent = true
filter {
name = "owner-alias"
values = ["amazon"]
}
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-ebs"]
}
owners = ["amazon"]
}
resource "aws_instance" "haru_ec2" {
depends_on = [
aws_internet_gateway.haru_igw
]
ami = data.aws_ami.haru_amazonlinux2.id
associate_public_ip_address = true
instance_type = "t2.micro"
vpc_security_group_ids = ["${aws_security_group.haru_sg.id}"]
subnet_id = aws_subnet.haru_subnet1.id
user_data = <<-EOF
#!/bin/bash
wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
mv busybox-x86_64 busybox
chmod +x busybox
RZAZ=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone-id)
IID=$(curl 169.254.169.254/latest/meta-data/instance-id)
LIP=$(curl 169.254.169.254/latest/meta-data/local-ipv4)
echo "<h1>RegionAz($RZAZ) : Instance ID($IID) : Private IP($LIP) : Web Server</h1>" > index.html
nohup ./busybox httpd -f -p 80 &
EOF
user_data_replace_on_change = true
tags = {
Name = "t101-myec2"
}
}
output "myec2_public_ip" {
value = aws_instance.haru_ec2.public_ip
description = "The public IP of the Instance"
}
실행
terraform plan && terraform apply -auto-approve
terraform state list
terraform state show data.aws_ami.haru_amazonlinux2
terraform console
> data.aws_ami.haru_amazonlinux2
terraform graph > graph.dot
terraform destroy
[악분님 제공] count 실습
- 악분님이 테라폼 반복문 관련 내용(count, for_each, for expression 등)이해를 위한 실습
- 실습환경 가져오기
git clone https://github.com/sungwook-practice/t101-study.git
cd t101-study/count_vs_foreach
cat Readme.md
tree -L 1
# 개요
* count와 for_each를 비교하기 위한 예제 모음
* 유투브 영상: https://youtu.be/enhSdIJ9xxQ
- step4_count_refactoring 실습
- 설명
- count index 를 사용하여 각각 이름으로 subnet을 여러개 생성
- subnet 설정 변수를 한 변수로 설정하도록 변수 선언
-. main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = {
Name = "terraform VPC"
}
}
resource "aws_subnet" "public" {
count = length(var.public_subnets)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnets[count.index].cidr
availability_zone = var.public_subnets[count.index].az
tags = var.public_subnets[count.index].tags
}
-. provider.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.4.0"
}
}
required_version = ">= 1.4"
}
provider "aws" {}
-. terraform.tfvars
vpc_cidr = "192.168.0.0/16"
public_subnets = [
{
"cidr" = "192.168.0.0/24",
"az" = "ap-northeast-2a",
tags = {
Name = "terraform-public-subnet-a"
Env = "dev"
}
},
{
"cidr" = "192.168.1.0/24",
"az" = "ap-northeast-2c",
tags = {
Name = "terraform-public-subnet-b"
Env = "dev"
}
}
]
variables.tf
variable "vpc_cidr" {
type = string
}
variable "public_subnets" {
type = list(object({
cidr = string
az = string
tags = map(string)
}))
}
# 신규 터미널 : 모니터링
while true; do aws ec2 describe-subnets --filters --query "Subnets[*].{ID:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone}" --output text; echo; time; sleep 1; done
# 배포 실행
terraform plan && terraform apply -auto-approve
# 실행 결과 확인
terraform state list
echo "aws_subnet.main[0].tags_all" | terraform console
echo "aws_subnet.main[1].tags_all" | terraform console
# 신규 생성한 VPC 내에 서브넷 확인
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$(terraform output -raw myvpc_id)" --query "Subnets[*].{ID:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone,Name:Tags[*]|[*].Value}" --output table
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$(terraform output -raw myvpc_id)" --query "Subnets[*].{ID:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone,Name:Tags[*]|[*].Value}" --output text
AWS Console
- step5_count_invalid_example 실습
- 설명
- 테라폼 변수에서 안쓰는 subnet을 삭제한 후, 테라폼을 실행하니 EC2 instance가 교체됨
- 오류 확인
- count index로 자원을 생성했을 경우 삭제/생성 시 원하는 것과 다르게 동작할 수 있음 인지
-. main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = {
Name = "terraform VPC"
}
}
resource "aws_subnet" "main" {
count = length(var.subnets)
vpc_id = aws_vpc.main.id
cidr_block = var.subnets[count.index].cidr
availability_zone = var.subnets[count.index].az
tags = var.subnets[count.index].tags
}
resource "aws_instance" "server" {
ami = "ami-0e8bd0820b6e1360b"
instance_type = "t4g.nano"
subnet_id = aws_subnet.main[1].id
# index 접근 방법 오류 해결 코드
# subnet_id = index(aws_subnet.main.*.cidr_block, "192.168.2.0/24")
tags = {
Name = "Terraform demo"
}
}
-. provider.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.4.0"
}
}
required_version = ">= 1.4"
}
provider "aws" {}
-. terraform.tfvars
vpc_cidr = "192.168.0.0/16"
subnets = [
# (유투브 7번째 시나리오) terrafprm apply 이 후, 첫 번째 요소를 주석하세요
{
cidr = "192.168.1.0/24",
az = "ap-northeast-2a",
tags = {
Name = "public-subnet"
Environment = "dev"
}
},
# (유투브 8번째 시나리오) terrafprm apply 이 후, 주석을 해제하고 terraform apply해보세요
# {
# cidr = "192.168.5.0/24",
# az = "ap-northeast-2a",
# tags = {
# Name = "public-subnet"
# Environment = "dev"
# }
# },
{
cidr = "192.168.2.0/24",
az = "ap-northeast-2a",
tags = {
Name = "private-subnet"
Environment = "dev"
}
},
{
cidr = "192.168.3.0/24",
az = "ap-northeast-2a",
tags = {
Name = "public-subnet"
Environment = "dev"
}
},
{
cidr = "192.168.4.0/24",
az = "ap-northeast-2c",
tags = {
Name = "private-subnet"
Environment = "dev"
}
}
]
-. variables.tf
variable "vpc_cidr" {
type = string
}
variable "subnets" {
type = list(object({
cidr = string
az = string
tags = map(string)
}))
}
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PrivateIP:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --output text
while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PrivateIP:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --output text; echo; aws ec2 describe-subnets --query "Subnets[*].{ID:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone,Name:Tags[*]|[*].Value}" --output text; echo; time; sleep 1; done
# 배포 실행
terraform init && terraform plan && terraform apply -auto-approve
# 실행 결과 확인
terraform state list
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PrivateIP:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --output table
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$(terraform output -raw myvpc_id)" --query "Subnets[*].{ID:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone,Name:Tags[*]|[*].Value}" --output table
[장애 재현] terraform.tfvars 수정 : 아래 부분 주석 처리 (첫 번째 요소를 주석)
-. terraform.tfvars
vpc_cidr = "192.168.0.0/16"
subnets = [
# (유투브 7번째 시나리오) terrafprm apply 이 후, 첫 번째 요소를 주석하세요
# {
# cidr = "192.168.1.0/24",
# az = "ap-northeast-2a",
# tags = {
# Name = "public-subnet"
# Environment = "dev"
# }
# },
# (유투브 8번째 시나리오) terrafprm apply 이 후, 주석을 해제하고 terraform apply해보세요
# {
# cidr = "192.168.5.0/24",
# az = "ap-northeast-2a",
# tags = {
# Name = "public-subnet"
# Environment = "dev"
# }
# },
{
cidr = "192.168.2.0/24",
az = "ap-northeast-2a",
tags = {
Name = "private-subnet"
Environment = "dev"
}
},
.....
설명 : 첫번째 subnet을 지웠는데 instance 가 재생성되는 상황
[장애 재현] terraform.tfvars 수정 : 아래 부분 주석 처리 (두 번째 요소를 주석 해제)
-. terraform.tfvars
vpc_cidr = "192.168.0.0/16"
subnets = [
# (유투브 7번째 시나리오) terrafprm apply 이 후, 첫 번째 요소를 주석하세요
# {
# cidr = "192.168.1.0/24",
# az = "ap-northeast-2a",
# tags = {
# Name = "public-subnet"
# Environment = "dev"
# }
# },
# (유투브 8번째 시나리오) terrafprm apply 이 후, 주석을 해제하고 terraform apply해보세요
{
cidr = "192.168.5.0/24",
az = "ap-northeast-2a",
tags = {
Name = "public-subnet"
Environment = "dev"
}
},
{
cidr = "192.168.2.0/24",
az = "ap-northeast-2a",
tags = {
Name = "private-subnet"
Environment = "dev"
}
},
.....
설명 : 새로운 subnet을 추가하려 했으나 192.168.4.0/24 가 없어지고 신규 subnet 인 192.168.5.0/24 가 생성됨
#자원삭제
terraform destroy