TFC

  • Terraform Cloud(TFC)
  • workflow 구성 환경 제공
  • VCS 연동 기능, 변수 구성
  • RBAC, 원격 실행환경

실습

저장소 포크 : https://github.com/terraform101/terraform-aws-tfc-workflow

 

GitHub - terraform101/terraform-aws-tfc-workflow: [Chapter 8] CICD - TFC Workflow

[Chapter 8] CICD - TFC Workflow. Contribute to terraform101/terraform-aws-tfc-workflow development by creating an account on GitHub.

github.com

저장소 포크 후 cloud 백엔드의 org값 변경

fork한 저장소 clone 후 org 바꿈

terraform init

token생성 후 terraform login 후 terraform init

terraform plan

l

원격지의 tfc에 설정될 value가 없음

add variable

  • Prefix : Type(Terraform variable), Key(prefix), value(사용자 닉네임)

  • Access Key ID : Type(Environment variable), Key(AWS_ACCESS_KEY_ID), value(자신의 값)
  • Secret Access Key : Type(Environment variable), Key(AWS_SECRET_ACCESS_KEY), value(자신의 값), Sensitive 선택

전부 입력

terraform plan : 에러 없네

ahn@GeunChangui-noteubug terraform-aws-tfc-workflow % terraform plan
Running plan in Terraform Cloud. Output will stream here. Pressing Ctrl-C
will stop streaming the logs, but will not stop the plan running remotely.

Preparing the remote plan...

To view this run in a browser, visit:
https://app.terraform.io/app/ahn/terraform-aws-tfc-workflow/runs/run-GD9j8pPk2hreWsiT

Waiting for the plan to start...

Terraform v1.5.6
on linux_amd64
Initializing plugins and modules...
data.aws_ami.ubuntu: Refreshing...
data.aws_ami.ubuntu: Refresh complete after 1s [id=ami-0a7f78325e47a7c27]

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_eip.hashicat will be created
  + resource "aws_eip" "hashicat" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = (known after apply)
      + id                   = (known after apply)
      + instance             = (known after apply)
      + network_border_group = (known after apply)
      + network_interface    = (known after apply)
      + private_dns          = (known after apply)
      + private_ip           = (known after apply)
      + public_dns           = (known after apply)
      + public_ip            = (known after apply)
      + public_ipv4_pool     = (known after apply)
      + tags_all             = {
          + "Owner"   = "jerry & tom"
          + "Project" = "Coffee-Mug-Cake"
        }
      + vpc                  = true
    }

  # aws_eip_association.hashicat will be created
  + resource "aws_eip_association" "hashicat" {
      + allocation_id        = (known after apply)
      + id                   = (known after apply)
      + instance_id          = (known after apply)
      + network_interface_id = (known after apply)
      + private_ip_address   = (known after apply)
      + public_ip            = (known after apply)
    }

  # aws_instance.hashicat will be created
  + resource "aws_instance" "hashicat" {
      + ami                                  = "ami-0a7f78325e47a7c27"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = true
      + availability_zone                    = (known after apply)
      + cpu_core_count                       = (known after apply)
      + cpu_threads_per_core                 = (known after apply)
      + disable_api_stop                     = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + host_resource_group_arn              = (known after apply)
      + iam_instance_profile                 = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t3.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = "감동맨-ssh-key.pem"
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (known after apply)
      + primary_network_interface_id         = (known after apply)
      + private_dns                          = (known after apply)
      + private_ip                           = (known after apply)
      + public_dns                           = (known after apply)
      + public_ip                            = (known after apply)
      + secondary_private_ips                = (known after apply)
      + security_groups                      = (known after apply)
      + source_dest_check                    = true
      + subnet_id                            = (known after apply)
      + tags                                 = {
          + "Name" = "감동맨-hashicat-instance"
        }
      + tags_all                             = {
          + "Name"    = "감동맨-hashicat-instance"
          + "Owner"   = "jerry & tom"
          + "Project" = "Coffee-Mug-Cake"
        }
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = false
      + vpc_security_group_ids               = (known after apply)
    }

  # aws_internet_gateway.hashicat will be created
  + resource "aws_internet_gateway" "hashicat" {
      + arn      = (known after apply)
      + id       = (known after apply)
      + owner_id = (known after apply)
      + tags     = {
          + "Name" = "감동맨-internet-gateway"
        }
      + tags_all = {
          + "Name"    = "감동맨-internet-gateway"
          + "Owner"   = "jerry & tom"
          + "Project" = "Coffee-Mug-Cake"
        }
      + vpc_id   = (known after apply)
    }

  # aws_key_pair.hashicat will be created
  + resource "aws_key_pair" "hashicat" {
      + arn             = (known after apply)
      + fingerprint     = (known after apply)
      + id              = (known after apply)
      + key_name        = "감동맨-ssh-key.pem"
      + key_name_prefix = (known after apply)
      + key_pair_id     = (known after apply)
      + key_type        = (known after apply)
      + public_key      = (known after apply)
      + tags_all        = {
          + "Owner"   = "jerry & tom"
          + "Project" = "Coffee-Mug-Cake"
        }
    }

  # aws_route_table.hashicat will be created
  + resource "aws_route_table" "hashicat" {
      + arn              = (known after apply)
      + id               = (known after apply)
      + owner_id         = (known after apply)
      + propagating_vgws = (known after apply)
      + route            = [
          + {
              + carrier_gateway_id         = ""
              + cidr_block                 = "0.0.0.0/0"
              + core_network_arn           = ""
              + destination_prefix_list_id = ""
              + egress_only_gateway_id     = ""
              + gateway_id                 = (known after apply)
              + instance_id                = ""
              + ipv6_cidr_block            = ""
              + local_gateway_id           = ""
              + nat_gateway_id             = ""
              + network_interface_id       = ""
              + transit_gateway_id         = ""
              + vpc_endpoint_id            = ""
              + vpc_peering_connection_id  = ""
            },
        ]
      + tags_all         = {
          + "Owner"   = "jerry & tom"
          + "Project" = "Coffee-Mug-Cake"
        }
      + vpc_id           = (known after apply)
    }

  # aws_route_table_association.hashicat will be created
  + resource "aws_route_table_association" "hashicat" {
      + id             = (known after apply)
      + route_table_id = (known after apply)
      + subnet_id      = (known after apply)
    }

  # aws_security_group.hashicat will be created
  + resource "aws_security_group" "hashicat" {
      + arn                    = (known after apply)
      + description            = "Managed by Terraform"
      + egress                 = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = ""
              + from_port        = 0
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "-1"
              + security_groups  = []
              + self             = false
              + to_port          = 0
            },
        ]
      + id                     = (known after apply)
      + ingress                = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = ""
              + from_port        = 22
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "tcp"
              + security_groups  = []
              + self             = false
              + to_port          = 22
            },
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = ""
              + from_port        = 443
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "tcp"
              + security_groups  = []
              + self             = false
              + to_port          = 443
            },
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = ""
              + from_port        = 80
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "tcp"
              + security_groups  = []
              + self             = false
              + to_port          = 80
            },
        ]
      + name                   = "감동맨-security-group"
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags                   = {
          + "Name" = "감동맨-security-group"
        }
      + tags_all               = {
          + "Name"    = "감동맨-security-group"
          + "Owner"   = "jerry & tom"
          + "Project" = "Coffee-Mug-Cake"
        }
      + vpc_id                 = (known after apply)
    }

  # aws_subnet.hashicat will be created
  + resource "aws_subnet" "hashicat" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = (known after apply)
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "10.0.10.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                                           = {
          + "name" = "감동맨-subnet"
        }
      + tags_all                                       = {
          + "Owner"   = "jerry & tom"
          + "Project" = "Coffee-Mug-Cake"
          + "name"    = "감동맨-subnet"
        }
      + vpc_id                                         = (known after apply)
    }

  # aws_vpc.hashicat will be created
  + resource "aws_vpc" "hashicat" {
      + arn                                  = (known after apply)
      + cidr_block                           = "10.0.0.0/16"
      + 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_classiclink                   = (known after apply)
      + enable_classiclink_dns_support       = (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" = "Production"
          + "name"        = "감동맨-vpc-ap-northeast-2"
        }
      + tags_all                             = {
          + "Owner"       = "jerry & tom"
          + "Project"     = "Coffee-Mug-Cake"
          + "environment" = "Production"
          + "name"        = "감동맨-vpc-ap-northeast-2"
        }
    }

  # null_resource.configure-cat-app will be created
  + resource "null_resource" "configure-cat-app" {
      + id = (known after apply)
    }

  # tls_private_key.hashicat will be created
  + resource "tls_private_key" "hashicat" {
      + algorithm                     = "RSA"
      + ecdsa_curve                   = "P224"
      + id                            = (known after apply)
      + private_key_openssh           = (sensitive value)
      + private_key_pem               = (sensitive value)
      + private_key_pem_pkcs8         = (sensitive value)
      + public_key_fingerprint_md5    = (known after apply)
      + public_key_fingerprint_sha256 = (known after apply)
      + public_key_openssh            = (known after apply)
      + public_key_pem                = (known after apply)
      + rsa_bits                      = 2048
    }

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

Changes to Outputs:
  + catapp_ip  = (known after apply)
  + catapp_url = (known after apply)
ahn@GeunChangui-noteubug terraform-aws-tfc-workflow %

 

실제 terraform apply (하고 yes 입력은 하지 않고) overview 확인

  • 실제로 저 run 부분이 ci/cd도구처럼 실행시 planning시 돌다가 현재 planned상태
  •  

See detail을 보면 전체 스트림으로 출력된 계획을 확인 가능

 

내가 직접 확인하고 커멘트를 달고 컨펌하면

CLI쪽에서 승인 처리가 됨을 확인할 수 있다

CLI Yes를 하지 않아도 UI에서 컨펌했음이 나타난다.

리소스 삭제  

terraform destory -auto-approve

 

리소스가 안전하게 삭제 됨을 확인

블로그 이미지

감동맨

rkaehdaos의 블로그

,
기본기 부족
프로그래밍 공부가 더 필요하다냥 ㅠ

 

8. 테라폼

8.1 규모에 따른 워크플로

  • 테라폼 워크플로 : Write → Plan → Apply
  • 워크스페이스 별로 접근 권한을 관리하고 중앙에서 관리되는 실행 환경을 설계하여 규모에 맞는 워크플로 설계가 필요
  • 참고: The Core Terraform Workflow
딥엘로 번역해서 봤는데.. 
밑의 내용이 나오긴 하는데.. 나는
테라폼 클라우드부터 계속 햇갈림 ㅠ
 

The Core Terraform Workflow - Guides | Terraform | HashiCorp Developer

An overview of how individuals, teams, and organizations can use Terraform.

developer.hashicorp.com

8.1.1 개인의 워크 플로우

개인의 워크플로우

  • Write : 프로비저닝  목적에 따라 코드 작성
    • 개인 작업이어도 반복적 사용성 고려.
    • 인수에 할당되는 값을 입력 변수화
    •  반복적 구조가 발생하면?
      • 리소스 단위별로 반복문을 사용할지
      • 다수의 리소스를 모듈화할지 결정
  • Plan : apply 전 plan 통한 리뷰
    • 테라폼의 Plan뿐 아니라, terraform fmt를 통해 코드 형태를 포멧팅하고 변경되는 리소스를 리뷰
    • 또한 테라폼과 함께 동작하는 tfsec이나 terrascan 같은 보안 취약성 점검 툴 등을 활용하는 것도 좋은 방안
  • Apply : 인프라 프로비저닝
    • plan에선  정상이지만 실제 프로비저닝하는 단계에서 인수 값, 생성 순서, 종속성에 따라 오류가 발생 가능
    • 성공적인 완료를 위해 Write > Plan > Apply 단계를 반복하고 성공하는 경우 코드 관리를 위해 VCS에 코드를 병합.

8.1.2 다중 작업자 워크플로

팀의 워크플로

  • Write
    • 여러 작업자의 테라폼 코드가 충돌하지 않도록 형상관리 도구에 익숙해져야 한다.
    • 작업자는 작업 전에 미리 원격 저장소의 코드를 받고(pull, fetch)
      git에서는 브랜치를 활용해 개별적으로 작업한다.
    • 개인의 워크플로에서 고려한 변수화와 더불어 패스워드인증서 같은 민감 데이터가 포함되지 않도록 코드를 설계한다.
      또한 개인 작업 환경에서만 사용되는 변수는 공유하지 않는다.
    • 깃을 사용한다면 작업자 개인의 변수terraform.tfvars 에 선언하고 .gitignore에 추가해 개별적으로 테스트할 수 있는 환경을 구성할 수 있다(이거 레알 짱인듯). 이 단계에서 개별 작업자는 더 작은 단위의 개인별 워크플로(Write > Plan > Apply)를 반복해야 한다.
    • 개별 작업 환경과 별개로 병합되는 코드가 실제 운영 중인 인프라에 즉시 반영되면 실행 후 발생할 오류 예측이 어려워 부담이 될 수 있다. 이를 보완하기 위해 프로비저닝 대상의 환경을 검증과 운영, 또는 그 이상의 환경으로 구성 가능하도록 구조화한다.이때 사용하는 방식은 디렉터리 기반 격리깃 기반의 브랜치 격리다.
성공적인 깃 브렌치 모델 - https://nvie.com/posts/a-successful-git-branching-model
  • Plan
    • 둘 이상의 작업자는 프로비저닝 이전에 팀원 간 리뷰를 거쳐 변경된 내역을 확인하고 공통 저장소에 병합해야 한다.
    • 리뷰 단계에서는 추가, 삭제, 수정된 내역을 관련 작업자가 검증, 질의, 배움의 단계를 거쳐 복기함으로써 코드 상태를 개선 유지하고 작업자 간에 의도를 공유한다.
    • 코드 자체 외에도 테라폼의 Plan 결과를 풀 리퀘스트 단계에 같이 제공하면 영향을 받는 리소스와 서비스 중단에 대한 예측이 더 쉬워진다.
    • CI 툴과 연계하거나 Terraform Cloud/Enterprise의 VCS 통합 기능으로 자동화할 수 있다.
  • Apply
    • 코드가 최종 병합되면 인프라 변경이 수행됨을 알리고 변경되는 대상 환경의 중요도에 따라 승인이 필요할 수 있다.
    • 또한 변경하는 코드가 특정 기능, 버그 픽스, 최종 릴리즈를 위한 병합인가에 따라 이 단계에 추가로 코드 병합이 발생할 수 있다.
    • 관리하는 단위를 나누는 기준은 조직 R&R, 서비스, 인프라 종류 등으로 구분된다.

8.1.3 다수 팀의 워크플로

  • R&R이 분리된 다수 팀 또는 조직의 경우 테라폼의 프로비저닝 대상은 하나이지만 관리하는 리소스가 분리된다.
  • 단일 팀의 워크플로가 유지되고 그 결과에 대해 공유해야 하는 핵심 워크플로가 필요!

다수 팀의 워크플로

  • Write
    • 대상 리소스가 하나의 모듈에서 관리되지 않고 R&R에 의해 워크스페이스가 분리된다.
    • 서로 다른 워크스페이스에서 구성된 리소스 데이터를 권한이 다른 팀에게 공유하기 위해, 저장된 State 접근 권한을 제공하고 output을 통해 공유 대상 데이터를 노출한다.
    • 테라폼 코드 작성 시 다른 워크스페이스에서의 변경 사항을 데이터 소스로 받아 오는 terraform_remote_state 또는 별도 KV-store를 활용하는 코드 구성이 요구된다.
    • 또한 관리 주체가 다른 곳에서 생긴 변경 사항의 영향을 최소화하도록 리모트 데이터 소스의 기본값을 정의하거나 코드적인 보상 로직을 구현하는 작업이 필요하다.
  • Plan
    • 코드 기반으로 진행되는 리뷰는 반영되는 다른 팀의 인프라를 VCS상의 코드 리뷰만으로도 공유받고 영향도를 검토할 수 있다.
    • 병합을 승인하는 단계에 영향을 받는 다른 팀의 작업자도 참여해야 한다.
  • Apply
    • 프로비저닝 실행과 결과에 대한 안내가 관련 팀에 알려져야 하므로 파이프라인 구조에서 자동화하는 것을 추천한다.
    • 실행 후의 영향도가 여러 팀이 관리하는 리소스에 전파될 수 있으므로 코드 롤백 훈련이 필요하다.
    • 생성된 결과에 다른 워크스페이스에서 참조되는 output 값의 업데이트된 내용을 다른 팀이 확인하는 권한 관리가 필요하다

8.2 격리 구조

  • 테라폼 수준의 격리 목표 : State를 분리
    • 테라폼은 파일이나 하위 모듈로 구분하더라도
      동작 기준은 실행하는 루트 모듈에서 코드를 통합하고 하나의 State로 관리한다.
    • 애플리케이션 구조가 모놀리식(+아키텍처)에서 MSA로 변화하는 과정은
      테라폼의 IaC 특성과도 결부된다.

모놀리식과 MSA 비교

  • 테라폼 또한 사용하는 리소스가 적고 구조가 단순하면 모놀리식 방식으로 구성하는 것이 인프라 프로비저닝 구축 속도는 빠를 수 있다.
  • 하지만 유지 보수, 인수인계, 운영의 관점에서는 프로비저닝 단위별로 분류하는,
    마치 MSA와도 같은 분산된 설계가 매몰 비용과 기술 부채를 줄이는 데 효과적이다.
  • 규모가 큰 워크플로를 만들기 위해서는 간단하고 조합 가능한 부분들이 모여 집합을 이루어야 한다.
  • 이러한 집합에서 발생하는 정보는 다른 집합과 교환할 수 있지만, 각 집합은 독립적으로 실행되며 다른 집합에 영향을 받지 않는 격리된 구조가 필요하다.
  • 초기 테라폼 적용 단계에서 단일 또는 소수의 작업자는 단일 대상에 대해 IaC를 적용하고 하나의 루트 모듈에 많은 기능을 포함시킬 가능성이 높다.

8.2.1 루트 모듈 격리(파일/디렉터리)

모놀리식 구조의 파일 디렉토리 격리 구조

  • 단일 작업자가 테라폼으로 프로비저닝을 하는 많은 경우에 관리 편의성 및 배포 단순화를 위해 하나의 루트 디렉터리에 파일로 리소스들을 구분하거나, 디렉터리를 생성하고 하위에 구성 파일 묶음을 위치시켜 루트 모듈에서 하위 디렉터리를 모듈로 읽는 구조를 사용한다.
  • 작업자가 관리하는 영역 또는 프로비저닝되는 리소스 묶음의 독립적인 실행을 위해 단일 루트 모듈 내의 리소스를 다수의 루트 모듈로 분리하고 각 모듈의 State를 참조하도록 격리한다.
  • 관리적인 측면으로는 작업자들의 관리 영역을 분리시키고 깃 기준의 리모트 저장소도 접근 권한을 관리할 수 있다.
  • 협업과 관련해 작업자별로 특정 루트 모듈을 선정해 구성 작업을 진행해 코드 충돌을 최소화하는 환경을 구성하고 인수인계 과정에서 리뷰하는 영역을 최소화할 수 있다.

8.2.2 환경격리 -깃 브랜치

  • 서비스의 테스트, 검증, 운영 배포를 위해 테라폼으로 관리하는 리소스가 환경별로 격리되어야 한다면 디렉터리 구조로 분리하는 방안을 고려할 수 있다.
  • 디렉터리별로 각 환경을 나누는 것은 개인의 관리 편의성은 높지만, 환경의 아키텍처를 고정시키고 코드 수준의 승인 체계를 만들기 위해서는 최종 형상에 대한 환경별 브랜치를 구성하기를 권장한다.
  • 디렉터리 구조 만으로는 환경에 따라 사용자를 격리할 수 없다. 이때 깃의 브랜치 기능을 활용하면 환경 별로 구별된 작업과 협업이 가능하다.

깃 플로 예시

  • 브랜치 전략
    • main : 운영 코드가 관리되며 이곳에는 직접적으로 구성 변경을 수행하지 않음
    • QA : 검증 대상 인프라를 구성하는 코드로, 메인 브랜치와 같이 직접적인 구성 변경을 수행하지 않음
    • DEV : QA 전 단계로 메인 코드 구성과 기능 브랜치의 병합을 담당
    • feature : 새로운 리소스를 추가하고 구현하며 여러 개가 될 수 있음
  • 관리의 편의성을 고려해 Hot-fix와 Release 브랜치를 추가할 수도 있지만 인프라의 특성상 개발, 검증, 운영으로 나눈다.
  • 환경 간에 프로비저닝이 되는 리소스를 갖추고 있다면 운영을 위한 프로비저닝 환경을 안정적으로 유지할 수 있다는 장점이 있다.
  • 디렉터리 구조로 관리하는 환경별 디렉터리 구성 방식에서는 개발할 때 작성한 구성을 다시 복사해 검증 또는 운영에 반영하므로 환경별로 구성이 다른 상황이 발생할 여지가 높고, 모든 디렉터리에 접근 가능할 경우 검증과 운영을 위한 구성을 직접 수정하는 일이 발생할 가능성이 높다.
  • 따라서 작업자가 다수의 환경을 동시에 관리한다면 디렉터리로 구분하더라도 각 디렉터리마다 동일한 깃 저장소의 브랜치별 리모트 구성을 하는 것이 바람직하다.

디렉토리 격리에 깃 브랜치 연결

이 방식은 동일한 브랜치를 변경해가면 작업해 발생하는 실수를 줄일 수 있고, 각 브랜치가 연결되어 있으므로 단일 작업자가 다수의 환경을 관리하는 이점과 각 환경별로 리소스 구성이 동일하게 유지되는 장점이 있다.

블로그 이미지

감동맨

rkaehdaos의 블로그

,

5.State

5.1 State 목적과 의미

  • 상태 파일은 배포할 때마다 변경되는 프라이빗 API private API
  • 오직 테라폼 내부에서 사용하기 위한 것입니다.
  • 테라폼 상태 파일을 직접 편집하거나 직접 읽는 코드로 작성해서는 안된다

팀 단위에서 테라폼 운영 시 문제점

  • 상태 파일을 저장하는 공유 스토리지( Shared storage for state files)
    • 각 팀원이 동일한 테라폼 상태 파일 사용을 위해서, 공유 위치에 저장이 필요
  • 상태 파일 잠금 Locking state files
    • 잠금 기능 없이 두 팀원이 동시에 테라폼 실행 시 여러 테라폼 프로세스가 상태 파일을 동시에 업데이트하여 충돌 가능(경쟁 상태 race condition)
  • 상태 파일 격리 Isolating state files
    • 예를 들면 테스트 dev 와 검증 stage 과 상용 prodction 각 환경에 대한 격리가 필요

상태 파일 공유로 버전 관리 시스템 비추천

  1. 수동 오류 Manual error
    • 테라폼을 실행하기 전에 최신 변경 사항을 가져오거나 실행하고 나서 push 하는 것을 잊기 쉽습니다(?).
    • 팀의 누군가가 이전 버전의 상태 파일로 테라폼을 실행하고, 그 결과 실수로 이전 버전으로 롤백하거나 이전에 배포된 인프라를 복제하는 문제가 발생 할 수 있음.
  2. 잠금 Locking
    • 대부분의 버전 관리 시스템(VCS)은 여러 명의 팀 구성원이 동시에 하나의 상태 파일에 terraform apply 명령을 실행하지 못하게 하는 잠금 기능이 제공되지 않음.
  3. 시크릿 Secrets
    • 테라폼 상태 파일의 모든 데이터는 평문으로 저장됨. 민감 정보가 노출될 위험.

지원되는 원격 백엔드

  • AWS S3, Azure Blob Storage, Google Cloud Storage, Consul, Postgres database 등
  •  위 제약사항에 대한 해결방안!→ 링크 
  1. 수동 오류 해결 : plan/apply 실행 시 마다 해당 백엔드에서 파일을 자동을 로드, apply 후 상태 파일을 백엔드에 자동 저장
  2. 잠금(Lock) : apply 실행 시 테라폼은 자동으로 잠금을 활성화, `-lock-timout=<TIME>` 로 대기 시간 설정 지정 가능
  3. 시크릿 : 대부분 원격 백엔드는 기본적으로 데이터를 보내거나 상태 파일을 저장할 때 암호화(Encryption)하는 기능을 지원

책 내용

  • 소개 및 확인 : 테라폼은 Stateful 애플리케이션. 프로비저닝 결과 State를 저장하고 추적에 활용
    • 개인 1인 : 로컬 환경으로 terraform.tfstate 파일에 JSON 형태로 저장
    • 팀이나 조직 : 공동 관리를 위해 원격 저장소에 저장해 공유 - 링크
    • State에는 작업자가 정의한 코드와 실제 반영된 프로비저닝 결과를 저장하고, 이 정보를 토대로 이후의 리소스 생성, 수정, 삭제에 대한 동작 판단 작업을 수행
    State 역할
    • State에는 테라폼 구성과 실제를 동기화하고 각 리소스에 고유한 아이디(리소스 주소)로 맵핑
    • 리소스 종속성과 같은 메타데이터를 저장하고 추적
    • 테라폼 구성으로 프로비저닝 결과를 캐싱하는 역할을 수행

5.1 실습

resource "random_password" "mypw" {
  length           = 16
  special          = true
  override_special = "!#$%"
}

5.1 실행

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

# State List 및 생성된 Password 확인
terraform state list
terraform state show random_password.mypw

# State에 저장된 result를 확인해보자! -> 어떻게 관리해야할까요?
ls *.tfstate
cat terraform.tfstate | jq
cat terraform.tfstate | jq | grep result
YLGmKyb3jOuI8sEf

# (참고) sensitive value 내용은 테라폼 콘솔에서 보일까요? 
echo "random_password.mypw" | terraform console
echo "random_password.mypw.result" | terraform console
  • 테라폼에서는 JSON 형태로 작성된 State를 통해 속성인수를 읽고 확인할 수 있다. 테라폼에서는 typename으로 고유한 리소스분류하며, 해당 리소스의 속성인수를 구성과 비교해 대상 리소스생성, 수정, 삭제한다.
  • State는 테라폼만을 위한 API로 정의할 수도 있다. Plan을 실행하면 암묵적으로 refresh 동작을 수행하면서 리소스 생성의 대상(클라우드 등)과 State를 기준으로 비교하는 과정을 거친다. 이 작업은 프로비저닝 대상의 응답 속도와 기존 작성된 State의 리소스 양에 따라 속도 차이가 발생한다. 대량의 리소스를 관리해야 하는 경우 Plan 명령에서 -refresh=false 플래그를 사용해 State를 기준으로 실행 계획을 생성하고, 이를 실행에 활용해 대상 환경과의 동기화 과정을 생략할 수 있다.
# 실행 계획 생성 시 저장되어 있는 State와 실제 형상을 비교하는 기본 실행
time terraform plan

# 실행 계획 생성 시 실제 형상과 비교하지 않고 실행 계획을 생성하는 -refresh=false 옵션
time terraform plan -refresh=false

5.2 State 동기화

  • 테라폼 구성과 State 흐름 : Plan 과 Apply 중 각 리소스에 발생할 수 있는 네 가지 사항, 아래 실행 계획 출력 기호와 의미기호 의미
    + Create
    - Destroy
    -/+ Replace
    ~ Updated in-place
    • Replace 동작은 기본값을 삭제 후 생성하지만 lifecycle의 create_before_destroy 옵션을 통해 생성 후 삭제 설정 가능
  • 유형 별 실습 + 문제상황 추가 : 테라폼 구성에 추가된 리소스와 State에 따라 어떤 동작이 발생하는지 다음 표로 살펴본다유형 구성 리소스 정의 State 구성 데이터 실제 리소스 기본 예상 동작
    1 있음     리소스 생성
    2 있음 있음   리소스 생성
    3 있음 있음 있음 동작 없음
    4   있음 있음 리소스 삭제
    5     있음 동작 없음
    6 있음   있음  

유형1 : 신규 리소스 정의 → Apply ⇒ 리소스 생성(처음부터 Terraform 코드로 작업)

locals {
  name = "mytest"
}

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

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

#########
terraform init && terraform apply -auto-approve
terraform state list
terraform state show aws_iam_user.myiamuser1

ls *.tfstate
cat terraform.tfstate | jq

terraform apply -auto-approve
ls *.tfstate

# iam 사용자 리스트 확인
aws iam list-users | jq

유형2 : 실제 리소스 수동 제거 → Apply ⇒ 리소스 생성

# 실제 리소스 수동 제거
aws iam delete-user --user-name mytest1
aws iam delete-user --user-name mytest2
aws iam list-users | jq

# 아래 명령어 실행 결과 차이는? (refresh 유무)
# 실제 리소스는 제거되었으나 -refrest=false로 인해 변경사항 없으므로 인식
terraform plan
terraform plan -refresh=false
cat terraform.tfstate | jq .serial

#
terraform apply -auto-approve
terraform state list
cat terraform.tfstate | jq .serial

# iam 사용자 리스트 확인
aws iam list-users | jq

유형3 : Apply → Apply - 코드, State, 형상 모두 일치한 경우 ⇒ 변경사항 없음

# Serial 값일 동일!
terraform apply -auto-approve
cat terraform.tfstate | jq .serial
terraform apply -auto-approve
cat terraform.tfstate | jq .serial
terraform apply -auto-approve
cat terraform.tfstate | jq .serial

유형4 : 코드에서 일부 리소스 삭제 → Apply ⇒ 리소스 삭제

locals {
  name = "mytest"
}

resource "aws_iam_user" "myiamuser1" {
  name = "${local.name}1"
}
#####################
# 코드 변경으로 인해 user2 삭제
terraform apply -auto-approve
terraform state list
terraform state show aws_iam_user.myiamuser1

ls *.tfstate
cat terraform.tfstate | jq

# iam 사용자 리스트 확인
aws iam list-users | jq

유형5 : 리소스만 있으며 코드, State는 없는 경우 → Import 또는 신규코드 작성

유형6 : 실수로 tfstate 파일 삭제 → plan/apply ← 책에는 없는 내용

# 실수로 tfstate 파일 삭제
rm -rf terraform.tfstate*

# 아래 두 명령 결과 차이는?
# Local에 State 파일이 없으므로 두 명령 모두 새로운 리소스 추가(add) 계획
terraform plan
terraform plan -refresh=false

# apply할 경우 어떤 결과 발생?
# 이미 리소스가 있으므로 Error 발생: EntityAlreadyExists: User with name mytest1 already exists.
terraform apply -auto-approve
terraform state list
cat terraform.tfstate | jq

# iam 사용자 리스트 확인
aws iam list-users | jq

# 다음 실습을 위해 iam user 삭제
aws iam delete-user --user-name mytest1

5.3 워크 스페이스

 

 

 

 

소개 : State관리하는 논리적인 가상 공간워크스페이스라고 한다 - 링크

  • CE와 Cloud/Enterprise에서 제공되는 Workspace의 기능을 비교해보자!Component Local Terraform Terraform Cloud/Enterprise
    Terraform configuration On disk In linked version control repository, or periodically uploaded via API/CLI
    Variable values As .tfvars files, as CLI arguments, or in shell environment In workspace
    State On disk or in remote backend In workspace
    Credentials and secrets In shell environment or entered at prompts In workspace, stored as sensitive variables(via Vault)
         
  • 테라폼 구성 파일은 동일하지만 작업자는 서로 다른 State를 갖는 실제 대상을 프로비저닝할 수 있다.
  • 워크스페이스는 기본 default로 정의된다. 로컬 작업 환경의 워크스페이스 관리를 위한 CLI 명령어로 workspace가 있다.
terraform workspace list
* default

실습

resource "aws_instance" "mysrv1" {
  ami           = "ami-0ea4d4b8dc1e46212"
  instance_type = "t2.micro"
  tags = {
    Name = "t101-week4"
  }
}

실행

# [분할/터미널1] 모니터링
export AWS_PAGER=""
while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done

# [분할/터미널2] 배포
terraform init && terraform apply -auto-approve
terraform state list

#
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.private_ip'

# terraform.tfstate에 private 담긴 내용은?
cat terraform.tfstate | jq -r '.resources[0].instances[0].private' | base64 -d | jq

# 워크스페이스 확인
terraform workspace list

# graph 확인
terraform graph > graph.dot

신규 워크스페이스 생성 및 확인

# 새 작업 공간 workspace 생성 : mywork1
terraform workspace new mywork1
terraform workspace show

# 서브 디렉터리 확인 -> 향후 저장되는 State는 여기에!
tree terraform.tfstate.d
terraform.tfstate.d
└── mywork1

# plan 시 어떤 결과 내용이 출력되나요?
terraform plan

# apply 해보자!
terraform apply -auto-approve


# 워크스페이스 확인
terraform workspace list

# State 확인
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
cat terraform.tfstate.d/mywork1/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'

# graph 확인
terraform graph > graph.dot


# (실습생략)새 작업 공간 workspace 생성 : mywork2
terraform workspace new mywork2

# 서브 디렉터리 확인
tree terraform.tfstate.d
...

# plan & apply
terraform plan && terraform apply -auto-approve
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
cat terraform.tfstate.d/mywork1/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
cat terraform.tfstate.d/mywork2/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'

# workspace 정보 확인
terraform workspace show
terraform workspace list

# 실습 리소스 삭제
terraform workspace select default
terraform destroy -auto-approve
terraform workspace select mywork1
terraform destroy -auto-approve
terraform workspace select mywork2
terraform destroy -auto-approve
  • 장점
    • 하나의 루트 모듈에서 다른 환경을 위한 리소스를 동일한 테라폼 구성으로 프로비저닝하고 관리
    • 기존 프로비저닝된 환경에 영향을 주지 않고 변경 사항 실험 가능
    • 깃의 브랜치 전략처럼 동일한 구성에서 서로 다른 리소스 결과 관리
      [참고 : 화해 - Git 브랜치 전략 수립을 위한 전문가의 조언들]
  • 단점
    • State가 동일한 저장소(로컬 또는 백엔드)에 저장되어 State 접근 권한 관리가 불가능(어려움)
    • 모든 환경이 동일한 리소스를 요구하지 않을 수 있으므로 테라폼 구성에 분기 처리가 다수 발생 가능
    • 프로비저닝 대상에 대한 인증 요소를 완벽히 분리하기 어려움⇒ 해결방안 1. 해결하기 위해 루트 모듈을 별도로 구성하는 디렉터리 기반의 레이아웃을 사용할 수 있다. ⇒ 해결방안 2. Terraform Cloud 환경의 워크스페이스를 활용
    • → 가장 큰 단점은 완벽한 격리가 불가능
블로그 이미지

감동맨

rkaehdaos의 블로그

,