T101 study

[2주차] 기본 사용 2/3

haru224 2024. 6. 22. 05:49

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