[Terraform@CloudNet] Terraform 기본 사용 (2)

2024. 6. 23. 00:46EKS@Terraform

 

- CloudNet에서 주관하는 Terraform 스터디 내용입니다

- 내용은 위 책 테라폼으로 시작하는 IaC 를 기준으로 정리하였습니다.

- 실습은 M1 macbook air 에서 진행했습니다.

- 매번 좋은 스터디를 진행해 주시는 CloudNet 팀 감사드립니다

- 잘못된 점, 업데이트된 지식, 다른 코멘트 언제나 환영입니다!

 

 

목차

1. Terraform 문법

2. 실습 1: VPC + 보안그룹 + EC2 배포

3. 실습 2: AWS IAM user 배포

4. 실습 3: 반복문 사용

5. 실습 4: 반복문 사용+

 

 

 

Terraform 문법

데이터 소스

테라폼의 '데이터 소스' 는 테라폼으로 정의되지 않은 외부 리소스 또는 정보를 가져올 때 사용한다. 사용 사례는

- 기존 인프라 참조

- 동적 데이터 조회

- 계정 정보 조회

- 외부 DB에서 정보 참조

- 다른 테라폼 리소스의 속성 값 참조

 

등이 있다.

 

 

data "local_file" "abc" {
  filename = "${path.module}/abc.txt"
}

 

이 예시를 보면서 간단히 문법을 살펴보자.

1. 우선 data 로 시작해서 프로바이더명_리소스명 으로 데이터 소스 유형 정의: 여기서는 로컬의 .txt를 사용, "local_file"이 유형이 된다. 

2. 데이터 소스 이름 정의: 여기서는 "abc" 가 된다.

 

 

아래 코드를 이용해서 실습에 필요한 텍스트 파일을 생성하고 읽어올 수 있다.

# 참조할 파일 생성
echo "t101 study - 2week" > abc.txt

# 테라폼에서 불러오기
terraform init
terraform plan
terraform apply -auto-approve
terraform state list

 

 

그래프로 그리면 depency 는 없이 단독이다.

 

 

 

이번에는 aws provider 로 서울리전에서 사용 가능한 zone을 가져와보자.

data "aws_availability_zones" "seoul" {
  state = "available"
}

 

위와 마찬가지로 init -> plan -> apply 후, show 명령어로 결과를 텍스트로 확인할 수 있다.

terraform state show data.aws_availability_zones.seoul

 

서울리전은 a,b,c,d 로 나뉘어있고 모두 활용가능하다.

 

 

입력변수

테라폼의 '입력 변수' 는 인프라 구성에 필요한 속성값을 정의하고 코드를 변경하지 않기 위해 사용된다.

- 환경별 구성: 같은 인프라 구성에서 개발서버에는 저성능 인스턴스, 실서버에는 고성능 인스턴스로 유형만 변경할 수 있다. 

- secrets 별도 관리: 키를 코드에 직접 포함시키지 않고 필요할 때마다 읽어서 쓸 수 있다(데이터 소스는 읽어서 저장하는 것과 다른 듯)

- 리소스 수량 조절: '환경별 구성' 과 유사

- 태그, 지역 관리: 리전별로 다른 AMI ID나 버전관리에 필요한 태그 등에 사용

 

 

변수 유형: string, number, bool, any

집합 유형: list, map, set, object, tuple

메타 인수: default, type, description, validation, sensitive, nullable

 

이 있으니 아래 예시를 보고 필요한 걸 검색해서 쓰도록 하자.

 

 

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" }]
}

 

 

실행시에는 마찬가지로 init -> plan -> apply 순서로 하는데, terraform output 명령으로 결과를 프린트할 수 있다.

terraform init
terraform plan
terraform apply -auto-approve

# 확인
terraform output

 

 

 

변수 우선순위가 지정되어 있어서 같은 변수에 다른 내용이 지정되어 있으면 우선순위가 높은 게 사용된다.

https://spacelift.io/blog/terraform-tfvars

1. 실행 후 터미널에 입력

2. variable 블록의 디폴트 값

3. 환경변수

4. terraform.tfvars에 정의된 변수

5. *.auto.tfvars 에 저장된 변수

6. *.auto.tfvars.json 에 저장된 변수

7. cli 실행시 -var 나 -var-file 로 지정

 

 

실습 1: VPC + 보안그룹 + EC2 배포

- default VPC 대신 직접 만든 VPC 에 EC2 1대를 배포해본다

 

vpc 를 배포하는 테라폼 코드는 아래와 같다

provider "aws" {
  region  = "ap-northeast-2"
}

resource "aws_vpc" "myvpc" {
  cidr_block       = "10.10.0.0/16"

  tags = {
    Name = "t101-study"
  }
}

 

 

- 아래와 같이 init -> plan -> apply  순서로 적용하여 vpc 를 배포한다

terraform init && terraform plan && terraform apply -auto-approve
terraform state list

 

 

- aws cli를 이용해서 jq로 검색해서 확인할 수도 있다.

export AWS_PAGER=""
aws ec2 describe-vpcs --filter 'Name=isDefault,Values=false'** --output yaml

- vpc 가 배포되었으나, 이제 우리는 ec2 로 직접 접근을 시도할 것이기 때문에 게이트웨이와 라우팅 설정이 추가로 필요하다. 아래 코드를 적용하고 plan -> apply 해서 게이트웨이를 열고 디폴트 라우팅 경로를 지정한다

  region  = "ap-northeast-2"
}

resource "aws_vpc" "myvpc" {
  cidr_block       = "10.10.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "t101-study"
  }
}

resource "aws_subnet" "mysubnet1" {
  vpc_id     = aws_vpc.myvpc.id
  cidr_block = "10.10.1.0/24"

  availability_zone = "ap-northeast-2a"

  tags = {
    Name = "t101-subnet1"
  }
}

resource "aws_subnet" "mysubnet2" {
  vpc_id     = aws_vpc.myvpc.id
  cidr_block = "10.10.2.0/24"

  availability_zone = "ap-northeast-2c"

  tags = {
    Name = "t101-subnet2"
  }
}


resource "aws_internet_gateway" "myigw" {
  vpc_id = aws_vpc.myvpc.id

  tags = {
    Name = "t101-igw"
  }
}

resource "aws_route_table" "myrt" {
  vpc_id = aws_vpc.myvpc.id

  tags = {
    Name = "t101-rt"
  }
}

resource "aws_route_table_association" "myrtassociation1" {
  subnet_id      = aws_subnet.mysubnet1.id
  route_table_id = aws_route_table.myrt.id
}

resource "aws_route_table_association" "myrtassociation2" {
  subnet_id      = aws_subnet.mysubnet2.id
  route_table_id = aws_route_table.myrt.id
}

resource "aws_route" "mydefaultroute" {
  route_table_id         = aws_route_table.myrt.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.myigw.id
}

output "aws_vpc_id" {
  value = aws_vpc.myvpc.id
}

 

 

- plan-apply 로 배포하면 vpc 는 수정되고, list 로 gateway, default route, route table, subnet 2개, Table association 2개 총 7개의 리소스가 추가로 배포되었음을 볼 수 있다.

 

 

- 이번에는 실습을 위한 보안그룹을 배포해보자. 마찬가지로 my-vpc-ec2 디렉토리에서 작업한다

resource "aws_security_group" "mysg" {
  vpc_id      = aws_vpc.myvpc.id
  name        = "T101 SG"
  description = "T101 Study SG"
}

resource "aws_security_group_rule" "mysginbound" {
  type              = "ingress"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.mysg.id
}

resource "aws_security_group_rule" "mysgoutbound" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.mysg.id
}

output "aws_security_group_id" {
  value       = aws_security_group.mysg.id
}

 

security group, inbound rule, outbound rule 3개가 잘 배포되었다.

아래 jq 명령어로도 배포된 보안그룹을 검색할 수 있다

aws ec2 describe-security-groups --group-ids $(terraform output -raw aws_security_group_id)

 

- 마지막으로 ec2 를 배포한다.

data "aws_ami" "my_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" "myec2" {

  depends_on = [
    aws_internet_gateway.myigw
  ]

  ami                         = data.aws_ami.my_amazonlinux2.id
  associate_public_ip_address = true
  instance_type               = "t2.micro"
  vpc_security_group_ids      = ["${aws_security_group.mysg.id}"]
  subnet_id                   = aws_subnet.mysubnet1.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.myec2.public_ip
  description = "The public IP of the Instance"
}

 

- plan-apply 로 배포를 성공했다. 최종적으로 curl 로 인스턴스에 직접 접근해보도록 하겠다

while true; do curl --connect-timeout 1 \
http://$MYIP/ ; echo "------------------------------"; date; sleep 1; done

- terraform graph 명령어로 서로의 의존관계를 시각화했다. vpc 내부에 보안그룹과 인스턴스가 있으며 이 인스턴스는 다시 게이트웨이와 라우팅 테이블을 통해 외부와 통신함을 알 수 있다.

 

그리고 terraform destory 로 이 모든 것을 쉽고 빠르고 간편하게 삭제할 수 있다.

 

 

 

실습 2: AWS IAM user 배포

두번째로 IAM user 를 생성해보았다. 새 디렉토리를 생성하고 iamuser.tf 파일을 작성한다

provider "aws" {
  region = "ap-northeast-2"
}

locals {
  name = "mytest"
  team = {
    group = "dev"
  }
}

resource "aws_iam_user" "myiamuser1" {
  name = "${local.name}1"
  tags = local.team
}

resource "aws_iam_user" "myiamuser2" {
  name = "${local.name}2"
  tags = local.team
}

 

aws 콘솔과 terraform terminal 에서 유저가 잘 생성되었음을 확인할 수 있다

 

- aws console 에서도 생성된 iam user를 확인할 수 있다. mytest1 과 mytest2 라는 유저가 새로 생겼음을 볼 수 있따.

aws iam list-users | jq

 

- terraform graph 명령어로 dependency 를 생성했을 때, 두 user는 완전히 독립 관계임을 확인할 수 있다.

 

 

- 이번에는 선택 삭제를 해보겠다. destroy 에서 -target 옵션을 주면 원하는 리소스만 골라 삭제할 수 있다

terraform destroy -auto-approve -target=aws_iam_user.myiamuser1

 

mytest1 만 골라서 삭제된 것을 확인할 수 있다

 

 

 

실습 3: 반복문 사용

테라폼의 꽃🌸 이라는 반복문. 동일한 내용을 반복 하드코딩 하지 않을 수 있다는 점이 매력적이다. 별도 모듈로 빼서 관리하기도 편할 것 같다. 반복문에는 count 와 for_each가 있다. for_each 가 인덱스에서 자유로워서 좀더 강건하므로 사용하는 것이 좋겠지만 우선 count 를 사용해보고, 실습 4에서 for_each 를 사용해보았다.

 

현재 주언어를 python으로 쓰는 사람의 관점에서 설명하면 list를 순회할 때 range로 인덱스를 사용하는 건 count, 그냥 바로 변수로 순회하는 건 for_each에 가깝다고 생각된다.

alphabet = ['a', 'b', 'c', 'd', 'e']

# count와 비슷한 list 순회
for i in range(len(alphabet)):
	print(alphabet[i])
  
# for_each 와 비슷한 list 순회
for character in alphabet:
	print(character)
    
# 출력결과는 둘다 같다

 

 

 

우선 variables 가 필요하다. list 형태로 되어 있어서 순회할 수 있다. 그리고 리소스에서는 이 선언한 variables 의 길이를 가져와서 그만큼 루프를 돌면서 인덱스로 사용하고, 최종적으로 그 인덱스를 사용하는 형식이다.

variable "names" {
  type    = list(string)
  default = ["a", "b", "c"]
}

resource "local_file" "abc" {
  count   = length(var.names)
  content = "abc"
  filename = "${path.module}/abc-${var.names[count.index]}.txt"
}

resource "local_file" "def" {
  count   = length(var.names)
  content = local_file.abc[count.index].content
  filename = "${path.module}/def-${element(var.names, count.index)}.txt"
}

 

이 테라폼은 별도의 리소스를 생성하는 것은 아니고 반복문을 돌면서 .txt 파일만 여러 개 생성한다. 결과물은 아래 사진과 같은 이름에 내용은 모두 abc 이다.

코드의 두번째 블럭을 설명해보면 local_file.abc 에서 인덱스가 같은 파일의 컨텐츠를 그대로 가져와서 제목을 def-x.txt 로 바꿔서 저장하는 것이다. 

 

 

 

실습 4: 반복문 사용+

 

악분일상 님의 반복문 실습 영상을 보고 실습했다. 감사합니다! https://www.youtube.com/watch?v=enhSdIJ9xxQ

 

이 실습은 위 실습과는 분리되어 있기 때문에 따로 디렉토리를 파서 악분님 레포를 클론하여 환경을 구성했다.

git clone https://github.com/sungwook-practice/t101-study.git

 

 

실습 환경을 구성하기 위해 template 폴더의 terraform 을 apply 해서 vpc를 생성하고 정보를 확인한다.

생성이 잘 되었다!

 

 

 

이제 템플릿에서 하나씩 요구사항을 추가해나갈 것이다.

그 전에 template 을 보면 아래와 같이 구성되어 있다.

실질적으로 우리가 건드려야 할 테라폼 파일은 main.tf, terraform.tfvars, variables.tf 이 3가지 이다

미션1: Subnet 을 추가하고, subnet cidr 을 변수로 입력

 

--BEFORE--

# main.tf
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr

  tags = {
    Name = "terraform VPC"
  }
}

# variables.tf
variable "vpc_cidr" {
  type = string
}

# terraform.tfvars
vpc_cidr            = "192.168.0.0/16"

 

 

--AFTER--

## main.tf 밑에 아래 내용 추가

resource "aws_subnet" "main" {
  vpc_id     = aws_vpc.main.id
  cidr_block = var.subnet_cidr
} #subnet 생성

output "myvpc_id" {
  value = aws_vpc.main.id
} #실습 편의를 위한 출력


## variables.tf 밑에 아래 내용 추가
variable "subnet_cidr" {
  type = string
}


## terraform.tfvars 밑에 아래 내용 추가
subnet_cidr = "192.168.1.0/24"

 

 

 

[!DEBUG] creating EC2 Subnet: InvalidSubnet.Range: The CIDR '10.0.1.0/24' is invalid.

-> variables.tf 를 수정하고 저장하지 않았다

subnet이 잘 생성되었고 콘솔에서 output으로 subnet id 를 출력해준다..

콘솔의 subnets 페이지에서 VPC로 ID를 검색하면 생성된 subnet을 쉽게 찾을 수 있다.

 

 

 

 

미션2: count 를 사용해서 subnet cidr 변수 값을 여러 개 입력받는다

--BEFORE--

## main.tf 의 subnet resource 부분
resource "aws_subnet" "main" {
  vpc_id     = aws_vpc.main.id
  count = length(var.subnet_cidr)
  cidr_block = element(var.subnet_cidr, count.index)
}


## variables.tf 의 subnet 부분
variable "subnet_cidr" {
  type = string
}

## terraform.tfvars 의 subnet_cidr 부분
subnet_cidr = "192.168.1.0/24"

 

 

--AFTER--

## main.tf 의 subnet resource 부분: *count* 를 사용해 List 를 돌면서 subnet 배포
resource "aws_subnet" "main" {
  vpc_id     = aws_vpc.main.id
  count = length(var.subnet_cidr)
  cidr_block = element(var.subnet_cidr, count.index)
}


## variables.tf 의 subnet 부분: string 을 List 로 감싸줌
variable "subnet_cidr" {
  type = list(string)
}

## terraform.tfvars 의 subnet_cidr 부분: list 형태로 subnet 주소 제공
subnet_cidr = ["192.168.1.0/24", "192.168.2.0/24"]

 

마찬가지로 배포가 잘 되었다. 미션1과 동일하게 aws 콘솔에서도 확인할 수 있다

그러나 count는 인덱스를 참조하기 때문에 구성 인스턴스 중 하나가 삭제된다던지 하면 인덱스가 모두 변경되므로 더는 인덱스로 접근할 수 없게 된다. 따라서 각 인스턴스에 id를 부여하는 for_each 메소드를 사용하는 것이 좀 더 안정적이다. 자세한 내용은 공식문서: When to Use for_each Instead of count 를 참고하자

 

다른 실습시나리오에서 for_each를 사용해서 장애상황을 지연한 부분이 있었는데, 이것을 참조하여 count 대신 for_each를 사용해서 미션2의 subnet 여러 개 생성하기를 구현하고 하나씩 설명해보려 한다.

 

 

우선 for_each 문법은 map 과 set 형태의 집합 유형만 지원하므로, variables.tf 에서 list 형태를 map 형태로 바꿔줘야 한다. map 형태로 만들때는 키값도 같이 명시해 줘야 한다. 덤으로 태그도 같이 달거니까 키에 name 변수도 추가해주자.

 

--variables.tf--

## BEFORE
variable "subnet_cidr" {
  type = list(string)
}

## AFTER
variable "subnet_cidr" {
  type = map(object({
    cidr_block = string
    name       = string
    })
  )
}

 

 

 

두번째로 terraform.tfvars 에서 현재 list 형태로 묶여있는 cidr_block 을 map 형태로 바꿔줘야 한다.새로 생긴 name 도 잊지말고 채운다.

 

--terraform.tfvars-- 

## BEFORE
subnet_cidr = ["192.168.1.0/24", "192.168.2.0/24"]

## AFTER
subnet_cidr = {
  subnet1 = {
    cidr_block = "192.168.1.0/24"
    name       = "Subnet 1"
  },
  subnet2 = {
    cidr_block = "192.168.2.0/24"
    name       = "Subnet 2"
  }
}

 

 

 

마지막으로 variable 이 적용될 main.tf 를 수정한다. count 와 마찬가지로 선언을 해줘야 하지만, 인덱스랑 묶어서 돌아가는 count 와는 달리 each 로 지정하게 되므로 좀더 robust 하다.

## BEFORE
resource "aws_subnet" "main" {
  vpc_id     = aws_vpc.main.id

  count = length(var.subnet_cidr)
  cidr_block = element(var.subnet_cidr, count.index)
}


## AFTER
resource "aws_subnet" "main" {
  vpc_id = aws_vpc.main.id

  for_each = var.subnet_cidr
  cidr_block = each.value.cidr_block

  tags = {
    Name = each.value.name
  }
}

output "subnet_ids" {
  value = { for k, v in aws_subnet.main : k => v.id }
}

여기서도 배포가 잘 되었다

 

 

 

마지막으로

잊지 말아요 리소스 삭제

모든 폴더에서 삭제하는 것도 잊지 말아요

terraform destroy -auto-approve

 

정리하고 실습할 때는 엄청 오래 걸리는데 읽으니 정말 금방이네요

읽으시는 분들과 미래의 테라폼이 필요한 나에게 도움이 되었으면!