본문 바로가기

Terraform

[테라폼] 기본 사용3

반응형

1. 조건문 (3항 연산자)

Terraform은 3항 연산자 조건문을 지원하며, 간단한 조건문을 구현할 때 적합

<조건> ? <조건 만족> : <조건 불만족>

 

2. 내장 함수

테라폼은 프로그래밍 언어적인 특성을 가지고 있어, 값의 유형을 변경하거나 조합할 수 있는 내장 함수를 사용 가능

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

 

Challenge1,2

조건문을 활용하여 (각자 편리한) AWS 리소스를 배포하는 코드를 작성

내장 함수를 활용하여 (각자 편리한) 리소스를 배포하는 코드 작성

코드 참고 : https://github.com/junho102/Terraform-Study/tree/main/Week3/challenge1%2C2

resource "aws_vpc" "vpc" {
  cidr_block           = local.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${local.project_name}-vpc-${local.env}-apne2"
  }
}

resource "aws_subnet" "pub_subnet" {
  count             = length(local.pub_cidr)
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = element(local.pub_cidr, count.index)
  availability_zone = ((count.index) % 2) == 0 ? local.zone_id.names[0] : local.zone_id.names[2]

  tags = {
    Name = "${local.project_name}-snet-${local.env}-pub-${((count.index) % 2) == 0 ? "a" : "c"}-apne2"
  }
}

resource "aws_subnet" "primary_pri_subnet" {
  count             = length(local.primary_pri_cidr)
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = element(local.primary_pri_cidr, count.index)
  availability_zone = ((count.index) % 2) == 0 ? local.zone_id.names[0] : local.zone_id.names[2]

  tags = {
    Name = "${local.project_name}-snet-${local.env}-pri-${((count.index) % 2) == 0 ? "a" : "c"}-apne2"
  }
}

 

※ 코드설명

- 3항 연산자를 사용하여 count의 선언된 값을 나누어 나머지 값이 0이면 availability zone a 에 생성하고, 나머지가 0이 아니라면 availability zone c 에 서브넷을 생성

- 내장함수 element를 사용하여 list형식으로 저장된 local변수 pub_cidr의 cidr값을 순차적으로 가져오며 subnet 생성

 

 

3. local-exec & remote-exec 프로비저너 (w. file 프로비저너)

local-exec 프로비저너는 테라폼이 실행되는 환경에서 수행할 커맨드를 정의

remote-exec 프로비저너는 원격지 환경에서 실행할 커맨드와 스크립트를 정의

file 프로비저너는 테라폼을 실행하는 시스템에서 연결 대상으로 파일 또는 디렉토리를 복사하는데 사용

 

Challenge3

AWS EC2 배포시 remote-exec/file 프로비저너를 활용하는 코드를 작성

코드 참고 : https://github.com/junho102/Terraform-Study/tree/main/Week3/challenge3

resource "aws_instance" "apache" {
  ami                    = data.aws_ami.latest_amazon_linux_2.id
  instance_type          = "t2.micro"
  vpc_security_group_ids = [aws_security_group.apache_sg.id]
  key_name               = var.key_pair_name

  tags = { Name = "${var.name}-apache-instance" }
}

resource "null_resource" "file" {
  connection {
    type        = "ssh"
    user        = "ec2-user"
    private_key = file("${path.module}/test.pem")
    host        = aws_instance.apache.public_ip
  }

  provisioner "file" {
    source      = "setup_apache.sh"
    destination = "setup_apache.sh"
  }

  provisioner "remote-exec" {
    inline = [
      "chmod +x setup_apache.sh",
      "./setup_apache.sh"
    ]
  }
}

 코드설명

- remote-exec와 file 프로비저너를 사용하기 위해 ec2에 연결할 connection 정보를 정의

- file 프로비저너를 사용하여 테라폼을 실행하는 환경(Mac)에서 생성한 ec2에 apache 설치 및 index.html파일 정의하는 shell script파일을 복사

- remote-exec 프로비저너를 사용하여 ec2내에서 shell script파일의 실행권한을 주고 실행하는 명령어 정의

 

 

4. terraform_data 리소스 (null_resource 비교)

- Terraform 1.4 버전이 릴리즈되며, 기존 null_resource 를 대체하는 terraform_data 추가

- terraform_data는 null_resource는 별도의 프로바이더 구성이 필요하는점과 추가 프로바이더 없이 테라폼 자체에 포함된 기본 수명주기 관리자를 제공

- null_resource와 terraform_data는 주로 테라폼 프로비저닝 동작을 설계하며 사용자가 의도적으로 프로비저닝하는 동작을 조율해야 하는 상황이 발생하여, 프로바이더가 제공하는 리소스 수명주기 관리만으로는 이를 해결하기 어려울 때 사용

- terraform_data 리소스는 임의의 값(input인수를 통해)을 저장하고 추후 다른 리소스의 수명 주기 트리거(강제 재실행을 위한 - trigger_replace)를 구현하는데 사용 가능하며, 적절한 관리 리소스를 사용할 수 없을 때 프로비저너를 트리거하는데 사용 가능.

 

Challenge4

terraform_data 리소스와 trigger_replace를 사용한 테라폼 코드 작성

코드 참고 : https://github.com/junho102/Terraform-Study/tree/main/Week3/challenge4

resource "aws_network_interface" "ec2_private_ip" {
  subnet_id       = aws_subnet.pub_subnet.id
  private_ips     = ["192.168.1.11"]
  security_groups = [aws_security_group.apache_instance_sg.id]
  tags = {
    Name = "private_network_interface"
  }
}

resource "aws_eip" "apache_ec2_eip" {
  domain = "vpc"
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.latest_amazon_linux_2.id
  instance_type = "t2.micro"
  key_name      = var.key_pair_name

  network_interface {
    network_interface_id = aws_network_interface.ec2_private_ip.id
    device_index         = 0
  }

  tags = { Name = "${var.env}-instance" }
}

resource "aws_eip_association" "eip_assoc" {
  instance_id   = aws_instance.web.id
  allocation_id = aws_eip.apache_ec2_eip.id
}

resource "terraform_data" "ec2_trigger" {
  triggers_replace = [aws_instance.web.private_ip]

  provisioner "remote-exec" {
    connection {
      type        = "ssh"
      user        = "ec2-user"
      private_key = file("${path.module}/test.pem")
      host        = aws_eip.apache_ec2_eip.public_ip
    }
    inline = [
      "sudo yum update -y",
      "sudo yum install -y httpd",
      "sudo systemctl enable httpd",
      "sudo systemctl start httpd"
    ]
    
  }

  depends_on = [aws_eip_association.eip_assoc, aws_instance.web]
}

 코드설명

- terraform_data 리소스 내 remote-exec 프로비저너를 사용하여 ec2에 접속하여 apache를 설치

- terraform_data 리소스 내 triggers_replace를 활용하여 ec2의 private_ip가 바뀌었을때만 동작하도록 설정

 

※ 결과 확인

1. 처음 테라폼 코드를 실행시킬 때는 network_interface의 private_ip를 192.168.1.11로 설정하여 실행

2. terraform_data 리소스 블럭의 triggers_replace를 주석 처리 하여 테라폼 재실행

     (아래와 같이 terraform_data 리소스 블럭의 remote-exec 프로비저너가 다시 실행되는 모습 확인 가능)

> terraform apply -auto-approve
data.aws_availability_zones.az: Reading...
aws_vpc.vpc: Refreshing state... [id=vpc-0bf92912f6a8cbe2b]
aws_eip.apache_ec2_eip: Refreshing state... [id=eipalloc-08123b33f6468c6bf]
data.aws_ami.latest_ubuntu_22_04: Reading...
data.aws_ami.latest_amazon_linux_2: Reading...
data.aws_availability_zones.az: Read complete after 0s [id=ap-northeast-2]
data.aws_ami.latest_ubuntu_22_04: Read complete after 0s [id=ami-0c0ea4662d3cca101]
data.aws_ami.latest_amazon_linux_2: Read complete after 0s [id=ami-0314c6b4d666713d7]
aws_internet_gateway.igw: Refreshing state... [id=igw-0a0216d3f5c6235bf]
aws_route_table.route_table_public: Refreshing state... [id=rtb-0847379a61cc54c4f]
aws_subnet.pub_subnet: Refreshing state... [id=subnet-0164714a54e2d3fb4]
aws_security_group.apache_instance_sg: Refreshing state... [id=sg-06d6207b06f4be11a]
aws_route.public_route: Refreshing state... [id=r-rtb-0847379a61cc54c4f1080289494]
aws_network_interface.ec2_private_ip: Refreshing state... [id=eni-0741b4d2b84d55f8b]
aws_route_table_association.public_asso_rt[0]: Refreshing state... [id=rtbassoc-05e30692a26bd21a7]
aws_instance.web: Refreshing state... [id=i-08c5cc8607b78d475]
aws_eip_association.eip_assoc: Refreshing state... [id=eipassoc-0eb1080611e176396]
terraform_data.ec2_trigger: Refreshing state... [id=730aff3b-3258-288c-d975-fc768362b2de]

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # terraform_data.ec2_trigger must be replaced
-/+ resource "terraform_data" "ec2_trigger" {
      ~ id               = "730aff3b-3258-288c-d975-fc768362b2de" -> (known after apply)
      - triggers_replace = [
          - "192.168.1.11",
        ] -> null
    }

Plan: 1 to add, 0 to change, 1 to destroy.
terraform_data.ec2_trigger: Destroying... [id=730aff3b-3258-288c-d975-fc768362b2de]
terraform_data.ec2_trigger: Destruction complete after 0s
terraform_data.ec2_trigger: Creating...
terraform_data.ec2_trigger: Provisioning with 'remote-exec'...
terraform_data.ec2_trigger (remote-exec): Connecting to remote host via SSH...
terraform_data.ec2_trigger (remote-exec):   Host: 52.78.93.204
terraform_data.ec2_trigger (remote-exec):   User: ec2-user
terraform_data.ec2_trigger (remote-exec):   Password: false
terraform_data.ec2_trigger (remote-exec):   Private key: true
terraform_data.ec2_trigger (remote-exec):   Certificate: false
terraform_data.ec2_trigger (remote-exec):   SSH Agent: true
terraform_data.ec2_trigger (remote-exec):   Checking Host Key: false
terraform_data.ec2_trigger (remote-exec):   Target Platform: unix
terraform_data.ec2_trigger (remote-exec): Connected!
terraform_data.ec2_trigger (remote-exec): Loaded plugins: extras_suggestions,
terraform_data.ec2_trigger (remote-exec):               : langpacks, priorities,
terraform_data.ec2_trigger (remote-exec):               : update-motd
terraform_data.ec2_trigger (remote-exec): No packages marked for update
terraform_data.ec2_trigger (remote-exec): Loaded plugins: extras_suggestions,
terraform_data.ec2_trigger (remote-exec):               : langpacks, priorities,
terraform_data.ec2_trigger (remote-exec):               : update-motd
terraform_data.ec2_trigger (remote-exec): Package httpd-2.4.57-1.amzn2.x86_64 already installed and latest version
terraform_data.ec2_trigger (remote-exec): Nothing to do
terraform_data.ec2_trigger: Creation complete after 3s [id=3cae2b7e-1c10-1f02-a77f-149f7761d033]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed

3. terraform_data 리소스 블럭의 triggers_replace를 주석 처리를 해제하여 테라폼 재실행

     (아래와 같이 terraform_data 리소스 블럭의 triggers_replace를 통해 aws_instance.web.private_ip가 변경될 때만

      동작하기 때문에 remote-exec가 동작하지 않는 모습을 확인 가능)

> terraform apply -auto-approve
data.aws_availability_zones.az: Reading...
aws_eip.apache_ec2_eip: Refreshing state... [id=eipalloc-08123b33f6468c6bf]
data.aws_ami.latest_ubuntu_22_04: Reading...
aws_vpc.vpc: Refreshing state... [id=vpc-0bf92912f6a8cbe2b]
data.aws_ami.latest_amazon_linux_2: Reading...
data.aws_availability_zones.az: Read complete after 0s [id=ap-northeast-2]
data.aws_ami.latest_ubuntu_22_04: Read complete after 0s [id=ami-0c0ea4662d3cca101]
data.aws_ami.latest_amazon_linux_2: Read complete after 0s [id=ami-0314c6b4d666713d7]
aws_internet_gateway.igw: Refreshing state... [id=igw-0a0216d3f5c6235bf]
aws_route_table.route_table_public: Refreshing state... [id=rtb-0847379a61cc54c4f]
aws_subnet.pub_subnet: Refreshing state... [id=subnet-0164714a54e2d3fb4]
aws_security_group.apache_instance_sg: Refreshing state... [id=sg-06d6207b06f4be11a]
aws_route.public_route: Refreshing state... [id=r-rtb-0847379a61cc54c4f1080289494]
aws_route_table_association.public_asso_rt[0]: Refreshing state... [id=rtbassoc-05e30692a26bd21a7]
aws_network_interface.ec2_private_ip: Refreshing state... [id=eni-0741b4d2b84d55f8b]
aws_instance.web: Refreshing state... [id=i-08c5cc8607b78d475]
aws_eip_association.eip_assoc: Refreshing state... [id=eipassoc-0eb1080611e176396]
terraform_data.ec2_trigger: Refreshing state... [id=ad4dd631-d5f5-e8c8-75e6-288babf6bb57]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

 

5. moved Block을 통한 코드 리팩터링

moved block은 테라폼 코드에서 리소스의 이름은 변경되지만 이미 테라폼으로 프로비저닝 환경을 그대로 유지하고자 하는 경우 사용

 

Challenge5

moved block을 사용한 테라폼 코드 리팩터링 테스트

코드 참조 : https://github.com/junho102/Terraform-Study/tree/main/Week3/challenge5

resource "aws_vpc" "vpc" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.env}-vpc"
  }
}

resource "aws_subnet" "pub_subnet3" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = var.pub_cidr[0]
  availability_zone = data.aws_availability_zones.az.names[0]
  tags = {
    Name = "${var.env}-pub-${data.aws_availability_zones.az.names[0]}"
  }
}

resource "aws_subnet" "pub_subnet4" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = var.pub_cidr[1]
  availability_zone = data.aws_availability_zones.az.names[2]
  tags = {
    Name = "${var.env}-pub-${data.aws_availability_zones.az.names[2]}"
  }
}

moved {
  from = aws_subnet.pub_subnet1
  to = aws_subnet.pub_subnet3
}

moved {
  from  = aws_subnet.pub_subnet2
  to = aws_subnet.pub_subnet4
}

 코드설명

- 기존 subnet 생성 리소스의 이름은 pub_subnet1pub_subnet2로 생성한 상태에서 진행

- aws_subnet 리소스 블럭의 이름을 각각 pub_subnet3, pub_subnet4로 변경

- moved 블럭을 사용하여 pub_subnet1이름은 pub_subnet3으로, pub_subnet2이름은 pub_subnet4로 변경

 

※ 결과 확인

> terraform apply -auto-approve
data.aws_availability_zones.az: Reading...
aws_vpc.vpc: Refreshing state... [id=vpc-0226ffc7069cc5471]
data.aws_availability_zones.az: Read complete after 0s [id=ap-northeast-2]
aws_subnet.pub_subnet3: Refreshing state... [id=subnet-061b4b028737002da]
aws_subnet.pub_subnet4: Refreshing state... [id=subnet-0002f8ffdf6295d89]

Terraform will perform the following actions:

  # aws_subnet.pub_subnet1 has moved to aws_subnet.pub_subnet3
    resource "aws_subnet" "pub_subnet3" {
        id                                             = "subnet-061b4b028737002da"
        tags                                           = {
            "Name" = "test-pub-ap-northeast-2a"
        }
        # (16 unchanged attributes hidden)
    }

  # aws_subnet.pub_subnet2 has moved to aws_subnet.pub_subnet4
    resource "aws_subnet" "pub_subnet4" {
        id                                             = "subnet-0002f8ffdf6295d89"
        tags                                           = {
            "Name" = "test-pub-ap-northeast-2c"
        }
        # (16 unchanged attributes hidden)
    }

Plan: 0 to add, 0 to change, 0 to destroy.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

 

6. Terraform Provider alias 

aws를 사용할 때 특정 리소스만 리전을 따로 설정하고 사용할 때 terraform의 provider에서 alias를 지정하여 사용

 

Challenge6

AWS의 S3 버킷을 2개의 리전에서 동작하는 테라폼 코드 작성

코드 참조 : https://github.com/junho102/Terraform-Study/tree/main/Week3/challenge6

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

provider "aws" {
  region = "ap-northeast-1"
  alias  = "tokyo_region"
}

variable "name" {
  default = "ljh"
}

resource "aws_s3_bucket" "seoul_s3_bucket" {
  provider = aws.seoul_region
  bucket   = "${var.name}-seoul-s3"
}

resource "aws_s3_bucket" "tokyo_s3_bucket" {
  provider = aws.tokyo_region
  bucket   = "${var.name}-tokyo-s3"
}

 코드설명

- provider block을 2개 생성하되, alias를 각각 seoul_regiontokyo_region으로 설정

- s3를 생성하기 위한 aws_s3_bucket에 각각 provider를 원하는 리전의 alias로 지정

 

 

Challenge7

Azure 프로바이더를 사용하여 인스턴스 배포

코드 참조: https://github.com/junho102/Terraform-Study/tree/main/Week3/challenge7

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=3.0.0"
    }
  }
}

provider "azurerm" {
  features {}
#   client_id = ""
#   client_secret = ""
#   tenant_id = ""
#   subscription_id = ""
}

# Create resource group
resource "azurerm_resource_group" "sldt_rg" {
  name     = "${var.prefix}-rg"
  location = "${var.region}"
}

# Create virtual network
resource "azurerm_virtual_network" "vnet" {
  name                = "${var.prefix}-vnet"
  location            = azurerm_resource_group.sldt_rg.location
  address_space       = [var.address_space]
  resource_group_name = azurerm_resource_group.sldt_rg.name
}

# Create subnet
resource "azurerm_subnet" "public_subnet" {
  name                 = "${var.prefix}-pub-subnet"
  virtual_network_name = azurerm_virtual_network.vnet.name
  resource_group_name  = azurerm_resource_group.sldt_rg.name
  address_prefixes     = [var.pub_subnet_prefix]

  service_endpoints = ["Microsoft.KeyVault", "Microsoft.Storage"]
}

# Create Network Security Group and rule(bastion)
resource "azurerm_network_security_group" "bastion_nsg" {
  name                = "${var.prefix}-bastion-nsg"
  location            = azurerm_resource_group.sldt_rg.location
  resource_group_name = azurerm_resource_group.sldt_rg.name

  security_rule {
    name                       = "SSH"
    priority                   = 1001
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

# Create network interface(bastion)
resource "azurerm_network_interface" "bastion_nic" {
  name                = "${var.prefix}-bastion-nic"
  location            = azurerm_resource_group.sldt_rg.location
  resource_group_name = azurerm_resource_group.sldt_rg.name

  ip_configuration {
    name                          = "${var.prefix}-nic-configuration"
    subnet_id                     = azurerm_subnet.public_subnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.bastion_pip.id
  }
}

# Connect the security group to the network interface(bastion)
resource "azurerm_network_interface_security_group_association" "bastion_nic_sg_asso" {
  network_interface_id      = azurerm_network_interface.bastion_nic.id
  network_security_group_id = azurerm_network_security_group.bastion_nsg.id
}

# Create public ip(bastion)
resource "azurerm_public_ip" "bastion_pip" {
  name                = "${var.prefix}-pip"
  location            = azurerm_resource_group.sldt_rg.location
  resource_group_name = azurerm_resource_group.sldt_rg.name
  allocation_method   = "Static"
  domain_name_label   = "${var.prefix}-bastion-pip-${random_id.bastion_pip.hex}"
}

# Create virtual machine(bastion)
resource "azurerm_linux_virtual_machine" "bastion" {
  name                = "${var.prefix}-bastion-vm"
  location            = azurerm_resource_group.sldt_rg.location
  resource_group_name = azurerm_resource_group.sldt_rg.name
  network_interface_ids = [azurerm_network_interface.bastion_nic.id]
  size                = var.vm_size[0]
  
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
    disk_size_gb         = "30"
  }

  source_image_reference {
    publisher = var.image_publisher
    offer     = var.image_offer
    sku       = var.image_sku
    version   = var.image_version
  }

  admin_username      = var.vm_username
  computer_name       = "bastion"

  admin_ssh_key {
    username   = var.vm_username
    public_key = file("sshkey/sldt_rsa.pub")
  }

  tags = {
    Name        = "${var.prefix}-bastion-vm"
    environment = var.environment
  }

  depends_on = [azurerm_network_interface_security_group_association.bastion_nic_sg_asso]
}
반응형