본문 바로가기
DevOps/IaC (Infrastructure as Code)

[T1012] Week 3. 테라폼 기본 사용법 정리 (3/3)

by Hwan,. 2023. 7. 22.
728x90
반응형

조건문

 Terraform에는 다른 언어처럼 조건문을 위한 if 키워드가 없기 때문에 3항 연산자를 사용하여 조건문을 작성해야 한다.

예를들면 아래처럼 환경에 대한 정보를 변수로 입력받아 product 환경일 경우 5번, 아닐 경우 1번만 리소스를 생성하도록 할 수 있다.

resource "aws_instance" "hwan001_instance" {
  count = var.env == "prod" ? 5 : 1
}

 

만약 dev 환경일 경우에만 리소스를 생성하고 싶다면 아래처럼 작성할 수 있다.

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

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]  # Canonical
}

resource "aws_instance" "hwan001-ec2-dev" {
  count				= var.env == "dev" ? 1 : 0
  ami				= data.aws_ami.ubuntu.id
  instance_type			= "t2.micro"
  subnet_id			= aws_subnet.primary_az.id
  vpc_security_group_ids	= [aws_security_group.hwan001-sg.id]
  associate_public_ip_address	= true

  tags = {
    Name = "hwan001-ec2"
    env  = var.env
  }
}

resource "aws_instance" "hwan001-ec2-prod" {
  count				= var.env == "prod" ? 3 : 0
  ami				= data.aws_ami.ubuntu.id
  instance_type			= "t2.micro"
  subnet_id			= aws_subnet.primary_az.id
  vpc_security_group_ids	= [aws_security_group.hwan001-sg.id]
  associate_public_ip_address	= true

  tags = {
    Name = "hwan001-ec2"
    env  = var.env
  }
}

 


내장함수

 내장 함수는 테라폼에 내장되어 있는 함수들로 여러 상황에서 유용하게 사용될 수 있으며, 아래와 같은 여러 이점들이 있다. 

  • 간결성 : 코드를 훨씬 간결하고 읽기 쉽게 만들 수 있다. 
  • 확장성 : 인프라 구조를 보다 쉽게 확장하거나 수정할 수 있다.
  • 유연성 : 다양한 데이터 타입(문자열, 숫자, 목록, 맵 등)을 처리하는데 유연성을 제공합니다.
  • 유효성 검사 : 내장 함수를 사용하면 입력값의 유효성을 쉽게 검사할 수 있다.
  • 기능성 : file이나 timestamp같은 일부 내장 함수는 특정 기능을 제공합니다.

 

아래는 많이 사용하는 내장 함수들이다. 

file : 입력받은 파일의 경로를 읽어 반환한다.

resource "aws_iam_policy" "iam_policy" {
  name        = "iam_policy"
  path        = "/"

  policy = file("policy.json")
}

 

format, formatlist : 문자열의 포맷을 정의 할 수 있다.

output "instance_id" {
  value = format("Instance ID is %s", aws_instance.hwan001-ec2.id)
}

 

element : 주어진 목록에서 특정 요소를 가져온다.

output "first_subnet" {
  value = element(aws_subnet.hwan001-subnet.*.id, 0)
}

 

lookup : 주어진 map 에서 특정 값을 찾는다.

output "instance_public_ip" {
  value = lookup(aws_instance.hwan001-ec2.*.attributes, "public_ip")
}

 

count.index : 현재 인덱스의 번호를 반환하며, 보통 반복적으로 리소스를 사용할 때 count와 같이 사용한다.

resource "aws_instance" "hwan001-ec2" {
  count         = 3
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  tags = {
    Name = "hwan001-ec2-instance-${count.index}"
  }
}

 

split, join : 문자열을 특정 구분자로 분리하거나 합칠 때 사용한다.

output "subnet_ids" {
  value = join(", ", aws_subnet.hwan001-subnet.*.id)
}

 

cidrhost :  네트워킹 설정을 할 때 유용하다. 예를 들면 여러 서브넷이 있는 VPC를 설정할 때, 각 서브넷의 IP 범위를 계산하는데 사용할 수 있다.

output "subnet_ip" {
  value = cidrhost("10.0.0.0/16", 4)
}

위 코드처럼 사용할 경우 10.0.0.0/16 의 4번째 호스트의 IP 주소(10.0.0.4)를 반환한다.

 

테라폼에서는 사용자가 함수를 직접 정의할 수는 없지만 다양한 함수들을 제공한다. 아래 문서에서 더 많은 정보를 얻을 수 있다.

https://developer.hashicorp.com/terraform/language/functions

 

Functions - Configuration Language | Terraform | HashiCorp Developer

An introduction to the built-in functions that you can use to transform and combine values in expressions.

developer.hashicorp.com


프로비저너

 프로바이더에서 제공되지 않는 파일 복사, 커맨드 등의 역할을 수행할 수 있지만 해당 코드로 인한 결과는 tfstate에 동기화되지 않는다.

따라서 실행 시 마다 실행의 결과가 항상 동일하지 않을 수도 있다.

 예를 들어 local-exec 프로비저너를 사용하면 테라폼을 활용해 리소스를 프로비저닝한 후 해당 리소스 내부를 앤서블 플레이북으로 프로비저닝 하는 등의 작업이 가능하다. 

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

resource "aws_instance" "hwan001-ec2" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"

  provisioner "local-exec" {
    command = "ansible-playbook -u ubuntu --private-key=${var.private_key_path} -i '${self.public_ip},' playbook.yml"
  }
}

 

하지만 해당 방식은 ssh 접속을 위한 정보가 필요하고, 여러 변수로 인해 안정적으로 프로비저닝되지 않을 수도 있다.

테라폼에서는 최근부터 terraform-provider-ansible를 제공한다. 

해당 프로바이더를 사용하면 tfstate 파일을 사용하여 인프라를 추적할 수 있기 때문에 좀 더 안정적으로 앤서블을 사용할 수 있다.

terraform {
  required_providers {
    ansible = {
      source = "ansible/ansible"
      version = "1.1.0"
    }
  }
}

provider "ansible" {}

resource "ansible_playbook" "playbook" {
  playbook   = "playbook.yml"
  name       = "host-1.example.com"
  replayable = true

  extra_vars = {
    var_a = "Some variable"
    var_b = "Another variable"
  }
}

 

자세한 정보는 아래링크에서 확인할 수 있다.

https://registry.terraform.io/providers/ansible/ansible/latest/docs

 

Terraform Registry

 

registry.terraform.io

https://github.com/ansible/terraform-provider-ansible/tree/main

 

GitHub - ansible/terraform-provider-ansible: community terraform provider for ansible

community terraform provider for ansible. Contribute to ansible/terraform-provider-ansible development by creating an account on GitHub.

github.com

 


Null Resource와 Terraform data

Null Resource

 Null Resource는 아무런 액션도 취하지 않는 리소스로 그 자체로는 아무런 효과가 없지만, 특정 조건에 따라 다른 리소스를 트리거하는 등의 복잡한 의존성이 필요한 경우에 유용하게 사용할 수 있다.

예를 들면 특정 조건에 따라 스크립트를 실행하거나, 다른 리소스가 변경될 때마다 새로운 리소스를 생성하는 등의 작업에 사용할 수 있다.

resource "null_resource" "example" {
  triggers = {
    always_run = "${timestamp()}"
  }

  provisioner "local-exec" {
    command = "echo Hello, World!"
  }
}

 

Null Resource는 주로 아래의 경우에 사용된다.

  • 프로비저닝 수행 과정에서 명령어 실행
  • 프로비저너와 함께 사용
  • 모듈, 반복문, 데이터 소스, 로컬 변수와 함께 사용
  • 출력을 위한 데이터 가공

 

예를 들어, AWS EC2 인스턴스를 프로비저닝하면서 웹서비스를 실행시키고 싶지만, 웹서비스 설정에는 노출되어야 하는 고정된 외부 IP가 포함된 구성이 필요하기 때문에 aws_eip 리소스를 생성해야 한다.

 

이런 경우 아래처럼 코드를 작성하면 aws_eip 부분에선 aws_instance.example.id가 필요하고, aws_instance 에선 aws_eip.myeip.public_ip가 필요하기 때문에 Cycle이 발생한다.

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

resource "aws_security_group" "instance" {
  name = "hwan001-sg"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"
  subnet_id              = "subnet-000000" 
  private_ip             = "172.31.1.100"
  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "Test page" > 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" {
  instance = aws_instance.example.id
  associate_with_private_ip = "172.31.1.100"
}

output "public_ip" {
  value       = aws_instance.example.public_ip
}

 

이런 경우 Null Resource를 사용해서 2개의 리소스 생성에 시간 간격을 두어 해결할 수도 있다.

...

resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"
  subnet_id              = "subnet-000000" 
  private_ip             = "172.31.1.100"
  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "Test page" > index.html
              nohup busybox httpd -f -p 80 &
              EOF

  tags = {
    Name = "Single-WebSrv"
  }
}

resource "aws_eip" "myeip" {
  instance = aws_instance.example.id
  associate_with_private_ip = "172.31.1.100"
}

resource "null_resource" "foo" {
  triggers = {
    ec2_id = aws_instance.example.id # instance의 id가 변경되는 경우 재실행
  }

  provisioner "remote-exec" {
    inline = [
      "echo ${aws_eip.myeip.public_ip}"
     ]
  }
}

output "public_ip" {
  value       = aws_instance.example.public_ip
}

 

Terraform_data

  Terraform 1.4버전 부터는 Null Resource를 대체하기 위해 terraform_data라는 리소스를 지원한다.

Null Resource를 사용하려면 null 공급자를 추가해줘야 한다. 하지만 Terraform Data는 해당 공급자를 추가하지 않아도 동작한다.

resource "terraform_data" "apply" {
  provisioner "local-exec" {
    command     = "echo ${aws_eip.myeip.public_ip}"
  }

  triggers_replace = [
    aws_instance.example.id
  ]
}

terraform_data 사용 시
null_resource 사용 시

좀 더 자세한 정보는 아래 링크 참고

https://developer.hashicorp.com/terraform/language/resources/terraform-data

 

The terraform_data Managed Resource Type | Terraform | HashiCorp Developer

Retrieves the root module output values from a Terraform state snapshot stored in a remote backend.

developer.hashicorp.com

 


moved 블록

 테라폼의 tfstate에서 리소스 주소의 이름이 변경되면 기존 리소스는 삭제되고 새로운 리소스가 생성된다. 하지만 기존 리소스의 이름만 변경하고 싶은 경우가 있다.

리소스의 이름은 변경되지만 이미 테라폼으로 프로비저닝된 환경을 그대로 유지하려고 하는 경우엔 moved 블록을 사용할 수 있다. (Terraform 1.1 이상부터 지원)

 

아래 코드로 ec2 instance를 생성했다.

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

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]  # Canonical
}

# EC2 Instance 생성
resource "aws_instance" "hwan001-ec2" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  subnet_id                   = aws_subnet.primary_az.id
  vpc_security_group_ids      = [aws_security_group.hwan001-sg.id]
  associate_public_ip_address = true

  tags = {
    Name = "hwan001-ec2"
  }
}

 

해당 리소스 태그를 moved로 변경해보자.

resource "aws_instance" "hwan001-ec2-new" {
  ami				= data.aws_ami.ubuntu.id
  instance_type			= "t2.micro"
  subnet_id			= aws_subnet.primary_az.id
  vpc_security_group_ids	= [aws_security_group.hwan001-sg.id]
  associate_public_ip_address	= true

  tags = {
    Name = "hwan001-ec2-new"
  }
}

moved {
  from = aws_instance.hwan001-ec2
  to   = aws_instance.hwan001-ec2-new
}

moved를 사용해서 리소스의 삭제없이 인스턴스의 태그 정보를 변경했다. 

해당 작업 후 moved 부분을 제거해주면 된다.

 

(cc. moved를 사용해도 associate_public_ip_address이나 instance_type등을 변경하고 apply하면 리소스가 재생성된다.)

 


CLI를 위한 시스템 환경 변수

 Terraform에서는 환경 변수를 사용하여 다양한 설정을 구성할 수 있다.

  • TF_LOG : 로그 레벨 (TRACE, DEBUG, INFO, WARN, ERROR)
  • TF_LOG_PATH : 로그 파일의 경로, TF_LOG 환경 변수가 설정되어 있고 TF_LOG_PATH가 설정되어 있으면 로그를 기록한다.
  • TF_VAR_name : Terraform 변수의 값 (name은 설정하려는 변수의 이름, ex) TF_VAR_instance_type=t2.micro)
  • TF_INPUT : 사용자 입력 요청 여부를 제어, false일 경우 사용자 입력을 요청 안함
  • TF_IN_AUTOMATION : 자동화 도구 내에서 실행되고 있는지 여부, 이 환경 변수가 설정되어 있으면 자동화 도구와의 호환성을 향상시킨다.
  • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN : AWS 프로바이더 인증 키
  • TF_CLI_ARGS / TF_CLI_ARGS_subcommand : 테라폼 실행 시 추가할 인수를 정의
  • TF_DATA_DIR : State 저장 백엔드 설정과 같은 작업 디렉터리별 데이터를 보관하는 위치를 지정

https://developer.hashicorp.com/terraform/cli/config/environment-variables

 

Environment Variables | Terraform | HashiCorp Developer

Learn to use environment variables to change Terraform's default behavior. Configure log content and output, set variables, and more.

developer.hashicorp.com

 


프로바이더

 테라폼은 terraform 바이너리 파일을 시작으로 로컬 환경에나 배포 서버와 같은 원격 환경에서 원하는 대상(프로바이더가 제공하는 API)을 호출하는 방식으로 실행된다. 각 프로바이더의 API 구현은 서로 다르지만 테라폼의 고유 문법으로 동일한 동작을 수행하도록 구현되어 있다.

https://malwareanalysis.tistory.com/619

 

다수의 프로바이더를 로컬 이름 지정

 required_providers 블록을 사용하여 다수의 프로바이더를 로컬 이름으로 지정해 사용할 수 있다.

예를 들면 아래는 여러 프로바이더가 제공해주는 동일한 http 관련 data 블록을 사용하는 예제이다.

terraform {
  required_providers {
    http = {
      source = "hashicorp/http"
      version = "3.4.0"
    }
    aws-http = {
      source = "terraform-aws-modules/http"
      version = "2.4.1"
    }
  }
}

data "http" "example1" {
  provider = http
  url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"

  request_headers = {
    Accept = "application/json"
  }
}

data "http" "example2" {
  provider = http
  url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"

  request_headers = {
    Accept = "application/json"
  }
}

 

해당 코드를 테라폼으로 apply하면 tfstate파일 내에서 해당 요청을 서로 다른 공급자로 얻어오는 걸 볼 수 있다. 

{
  ...
  "resources": [
    {
      "mode": "data",
      "type": "http",
      "name": "example1",
      "provider": "provider[\"registry.terraform.io/hashicorp/http\"]",
      "instances": [ ... ]
    },
    {
      "mode": "data",
      "type": "http",
      "name": "example2",
      "provider": "provider[\"registry.terraform.io/terraform-aws-modules/http\"]",
      "instances": [ ... ]
    }
  ],
  ...
}

 

단일 프로바이더의 다중 정의

 위의 경우와는 반대로 하나의 프로바이더를 여러번 정의해서 사용해야 하는 경우가 있다.

예를 들면 멀티 리전을 정의하는 경우인데, 아래처럼 alias를 활용해 같은 리소스 내부에서 리전을 지정해 줄 수 있다.

provider "aws" {
  region = "ap-southeast-1"
}

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

resource "aws_instance" "app_server1" {
  ami           = "ami-06b79cf2aee0d5c92"
  instance_type = "t2.micro"
}

resource "aws_instance" "app_server2" {
  provider      = aws.seoul
  ami           = "ami-0ea4d4b8dc1e46212"
  instance_type = "t2.micro"
}

 

하지만 해당 방식은 지역간 지연 시간, 고유 ID, 최종 일관성 등 여러가지 고려사항이 많고, 한 리전이 다운되었을 경우 전체 plan과 apply가 실패하기 때문에 다른 리전의 변경 사항도 적용할 수 없게 된다.

프로덕션 수준에서는 환경을 완전히 격리하여 서로 간의 영향도를 최소화하는 방법이 더 좋을 것 같다.


 

 

728x90
반응형

댓글