Terraform 101 - 4기 (2주차)

김성중·2024년 6월 21일
1

Terraform

목록 보기
2/7

가시다(gasida) 님이 진행하는 Terraform T101 4기 실습 스터디 게시글입니다.
책 '테라폼으로 시작하는 IaC' 를 참고하였고, 스터디하면서 도움이 될 내용들을 추가적으로 정리하였습니다.

2주차는 테라폼 기본 사용법으로 데이터소스, 입력변수, Local 지역값, 출력에 대해 학습하였으며
과제수행 방식으로 학습한 내용을 간략히 정리하였습니다.

실습환경 준비

  • Terminal 관련 설정

    # Credential 설정 시 복수로 AWS 계정을 사용(관리)할 경우 프로파일 사용을 추천(예, t101)
    $ aws configure --profile t101
    AWS Access Key ID [None]: AKIA******DWVHK27PF2
    AWS Secret Access Key [None]: fRQbW******KP
    Default region name [None]: ap-northeast-2
    Default output format [None]: json
    
    # aws cli 명령어 실행 시 --profile 프로파일명 옵션 값 사용
    $ aws sts get-caller-identity --profile=t101
    {
        "UserId": "AKIA******DWVHK27PF2",
        "Account": "5**********7",
        "Arn": "arn:aws:iam::5**********7:user/terraform"
    }
    
    # 매번 aws cli 실행시 마다 profile 옵션 값 생략 할려면?  
    $ export AWS_PROFILE=t101
    
    # aws cli 실행결과를 바로 출력하고 내용이 많을 경우 스크롤 하기 위해 페이징 기능 Disable 
    $ export AWS_PAGER=""
  • Terraform provider "aws" 설정

    • aws configure 명령 실행 시 옵션(--profile)으로 명시한 프로파일을 코드에 사용
    provider "aws" {
      region                   = "ap-northeast-2"
      shared_credentials_files = ["~/.aws/credentials"]
      profile                  = "t101"
    }
  • Terraform version 확인

    $ terraform version
    Terraform v1.8.5
    on darwin_arm64
    + provider registry.terraform.io/hashicorp/aws v5.55.0

도전과제

1. 리전내에서 사용 가능한 가용영역 목록을 가져와 VPC 생성(데이터소스 이용), 2. 리소스이름(myvpc, mysubnet1 등)을 자신의 닉네임으로 변경해서 배포

리소스의 유형과 리소스의 이름 차이를 알고, 리소스의 속성(예, ID)를 참조하는 방법에 대해서 익숙해지자

테라폼 코드

  • main.tf
    - aws cloud에 리소스를 생성하기 위해 provider aws와 credential의 파일경로와 profile을 코드화

    terraform {
      required_version = "~> 1.8"
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.55" # Lock version to avoid unexpected problems
        }
      }
    }
    
    provider "aws" {
      region                   = "ap-northeast-2"
      shared_credentials_files = ["~/.aws/credentials"]
      profile                  = "t101"
    }
  • data.tf
    - 데이터소스 블록은 1) 테라폼으로 정의되지 않은 외부 리소스 2) 저장된 정보를 테라폼 내에서 참조할 때 사용

    data "aws_availability_zones" "seoul" {
      state = "available"
    }
  • variable.tf
    - vpc_cidr 변수에 10.0.0.0/20 cidr를 할당
    - public_subnets 변수에 열거형으로 4개 subnet의 cidr를 할당

    variable "vpc_cidr" {
      type    = string
      default = "10.0.0.0/20"
    }
    
    variable "public_subnets" {
      type    = list(string)
      default = ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
    }
  • vpc.tf
    - vpc와 subnet이 동일 리소스이름(t101-sjkim)을 사용하여도 리소스유형 + 리소스이름 조합으로 참조됨으로 미충돌
    - vpc_ip와 cidr_block 변수는 variable.tf 파일에 정의된 변수값을 참조(동일 디렉토리에 있으면 타 파일에 위치해도 됨)

    • availability_zone의 경우는 데이터소스의 값을 참조함
    • 반복문중 하나인 count를 사용하여 public_subnets의 개수 만큼 반복하여 가용영역 이름(ap-northeast-2a, ap-northeast-2b 등)이 순차적으로 치환 됨
    resource "aws_vpc" "t101-sjkim" {
      cidr_block = var.vpc_cidr
    
      tags = {
        Name = "vpc-t101-sjkim"
      }
    }
    
    resource "aws_subnet" "t101-sjkim" {
      count = length(var.public_subnets)
    
      vpc_id            = aws_vpc.t101-sjkim.id
      cidr_block        = var.public_subnets[count.index]
      availability_zone = data.aws_availability_zones.seoul.names[count.index]
    }

코드 실행

  • terraform init

    Initializing the backend...
    
    Initializing provider plugins...
    - Reusing previous version of hashicorp/aws from the dependency lock file
    - Using previously-installed hashicorp/aws v5.55.0
    
    Terraform has been successfully initialized!
    
    You may now begin working with Terraform. Try running "terraform plan" to see
    any changes that are required for your infrastructure. All Terraform commands
    should now work.
    
    If you ever set or change modules or backend configuration for Terraform,
    rerun this command to reinitialize your working directory. If you forget, other
    commands will detect it and remind you to do so if necessary.
    
  • terraform validate(구문 검사) && terraform fmt(코드 포멧팅)

    $ terraform validate
    Success! The configuration is valid.
    
    $ terraform fmt
  • terraform plan -out=tfplan
    - 1개 vpc, 4개 subnet이 생성될 예정이라고 알려 줌

    data.aws_availability_zones.seoul: Reading...
    data.aws_availability_zones.seoul: Read complete after 0s [id=ap-northeast-2]
    
    Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
      + create
    
    Terraform will perform the following actions:
    
      # aws_subnet.t101-sjkim[0] will be created
      + resource "aws_subnet" "t101-sjkim" {
          + arn                                            = (known after apply)
          + assign_ipv6_address_on_creation                = false
          + availability_zone                              = "ap-northeast-2a"
          + availability_zone_id                           = (known after apply)
          + cidr_block                                     = "10.0.0.0/24"
          + enable_dns64                                   = false
          + enable_resource_name_dns_a_record_on_launch    = false
          + enable_resource_name_dns_aaaa_record_on_launch = false
          + id                                             = (known after apply)
          + ipv6_cidr_block_association_id                 = (known after apply)
          + ipv6_native                                    = false
          + map_public_ip_on_launch                        = false
          + owner_id                                       = (known after apply)
          + private_dns_hostname_type_on_launch            = (known after apply)
          + tags_all                                       = (known after apply)
          + vpc_id                                         = (known after apply)
        }
    
      # aws_subnet.t101-sjkim[1] will be created
      + resource "aws_subnet" "t101-sjkim" {
      
      ....
        
      # aws_subnet.t101-sjkim[3] will be created
      + resource "aws_subnet" "t101-sjkim" {
          + arn                                            = (known after apply)
      ...
              + vpc_id                                         = (known after apply)
        }
    
      # aws_vpc.t101-sjkim will be created
      + resource "aws_vpc" "t101-sjkim" {
          + arn                                  = (known after apply)
          + cidr_block                           = "10.0.0.0/20"
          + default_network_acl_id               = (known after apply)
          + default_route_table_id               = (known after apply)
          + default_security_group_id            = (known after apply)
          + dhcp_options_id                      = (known after apply)
          + enable_dns_hostnames                 = (known after apply)
          + enable_dns_support                   = true
          + enable_network_address_usage_metrics = (known after apply)
          + id                                   = (known after apply)
          + instance_tenancy                     = "default"
          + ipv6_association_id                  = (known after apply)
          + ipv6_cidr_block                      = (known after apply)
          + ipv6_cidr_block_network_border_group = (known after apply)
          + main_route_table_id                  = (known after apply)
          + owner_id                             = (known after apply)
          + tags                                 = {
              + "Name" = "vpc-t101-sjkim"
            }
          + tags_all                             = {
              + "Name" = "vpc-t101-sjkim"
            }
        }
    
    Plan: 5 to add, 0 to change, 0 to destroy.
    
    ────────────────────────────────────────────────────────────────────────────────────────────────────────
    
    Saved the plan to: tfplan
    
    To perform exactly these actions, run the following command to apply:
        terraform apply "tfplan"
    
  • terraform apply tfplan

    aws_vpc.t101-sjkim: Creating...
    aws_vpc.t101-sjkim: Creation complete after 1s [id=vpc-01ac5f29802044f56]
    aws_subnet.t101-sjkim[1]: Creating...
    aws_subnet.t101-sjkim[3]: Creating...
    aws_subnet.t101-sjkim[2]: Creating...
    aws_subnet.t101-sjkim[0]: Creating...
    aws_subnet.t101-sjkim[2]: Creation complete after 1s [id=subnet-03d34728de4f242e0]
    aws_subnet.t101-sjkim[1]: Creation complete after 1s [id=subnet-0145aaf2116093007]
    aws_subnet.t101-sjkim[3]: Creation complete after 1s [id=subnet-097d71134e6f1dd76]
    aws_subnet.t101-sjkim[0]: Creation complete after 1s [id=subnet-0406b76a46246823f]
    
    Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
    
  • terraform state list

    data.aws_availability_zones.seoul
    aws_subnet.t101-sjkim[0]
    aws_subnet.t101-sjkim[1]
    aws_subnet.t101-sjkim[2]
    aws_subnet.t101-sjkim[3]
    aws_vpc.t101-sjkim
  • terraform state show data.aws_availability_zones.seoul

    # data.aws_availability_zones.seoul:
    data "aws_availability_zones" "seoul" {
        group_names = [
            "ap-northeast-2",
        ]
        id          = "ap-northeast-2"
        names       = [
            "ap-northeast-2a",
            "ap-northeast-2b",
            "ap-northeast-2c",
            "ap-northeast-2d",
        ]
        state       = "available"
        zone_ids    = [
            "apne2-az1",
            "apne2-az2",
            "apne2-az3",
            "apne2-az4",
        ]
    }
  • terraform console

    $ terraform console
    > data.aws_availability_zones.seoul
    {
      "all_availability_zones" = tobool(null)
      "exclude_names" = toset(null) /* of string */
      "exclude_zone_ids" = toset(null) /* of string */
      "filter" = toset(null) /* of object */
      "group_names" = toset([
        "ap-northeast-2",
      ])
      "id" = "ap-northeast-2"
      "names" = tolist([
        "ap-northeast-2a",
        "ap-northeast-2b",
        "ap-northeast-2c",
        "ap-northeast-2d",
      ])
      "state" = "available"
      "timeouts" = null /* object */
      "zone_ids" = tolist([
        "apne2-az1",
        "apne2-az2",
        "apne2-az3",
        "apne2-az4",
      ])
    }
    > exit 

코드 실행결과

  • VPC : 서울리전에 10.0.0.0/20 CIDR로 생성됨!

  • Subnet : 4개 가용 영역에 Subnet이 생성됨!

리소스 삭제

  • terraform destroy -auto-approve

    data.aws_availability_zones.seoul: Reading...
    aws_vpc.t101-sjkim: Refreshing state... [id=vpc-01ac5f29802044f56]
    data.aws_availability_zones.seoul: Read complete after 0s [id=ap-northeast-2]
    aws_subnet.t101-sjkim[1]: Refreshing state... [id=subnet-0145aaf2116093007]
    aws_subnet.t101-sjkim[2]: Refreshing state... [id=subnet-03d34728de4f242e0]
    aws_subnet.t101-sjkim[0]: Refreshing state... [id=subnet-0406b76a46246823f]
    aws_subnet.t101-sjkim[3]: Refreshing state... [id=subnet-097d71134e6f1dd76]
    
    Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
      - destroy
    
    Terraform will perform the following actions:
    
      # aws_subnet.t101-sjkim[0] will be destroyed
      - resource "aws_subnet" "t101-sjkim" {
          - arn                                            = "arn:aws:ec2:ap-northeast-2:5**********7:subnet/subnet-0406b76a46246823f" -> null
          - assign_ipv6_address_on_creation                = false -> null
          - availability_zone                              = "ap-northeast-2a" -> null
          - availability_zone_id                           = "apne2-az1" -> null
          - cidr_block                                     = "10.0.0.0/24" -> null
          - enable_dns64                                   = false -> null
          - enable_lni_at_device_index                     = 0 -> null
          - enable_resource_name_dns_a_record_on_launch    = false -> null
          - enable_resource_name_dns_aaaa_record_on_launch = false -> null
          - id                                             = "subnet-0406b76a46246823f" -> null
          - ipv6_native                                    = false -> null
          - map_customer_owned_ip_on_launch                = false -> null
          - map_public_ip_on_launch                        = false -> null
          - owner_id                                       = "5**********7" -> null
          - private_dns_hostname_type_on_launch            = "ip-name" -> null
          - tags                                           = {} -> null
          - tags_all                                       = {} -> null
          - vpc_id                                         = "vpc-01ac5f29802044f56" -> null
            # (4 unchanged attributes hidden)
        }
    
      # aws_subnet.t101-sjkim[1] will be destroyed
      - resource "aws_subnet" "t101-sjkim" {
    ...
    
      # aws_vpc.t101-sjkim will be destroyed
      - resource "aws_vpc" "t101-sjkim" {
          - arn                                  = "arn:aws:ec2:ap-northeast-2:5**********7:vpc/vpc-01ac5f29802044f56" -> null
          - assign_generated_ipv6_cidr_block     = false -> null
          - cidr_block                           = "10.0.0.0/20" -> null
          - default_network_acl_id               = "acl-0006e0c11013a8f6d" -> null
          - default_route_table_id               = "rtb-07bec4aed7f9a3630" -> null
          - default_security_group_id            = "sg-01200af4543416ddc" -> null
          - dhcp_options_id                      = "dopt-6bf6b600" -> null
          - enable_dns_hostnames                 = false -> null
          - enable_dns_support                   = true -> null
          - enable_network_address_usage_metrics = false -> null
          - id                                   = "vpc-01ac5f29802044f56" -> null
          - instance_tenancy                     = "default" -> null
          - ipv6_netmask_length                  = 0 -> null
          - main_route_table_id                  = "rtb-07bec4aed7f9a3630" -> null
          - owner_id                             = "5**********7" -> null
          - tags                                 = {
              - "Name" = "vpc-t101-sjkim"
            } -> null
          - tags_all                             = {
              - "Name" = "vpc-t101-sjkim"
            } -> null
            # (4 unchanged attributes hidden)
        }
    
    Plan: 0 to add, 0 to change, 5 to destroy.
    aws_subnet.t101-sjkim[1]: Destroying... [id=subnet-0145aaf2116093007]
    aws_subnet.t101-sjkim[3]: Destroying... [id=subnet-097d71134e6f1dd76]
    aws_subnet.t101-sjkim[0]: Destroying... [id=subnet-0406b76a46246823f]
    aws_subnet.t101-sjkim[2]: Destroying... [id=subnet-03d34728de4f242e0]
    aws_subnet.t101-sjkim[0]: Destruction complete after 0s
    aws_subnet.t101-sjkim[1]: Destruction complete after 0s
    aws_subnet.t101-sjkim[3]: Destruction complete after 0s
    aws_subnet.t101-sjkim[2]: Destruction complete after 0s
    aws_vpc.t101-sjkim: Destroying... [id=vpc-01ac5f29802044f56]
    aws_vpc.t101-sjkim: Destruction complete after 1s
    
    Destroy complete! Resources: 5 destroyed.

3. 입력변수, 4. local, 5. count, 6. cidrsubnet Funcion 문을 활용해서 리소스 배포

입력 변수는 인프라를 구성하는 데 필요한 속성 값을 정의해 코드의 변경 없이 여러 인프라를 생성하는 데 목적이 있음

시스템 환경변수가 코드 보다 우선순위가 있어서 동적으로 코드 내용을 변경할 수 있음

Project명과 Environment명을 입력받아 Tag Name으로 활용

local은 사용자가 테라폼 코드를 구현할 때 값이나 표현식을 반복적으로 사용할 수 있는 편의성을 제공
블록 사용시 선언되는 인수에 표현되는 값은 상수 외에도 리소스의 속성, 변수의 값들도 조합해 정의할 수 있음

count block 사용 시 리전의 가용영역 수 만큼 반복적 실행으로 subnet 관련 코딩 량을 줄일 수 있음

cidrsubnet 내장함수를 이용하면 cidr 열거형 변수 값을 간략하게 줄일 수 있음

테라폼 코드

  • main.tf
    - aws cloud에 리소스를 생성하기 위해 provider aws와 credential의 파일경로와 profile을 코드화

    terraform {  
      required_version = "~> 1.8"  
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.55" # Lock version to avoid unexpected problems
        }
      } 
    }
    
    provider "aws" {
      region                   = "ap-northeast-2"
      shared_credentials_files = ["~/.aws/credentials"]
      profile                  = "t101"
    }
  • variable.tf

    • 변수 pjt, env의 기본 값을 선언(시스템 환경 변수 입력 없는 경우 기본적으로 적용 됨)
    variable "pjt" {
      type    = string
      default = "t101"
    }
    
    variable "env" {
      type    = string
      default = "dev"
    }
    
    variable "vpc_cidr" {
      type    = string
      default = "10.0.0.0/20"
    }
  • local.tf

    • pjt 와 env의 값을 조합하여 맵으로 구조화 함 (과제4)
    locals {
      common_tags = {
        Project          = var.pjt,
        Environment      = var.env,
        TerraformManaged = "true"
      }
    }
  • data.tf

    data "aws_availability_zones" "seoul" {
      state = "available"
    }
  • vpc.tf

    • local.common_tags 값을 tags에 적용하여 리소스 식별을 용이하게 함 (과제4)
    • count를 활용하여 aws_subnet 리소스 유형을 4번 유사하게 반복 코딩하지 않아 소스를 간결하게 함
    • subnet 리소스 정의 시 cidr_block 값을 열거하지 않고 cidrsubnet 함수로 코드를 간소화 함 (과제6)
    resource "aws_vpc" "t101-sjkim" {
      cidr_block = var.vpc_cidr
    
      enable_dns_hostnames = true
      enable_dns_support   = true
    
      tags = merge(
        local.common_tags,
        {
          Name = "vpc-${var.pjt}-${var.env}"
        }
      )
    }
    
    resource "aws_subnet" "t101-sjkim" {
      count = length(data.aws_availability_zones.seoul.names)
    
      vpc_id            = aws_vpc.t101-sjkim.id
      cidr_block        = cidrsubnet(var.vpc_cidr, 4, count.index)
      availability_zone = data.aws_availability_zones.seoul.names[count.index]
    
      tags = merge(
        local.common_tags,
        {
          Name = "sbn-${var.pjt}-${var.env}-pub-${data.aws_availability_zones.seoul.zone_ids[count.index]}"
        }
      )
    
    }
    
    resource "aws_internet_gateway" "t101-sjkim" {
      vpc_id = aws_vpc.t101-sjkim.id
    
      tags = merge(
        local.common_tags,
        {
          Name = "igw-${var.pjt}-${var.env}"
        }
      )
    
    }
    
    resource "aws_route_table" "t101-sjkim-pub-rt" {
      vpc_id = aws_vpc.t101-sjkim.id
    
      tags = merge(
        local.common_tags,
        {
          Name = "rt-pub-${var.pjt}-${var.env}"
        }
      )
    }
    
    resource "aws_route_table_association" "t101-sjkim-pub-rt" {
      count = length(data.aws_availability_zones.seoul.names)
    
      subnet_id      = aws_subnet.t101-sjkim[count.index].id
      route_table_id = aws_route_table.t101-sjkim-pub-rt.id
    }
    
    resource "aws_route" "mydefaultroute" {
      route_table_id         = aws_route_table.t101-sjkim-pub-rt.id
      destination_cidr_block = "0.0.0.0/0"
      gateway_id             = aws_internet_gateway.t101-sjkim.id
    }
    
    output "aws_vpc_id" {
      value = aws_vpc.t101-sjkim.id
    }

코드 실행

  • terraform init && terraform validate && terraform fmt

    Initializing the backend...
    
    Initializing provider plugins...
    - Reusing previous version of hashicorp/aws from the dependency lock file
    - Using previously-installed hashicorp/aws v5.55.0
    
    Terraform has been successfully initialized!
    
    You may now begin working with Terraform. Try running "terraform plan" to see
    any changes that are required for your infrastructure. All Terraform commands
    should now work.
    
    If you ever set or change modules or backend configuration for Terraform,
    rerun this command to reinitialize your working directory. If you forget, other
    commands will detect it and remind you to do so if necessary.
    Success! The configuration is valid.
  • terraform plan -var pjt=sjkim -var env=prd -out tfplan

    • pjt와 env 환경변수 값을 plan 단계에서 옵션으로 전달하여 동적으로 변경 함 (과제3)
    data.aws_availability_zones.seoul: Reading...
    data.aws_availability_zones.seoul: Read complete after 1s [id=ap-northeast-2]
    
    Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
      + create
    
    Terraform will perform the following actions:
    
      # aws_subnet.t101-sjkim[0] will be created
      + resource "aws_subnet" "t101-sjkim" {
          + arn                                            = (known after apply)
          + assign_ipv6_address_on_creation                = false
          + availability_zone                              = "ap-northeast-2a"
          + availability_zone_id                           = (known after apply)
          + cidr_block                                     = "10.0.0.0/24"
          + enable_dns64                                   = false
          + enable_resource_name_dns_a_record_on_launch    = false
          + enable_resource_name_dns_aaaa_record_on_launch = false
          + id                                             = (known after apply)
          + ipv6_cidr_block_association_id                 = (known after apply)
          + ipv6_native                                    = false
          + map_public_ip_on_launch                        = false
          + owner_id                                       = (known after apply)
          + private_dns_hostname_type_on_launch            = (known after apply)
          + tags                                           = {
              + "Environment"      = "prd"
              + "Name"             = "sbn-sjkim-prd-pub-apne2-az1"
              + "Project"          = "sjkim"
              + "TerraformManaged" = "true"
            }
          + tags_all                                       = {
              + "Environment"      = "prd"
              + "Name"             = "sbn-sjkim-prd-pub-apne2-az1"
              + "Project"          = "sjkim"
              + "TerraformManaged" = "true"
            }
          + vpc_id                                         = (known after apply)
        }
    ...
      # aws_subnet.t101-sjkim[1] will be created
    ...
      # aws_subnet.t101-sjkim[2] will be created
    ...
      # aws_subnet.t101-sjkim[3] will be created
    ...
      # aws_vpc.t101-sjkim will be created
      + resource "aws_vpc" "t101-sjkim" {
          + arn                                  = (known after apply)
          + cidr_block                           = "10.0.0.0/20"
          + default_network_acl_id               = (known after apply)
          + default_route_table_id               = (known after apply)
          + default_security_group_id            = (known after apply)
          + dhcp_options_id                      = (known after apply)
          + enable_dns_hostnames                 = true
          + enable_dns_support                   = true
          + enable_network_address_usage_metrics = (known after apply)
          + id                                   = (known after apply)
          + instance_tenancy                     = "default"
          + ipv6_association_id                  = (known after apply)
          + ipv6_cidr_block                      = (known after apply)
          + ipv6_cidr_block_network_border_group = (known after apply)
          + main_route_table_id                  = (known after apply)
          + owner_id                             = (known after apply)
          + tags                                 = {
              + "Environment"      = "prd"
              + "Name"             = "vpc-sjkim-prd"
              + "Project"          = "sjkim"
              + "TerraformManaged" = "true"
            }
          + tags_all                             = {
              + "Environment"      = "prd"
              + "Name"             = "vpc-sjkim-prd"
              + "Project"          = "sjkim"
              + "TerraformManaged" = "true"
            }
        }
    
    Plan: 12 to add, 0 to change, 0 to destroy.
    
    ─────────────────────────────────────────────────────────────────────────────────────────────────────────
    Saved the plan to: tfplan
    
    To perform exactly these actions, run the following command to apply:
        terraform apply "tfplan"
  • terraform apply tfplan

    aws_vpc.t101-sjkim: Creating...
    aws_vpc.t101-sjkim: Still creating... [10s elapsed]
    aws_vpc.t101-sjkim: Creation complete after 12s [id=vpc-05dc83dac662b9d98]
    aws_internet_gateway.t101-sjkim: Creating...
    aws_route_table.t101-sjkim-pub-rt: Creating...
    aws_subnet.t101-sjkim[2]: Creating...
    aws_subnet.t101-sjkim[1]: Creating...
    aws_subnet.t101-sjkim[3]: Creating...
    aws_subnet.t101-sjkim[0]: Creating...
    aws_internet_gateway.t101-sjkim: Creation complete after 0s [id=igw-0699e7518c79169ad]
    aws_route_table.t101-sjkim-pub-rt: Creation complete after 0s [id=rtb-07687fbd508e7d25e]
    aws_subnet.t101-sjkim[2]: Creation complete after 0s [id=subnet-0c4c3cf1d6164fcf6]
    aws_subnet.t101-sjkim[1]: Creation complete after 0s [id=subnet-0e3dbaf75db98556c]
    aws_route.mydefaultroute: Creating...
    aws_subnet.t101-sjkim[0]: Creation complete after 0s [id=subnet-08a9fc8cb1c293ff2]
    aws_subnet.t101-sjkim[3]: Creation complete after 0s [id=subnet-0091542a2a5a261b1]
    aws_route_table_association.t101-sjkim-pub-rt[0]: Creating...
    aws_route_table_association.t101-sjkim-pub-rt[2]: Creating...
    aws_route_table_association.t101-sjkim-pub-rt[3]: Creating...
    aws_route_table_association.t101-sjkim-pub-rt[1]: Creating...
    aws_route_table_association.t101-sjkim-pub-rt[2]: Creation complete after 1s [id=rtbassoc-01a66d60ab6a14389]
    aws_route_table_association.t101-sjkim-pub-rt[3]: Creation complete after 1s [id=rtbassoc-0c3fd372ad5187ede]
    aws_route_table_association.t101-sjkim-pub-rt[1]: Creation complete after 1s [id=rtbassoc-0b1f18f4245e94baf]
    aws_route_table_association.t101-sjkim-pub-rt[0]: Creation complete after 1s [id=rtbassoc-0b0bed5384653157c]
    aws_route.mydefaultroute: Creation complete after 1s [id=r-rtb-07687fbd508e7d25e1080289494]

코드 실행결과

  • terraform state list

    data.aws_availability_zones.seoul
    aws_internet_gateway.t101-sjkim
    aws_route.mydefaultroute
    aws_route_table.t101-sjkim-pub-rt
    aws_route_table_association.t101-sjkim-pub-rt[0]
    aws_route_table_association.t101-sjkim-pub-rt[1]
    aws_route_table_association.t101-sjkim-pub-rt[2]
    aws_route_table_association.t101-sjkim-pub-rt[3]
    aws_subnet.t101-sjkim[0]
    aws_subnet.t101-sjkim[1]
    aws_subnet.t101-sjkim[2]
    aws_subnet.t101-sjkim[3]
    aws_vpc.t101-sjkim  ```
    
  • terraform state show aws_vpc.t101-sjkim

    resource "aws_vpc" "t101-sjkim" {
        arn                                  = "arn:aws:ec2:ap-northeast-2:538558617837:vpc/vpc-05dc83dac662b9d98"
        assign_generated_ipv6_cidr_block     = false
        cidr_block                           = "10.0.0.0/20"
        default_network_acl_id               = "acl-0678400853a0ac97c"
        default_route_table_id               = "rtb-09ebc57d65c0e2af7"
        default_security_group_id            = "sg-041c6bf7cb37cfc5d"
        dhcp_options_id                      = "dopt-6bf6b600"
        enable_dns_hostnames                 = true
        enable_dns_support                   = true
        enable_network_address_usage_metrics = false
        id                                   = "vpc-05dc83dac662b9d98"
        instance_tenancy                     = "default"
        ipv6_association_id                  = null
        ipv6_cidr_block                      = null
        ipv6_cidr_block_network_border_group = null
        ipv6_ipam_pool_id                    = null
        ipv6_netmask_length                  = 0
        main_route_table_id                  = "rtb-09ebc57d65c0e2af7"
        owner_id                             = "5**********7"
        tags                                 = {
            "Environment"      = "prd"
            "Name"             = "vpc-sjkim-prd"
            "Project"          = "sjkim"
            "TerraformManaged" = "true"
        }
        tags_all                             = {
            "Environment"      = "prd"
            "Name"             = "vpc-sjkim-prd"
            "Project"          = "sjkim"
            "TerraformManaged" = "true"
        }
    }
  • VPC 콘솔 - Details

  • VPC 콘솔 - Tags

  • Subnt 콘솔

  • Internet Gateway

  • Routing Table - Routes

  • Routing Table - Subnet Association

자원 삭제

  • terraform destroy -auto-approve

    data.aws_availability_zones.seoul: Reading...
    aws_vpc.t101-sjkim: Refreshing state... [id=vpc-05dc83dac662b9d98]
    data.aws_availability_zones.seoul: Read complete after 0s [id=ap-northeast-2]
    aws_route_table.t101-sjkim-pub-rt: Refreshing state... [id=rtb-07687fbd508e7d25e]
    aws_internet_gateway.t101-sjkim: Refreshing state... [id=igw-0699e7518c79169ad]
    aws_subnet.t101-sjkim[0]: Refreshing state... [id=subnet-08a9fc8cb1c293ff2]
    ...
      
    Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
      - destroy
    
    Terraform will perform the following actions:
    
      # aws_internet_gateway.t101-sjkim will be destroyed
    - resource "aws_internet_gateway" "t101-sjkim" {
        - arn      = "arn:aws:ec2:ap-northeast-2:538558617837:internet-gateway/igw-0699e7518c79169ad" -> null
        - id       = "igw-0699e7518c79169ad" -> null
    ...
          - ipv6_native                                    = false -> null
          - map_customer_owned_ip_on_launch                = false -> null
          - map_public_ip_on_launch                        = false -> null
          - owner_id                                       = "5**********7" -> null
          - private_dns_hostname_type_on_launch            = "ip-name" -> null
          - tags                                           = {
              - "Environment"      = "prd"
              - "Name"             = "sbn-sjkim-prd-pub-apne2-az1"
              - "Project"          = "sjkim"
              - "TerraformManaged" = "true"
            } -> null
          - tags_all                                       = {
              - "Environment"      = "prd"
              - "Name"             = "sbn-sjkim-prd-pub-apne2-az1"
              - "Project"          = "sjkim"
              - "TerraformManaged" = "true"
            } -> null
          - vpc_id                                         = "vpc-05dc83dac662b9d98" -> null
            # (4 unchanged attributes hidden)
        }
    
      # aws_subnet.t101-sjkim[1] will be destroyed
    ...
      # aws_subnet.t101-sjkim[2] will be destroyed
    ...
      # aws_subnet.t101-sjkim[3] will be destroyed
    ...
    
    Plan: 0 to add, 0 to change, 12 to destroy.
    aws_subnet.t101-sjkim[1]: Destroying... [id=subnet-0e3dbaf75db98556c]
    ...
    aws_vpc.t101-sjkim: Destruction complete after 0s
    
    Destroy complete! Resources: 12 destroyed.

트러블슈팅

Error: Retrieving AWS account details

  • 현상
$ terraform plan -var pjt=sjkim -var env=prd -out tfplan

Planning failed. Terraform encountered an error while generating this plan.

╷
│ Error: Invalid provider configuration
│ 
│ Provider "registry.terraform.io/hashicorp/aws" requires explicit configuration. Add a provider block to the root module and configure the provider's
│ required arguments as described in the provider documentation.
│ 

│ Error: Retrieving AWS account details: validating provider credentials: retrieving caller identity from STS: operation error STS: GetCallerIdentity, https response error StatusCode: 403, RequestID: e5915abc-c2e3-446e-b3d2-30bdfdd66bbc, api error InvalidClientTokenId: The security token included in the request is invalid.
│ 
│   with provider["registry.terraform.io/hashicorp/aws"],
│   on main.tf line 11, in provider "aws":11: provider "aws" {
  • 원인
~/.aws/credentials 파일에 특정 프로파일명(t101)에 access_id, secret_access_key 값은 정상적으로 지정되었으나
유효한 [default] access_id, secret_access_key 값이 정의되지 않았을 때
STS (Security Token Service) 인증 실패 되어 발생
  • 조치
# AWS_PROFILE 환경변수에 사용하고자 하는 프로파일명을 명시

$ export AWS_PROFILE=t101
$ echo $AWS_PROFILE
t101
  • 결과
$ terraform init && terraform plan && terraform apply -auto-approve
...
Initializing the backend...
...
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Success! The configuration is valid.

data.local_file.abc: Reading...
data.local_file.abc: Read complete after 0s [id=75a47c30031f5fa02f6c4b5170e9539324f95c87]
data.aws_availability_zones.seoul: Reading...
data.aws_availability_zones.seoul: Read complete after 0s [id=ap-northeast-2]

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.

이번 학습 주요 메모사항

  • 변수 유효성 검사 : 변수 블록 내에 validation 블록에서 조건인 condition에 지정되는 규칙이 true 또는 false를 반환해야 하며, error_message는 condition 값의 결과가 false 인 경우 출력되는 메시지를 정의한다.

    variable "image_id" {
      type        = string
      description = "The id of the machine image (AMI) to use for the server."
    
      validation {
        condition     = length(var.image_id) > 4
        error_message = "The image_id value must exceed 4."
      }
    
      validation {
        # regex(...) fails if it cannot find a match
        condition     = can(regex("^ami-", var.image_id))
        error_message = "The image_id value must starting with \"ami-\"."
      }
    }
  • 변수 입력방식과 우선순위

    variable의 목적은 코드 내용을 수정하지 않고 테라폼의 모듈적 특성을 통해 입력되는 변수로 재사용성을 높이는 데 있다.
    특히 입력 변수라는 명칭에 맞게 사용자는 프로비저닝 실행 시에 원하는 값으로 변수에 정의할 수 있다.
    선언되는 방식에 따라 변수의 우선순위가 있으므로, 이를 적절히 사용해 로컬 환경과 빌드 서버 환경에서의 정의를 다르게 하거나, 프로비저닝 파이프라인을 구성하는 경우 외부 값을 변수에 지정할 수 있다.

  • 특정 리소스만 삭제 명령어

    terraform destroy -auto-approve -target=aws_instance.myec2

퀴즈

terraform.tfstate 파일을 보안적으로 안전하게 관리하는 방안은?

Terraform 상태 파일(`terraform.tfstate`)을 보안적으로 안전하게 관리하는 것은 매우 중요합니다. 
이 파일에는 인프라에 대한 중요한 정보가 포함되어 있기 때문입니다. 
다음은 `terraform.tfstate` 파일을 안전하게 관리하기 위한 몇 가지 방법입니다:

1. 원격 백엔드 사용
Terraform 상태 파일을 로컬 시스템에 저장하지 않고, 원격 백엔드를 사용하여 저장하는 것이 좋습니다. 
이를 통해 상태 파일을 중앙에서 관리하고 접근을 제어할 수 있습니다.
- **AWS S3 + DynamoDB**: 상태 파일은 S3에 저장하고 잠금(locking) 기능을 DynamoDB로 설정합니다.
- **HashiCorp Consul**
- **Terraform Cloud/Enterprise**
- **Azure Blob Storage**
- **Google Cloud Storage**

2. 상태 파일 암호화
상태 파일을 저장할 때 암호화하는 것이 좋습니다.
- **S3 버킷 암호화**: S3 버킷에 저장된 상태 파일을 암호화합니다.
- **Terraform Cloud/Enterprise**: 기본적으로 상태 파일을 암호화합니다.
- **Azure Blob Storage**: 암호화된 Blob을 사용합니다.

3. 접근 제어
상태 파일에 접근할 수 있는 권한을 최소한으로 제한합니다.
- **IAM 정책**: S3 버킷에 대한 접근을 제한하는 IAM 정책을 설정합니다.
- **RBAC (Role-Based Access Control)**: Azure 또는 Google Cloud Storage에서 
  역할 기반 접근 제어를 사용합니다.
- **Terraform Cloud/Enterprise**: 팀 및 사용자의 권한을 세밀하게 설정합니다.

4. 버전 관리
상태 파일의 변경 사항을 추적하고 필요한 경우 롤백할 수 있도록 버전 관리를 설정합니다.
- **S3 버전 관리**: S3 버킷에서 버전 관리를 활성화하여 상태 파일의 변경 사항을 추적합니다.
- **Terraform Cloud/Enterprise**: 상태 파일의 변경 이력을 자동으로 관리합니다.

5. 민감 정보 제거
상태 파일에 민감한 정보(예: 비밀번호, API 키 등)가 포함되지 않도록 주의합니다. 
필요한 경우 `terraform` 명령어에서 민감 정보를 마스킹할 수 있는 설정을 사용합니다.
- `sensitive = true` 속성을 사용하여 민감 정보를 숨깁니다.

6. 백업 및 복구 계획
상태 파일의 백업을 정기적으로 수행하고, 문제가 발생했을 때 복구할 수 있는 절차를 마련합니다.
- **정기 백업**: 상태 파일을 정기적으로 백업하고 안전한 위치에 저장합니다.
- **복구 테스트**: 복구 절차를 정기적으로 테스트하여 문제가 발생했을 때 신속하게 대응할 수 있도록 합니다.

7. 자동화된 보안 검사
상태 파일을 정기적으로 검사하여 보안 취약점을 발견하고 해결합니다.
- **Terraform Sentinel**: 정책을 설정하여 Terraform 실행 시 보안 규칙을 검토합니다.
- **정적 분석 도구**: 상태 파일을 분석하여 민감 정보 유출 여부를 확인합니다.

이러한 방안들을 종합적으로 적용하여 Terraform 상태 파일을 안전하게 관리할 수 있습니다. 
각 조직의 필요와 환경에 맞게 적절한 방법을 선택하고 구현하는 것이 중요합니다.

local은 어떻게 사용하면 좋을까요? 또 많아 질 경우 관리에 어려움이 발생할 것 같은데, 이럴때는 어떻게 해야 될까요?

Terraform에서 local 값은 간단한 값을 저장하고 재사용하기 위해 사용됩니다. locals 블록을 사용하여 정의하며, 변수의 값을 쉽게 설정하고, 여러 리소스에서 재사용할 수 있어 코드의 가독성과 유지보수성을 높여줍니다. 그러나 로컬 변수가 많아질 경우 관리가 어려워질 수 있습니다. 이러한 상황을 관리하기 위한 몇 가지 방법을 소개합니다.

local 값 사용 예제

hcl
코드 복사
locals {
  region        = "us-west-2"
  instance_type = "t2.micro"
  ami_id        = "ami-0c55b159cbfafe1f0"
}

provider "aws" {
  region = local.region
}

resource "aws_instance" "example" {
  ami           = local.ami_id
  instance_type = local.instance_type
}

위 예제에서 locals 블록을 사용하여 지역, 인스턴스 유형, AMI ID를 정의하고 이를 리소스 블록에서 사용하고 있습니다.

locals 블록의 장점

  1. 중복 코드 감소: 여러 리소스에서 반복적으로 사용되는 값을 한 곳에 정의하여 중복을 줄일 수 있습니다.
  2. 가독성 향상: 중요한 값들을 한 곳에서 관리하므로 코드의 가독성이 높아집니다.
  3. 유지보수 용이: 값을 변경할 때 여러 곳을 수정할 필요 없이 locals 블록만 수정하면 됩니다.

locals 관리 방안

로컬 변수가 많아질 경우 다음과 같은 방법으로 관리할 수 있습니다:

1. 구조화된 로컬 변수 사용

로컬 변수를 구조화된 데이터로 정의하여 관리할 수 있습니다. 예를 들어, 여러 환경에 대한 설정을 관리할 때 유용합니다.

locals {
  configs = {
    dev = {
      region        = "us-west-2"
      instance_type = "t2.micro"
      ami_id        = "ami-0c55b159cbfafe1f0"
    }
    prod = {
      region        = "us-east-1"
      instance_type = "m5.large"
      ami_id        = "ami-12345678"
    }
  }
  environment = "dev"
}

provider "aws" {
  region = local.configs[local.environment].region
}

resource "aws_instance" "example" {
  ami           = local.configs[local.environment].ami_id
  instance_type = local.configs[local.environment].instance_type
}

위 예제에서 configs라는 구조체를 사용하여 각 환경의 설정을 정의하고, 이를 environment 변수로 선택적으로 참조하고 있습니다.

2. 파일 분리

로컬 변수가 많아지는 경우, 파일을 분리하여 관리할 수 있습니다. 예를 들어, locals.tf 파일을 따로 만들어 로컬 변수만 관리합니다.

# locals.tf
locals {
  region        = "us-west-2"
  instance_type = "t2.micro"
  ami_id        = "ami-0c55b159cbfafe1f0"
}

# main.tf
provider "aws" {
  region = local.region
}

resource "aws_instance" "example" {
  ami           = local.ami_id
  instance_type = local.instance_type
}

3. 모듈 사용

로컬 변수가 특정 기능이나 역할에 따라 그룹화된다면, 모듈을 사용하여 코드를 재사용하고 관리할 수 있습니다. 각 모듈은 자체적으로 locals 블록을 가질 수 있으며, 이를 통해 코드의 응집성과 재사용성을 높일 수 있습니다.

# modules/web_server/locals.tf
locals {
  instance_type = "t2.micro"
  ami_id        = "ami-0c55b159cbfafe1f0"
}

# modules/web_server/main.tf
resource "aws_instance" "web_server" {
  ami           = local.ami_id
  instance_type = local.instance_type
}

# main.tf
module "web_server" {
  source = "./modules/web_server"
}

4. Terraform 변수 파일 사용

로컬 변수 대신 Terraform 변수 파일을 사용하여 관리할 수 있습니다. 변수 파일을 사용하면 환경별 설정을 쉽게 변경할 수 있습니다.

# variables.tf
variable "region" {
  default = "us-west-2"
}

variable "instance_type" {
  default = "t2.micro"
}

variable "ami_id" {
  default = "ami-0c55b159cbfafe1f0"
}

# main.tf
provider "aws" {
  region = var.region
}

resource "aws_instance" "example" {
  ami           = var.ami_id
  instance_type = var.instance_type
}

# dev.tfvars
region = "us-west-2"
instance_type = "t2.micro"
ami_id = "ami-0c55b159cbfafe1f0"

# prod.tfvars
region = "us-east-1"
instance_type = "m5.large"
ami_id = "ami-12345678"

# 명령어를 통해 적용
terraform apply -var-file="dev.tfvars"

결론

Terraform에서 local 값을 사용하면 코드의 중복을 줄이고 가독성을 높일 수 있습니다. 하지만 로컬 변수가 많아질 경우, 구조화된 데이터 사용, 파일 분리, 모듈 사용, 변수 파일 사용 등의 방법으로 관리할 수 있습니다. 이러한 방법들을 통해 Terraform 코드를 효율적으로 관리하고 유지보수할 수 있습니다.

profile
I'm SJ

0개의 댓글