Terraform/Study_과제

4주차 - State & 모듈

in-cloud 2023. 7. 29. 09:53

<참고> 테라폼으로 시작하는 IaC - 김민수, 김재준, 이규석, 이유종 저 한빛미디어

 

모든 실습은 Mac OS 기준으로 작성되었습니다

 

5. State

- 테라폼은 Statefull 애플리케이션이다.

- 프로비저닝 결과에 따른 State를 저장하고 프로비저닝한 모든 내용을 저장된 상태로 추적한다.

- 로컬 실행 환경에서는 terraform.tfstate파일에 JSON형태로 저장되고

- 팀이나 조직에서의 공동 관리를 위해서는 원격 저장소에 저장해 공유하는 방식을 활용한다.

- State에는 작업자가 정의한 코드와 실제 반영된 프로비저닝 결과를 저장하고

- 이 정보를 토대로 이후의 리소스 생성, 수정, 삭제에 대한 동작 판단 작업을 수행한다.

5.1 State 목적과 의미

- 대상 환경에서 어떤 리소스가 테라폼으로 관리되는 리소스인지 판별하고 결과를 기록한다.

- State의 역할

  • State에는 테라폼 구성과 실제를 동기화하고 각 리소스에 고유한 아이디(리소스 주소)로 맵핑
  • 리소스 종속성과 같은 메타데이터를 저장하고 추적
  • 테라폼 구성으로 프로비저닝된 결과를 캐싱하는 역할을 수행

- main.tf (random provider는 테라폼 구성 내에서 무작위로 기입해야 되는 숫자, 패스워드, 문자열 등의 값을 생성하는데 사용한다)

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

- 실행 결과

$ terraform init && terraform plan
$ ls *.tfstate

$ terraform apply -auto-approve
$ terraform state list
$ terraform state show random_password.mypw
resource "random_password" "mypw" {
    bcrypt_hash      = (sensitive value)
    id               = "none"
    length           = 16
    lower            = true
    min_lower        = 0
    min_numeric      = 0
    min_special      = 0
    min_upper        = 0
    number           = true
    numeric          = true
    override_special = "!#$%"
    result           = (sensitive value)
    special          = true
    upper            = true
}

$ ls *.tfstate

$ cat terraform.tfstate | jq | grep result
 "result": "btvRzj8AraAl$OXT",

# (참고) sensitive value 내용은 테라폼 콘솔에서 보일까요? >>>>>>> 안보입니다!!!!!
(sensitive value)
# echo "random_password.mypw.result" | terraform console

 

- 테라폼에서는 JSON형태로 작성된 State를 통해 속성과 인수를 읽고 확인할 수 있다.

- 테라폼에서는 type과 name으로 고유한 리소스를 분류하며,

- 해당 리소스의 속성과 인수를 구성과 비교해 대상 리소스를 생성, 수정, 삭제한다.

- State는 테라폼만을 위한 API로 정의할 수 있다.

- Plan을 실행시 암묵적으로 refresh 동작을 수행하며 리소스의 생성대상(클라우드, SaaS 서비스 등)과 State 비교한다. 

- 이 작업은 프로비저닝 대상의 응답 속도와 기존 작성된 State의 리소스 양에 따라 속도 차이가 발생한다.

- 대량의 리소스를 관리해야 하는 경우 Plan 명령에서 -refresh=false플래그를 사용해 State를 기준으로 실행 계획을 생성하고 이를 실행에 활용해 대상 환경과의 동기화 과정을 생략할 수 있다.

# 실행 계획 생성 시 저장되어 있는 State와 실제 형상을 비교하는 기본 실행
time terraform plan

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

5.2 State 동기화

- 테라폼의 구성 파일은 기존의 State와 구성을 비교해 실행 계획에서 생성, 수정, 삭제 여부를 결정한다.

출처 : https://kschoi728.tistory.com/135

- 테라폼 구성과 State 흐름 : Plan과 Apply 중 각 리소스에 발생할 수 있는 네가지 사항

기호 의미 비고
+ Create  
- Destroy  
-/+ Replace Replace동작은 기본값을 삭제 후 생성, LifeCycle의 create_before_destroy 를 통해 생성 후 삭제 설정 가능
~ Update in-place  

- 유형 별 실습

유형 구성 리소스 정의 State 구성 데이터 실제 리소스 기본 예상 동작
1 있음     리소스 생성
2 있음 있음   리소스 생성
3 있음 있음 있음 동작 없음
4   있음 있음 리소스 삭제
5     있음 동작 없음
  • 유형1 - 신규 리소스 정의 -> Apply -> 리소스 생성
# main.tf
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
aws_iam_user.myiamuser1
aws_iam_user.myiamuser2

$ terraform state show aws_iam_user.myiamuser1
# aws_iam_user.myiamuser1:
resource "aws_iam_user" "myiamuser1" {
    arn           = "arn:aws:iam::************:user/mytest1"
    force_destroy = false
    id            = "mytest1"
    name          = "mytest1"
    path          = "/"
    tags_all      = {}
    unique_id     = "AIDAWQMQENO3T56HWVIHI"
}


#
$ ls *.tfstate
terraform.tfstate

$ cat terraform.tfstate | jq
{
  "version": 4,
  "terraform_version": "1.5.2",
  "serial": 6,
  "lineage": "1eedf1e2-1afa-ec85-0534-bb6c8804e7e1",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_iam_user",
      "name": "myiamuser1",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "arn": "arn:aws:iam::************:user/mytest1",
            "force_destroy": false,
            "id": "mytest1",
            "name": "mytest1",
            "path": "/",
            "permissions_boundary": null,
            "tags": null,
            "tags_all": {},
            "unique_id": "AIDAWQMQENO3T56HWVIHI"
          },
          "sensitive_attributes": [],
          "private": "bnVsbA=="
        }
      ]
    },
    {
      "mode": "managed",
      "type": "aws_iam_user",
      "name": "myiamuser2",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "arn": "arn:aws:iam::************:user/mytest2",
            "force_destroy": false,
            "id": "mytest2",
            "name": "mytest2",
            "path": "/",
            "permissions_boundary": null,
            "tags": null,
            "tags_all": {},
            "unique_id": "AIDAWQMQENO346ZD3X64L"
          },
          "sensitive_attributes": [],
          "private": "bnVsbA=="
        }
      ]
    }
  ],
  "check_results": null
}
#
$ terraform apply -auto-approve
aws_iam_user.myiamuser1: Refreshing state... [id=mytest1]
aws_iam_user.myiamuser2: Refreshing state... [id=mytest2]

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.


$ ls *.tfstate
terraform.tfstate

# iam 사용자 리스트 확인
$ aws iam list-users | jq
{
  "Users": [
    {
      "Path": "/",
      "UserName": "mytest1",
      "UserId": "AIDAWQMQENO3T56HWVIHI",
      "Arn": "arn:aws:iam::************:user/mytest1",
      "CreateDate": "2023-07-28T05:28:27+00:00"
    },
    {
      "Path": "/",
      "UserName": "mytest2",
      "UserId": "AIDAWQMQENO346ZD3X64L",
      "Arn": "arn:aws:iam::************:user/mytest2",
      "CreateDate": "2023-07-28T05:28:27+00:00"
    }
  ]
}
  • 유형2 - 실제 리소스 수동 제거 -> Apply -> 리소스 생성
# 실제 리소스 수동 제거
$ aws iam delete-user --user-name mytest1
$ aws iam delete-user --user-name mytest2
$ aws iam list-users | jq
# 기존에 내 계정에 생성되어있던 유저만 나옴. mytest1, mytest2 삭제됨

# 아래 명령어 실행 결과 차이는?
$ terraform plan
# aws_iam_user.myiamuser1 will be created
  + resource "aws_iam_user" "myiamuser1" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "mytest1"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

  # aws_iam_user.myiamuser2 will be created
  + resource "aws_iam_user" "myiamuser2" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "mytest2"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

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

$ terraform plan -refresh=false
No changes. Your infrastructure matches the configuration.

$ cat terraform.tfstate | jq .serial
7

#
$ terraform apply -auto-approve
...
aws_iam_user.myiamuser2: Creation complete after 1s [id=mytest2]
aws_iam_user.myiamuser1: Creation complete after 1s [id=mytest1]

$ terraform state list
aws_iam_user.myiamuser1
aws_iam_user.myiamuser2

$ cat terraform.tfstate | jq .serial
10

# iam 사용자 리스트 확인
$ aws iam list-users | jq
{
  "Users": [
    {
      "Path": "/",
      "UserName": "mytest1",
      "UserId": "AIDAWQMQENO3RH7QVKARC",
      "Arn": "arn:aws:iam::************:user/mytest1",
      "CreateDate": "2023-07-23T11:21:26+00:00"
    },
    {
      "Path": "/",
      "UserName": "mytest2",
      "UserId": "AIDAWQMQENO35DD6LDHW2",
      "Arn": "arn:aws:iam::************:user/mytest2",
      "CreateDate": "2023-07-23T11:21:26+00:00"
    }
  ]
}
  • 유형3 : apply -> apply : 코드, State, 형상 모두 일치한 경우
#$ terraform apply -auto-approve
aws_iam_user.myiamuser2: Refreshing state... [id=mytest2]
aws_iam_user.myiamuser1: Refreshing state... [id=mytest1]

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.

$ cat terraform.tfstate | jq .serial
11

$ terraform apply -auto-approve
aws_iam_user.myiamuser2: Refreshing state... [id=mytest2]
aws_iam_user.myiamuser1: Refreshing state... [id=mytest1]

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.

$ cat terraform.tfstate | jq .serial
11

$ terraform apply -auto-approve
aws_iam_user.myiamuser1: Refreshing state... [id=mytest1]
aws_iam_user.myiamuser2: Refreshing state... [id=mytest2]

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.

$ cat terraform.tfstate | jq .serial
11
  • 유형 4 : 코드에서 일부 리소스 삭제 -> apply
# main.tf
locals {
  name = "mytest"
}

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

유형 4 실행

$ terraform apply -auto-approve
aws_iam_user.myiamuser2: Refreshing state... [id=mytest2]
aws_iam_user.myiamuser1: Refreshing state... [id=mytest1]

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_iam_user.myiamuser2 will be destroyed
  # (because aws_iam_user.myiamuser2 is not in configuration)
  - resource "aws_iam_user" "myiamuser2" {
      - arn           = "arn:aws:iam::************:user/mytest2" -> null
      - force_destroy = false -> null
      - id            = "mytest2" -> null
      - name          = "mytest2" -> null
      - path          = "/" -> null
      - tags          = {} -> null
      - tags_all      = {} -> null
      - unique_id     = "AIDAWQMQENO3V57VZD75N" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.
aws_iam_user.myiamuser2: Destroying... [id=mytest2]
aws_iam_user.myiamuser2: Destruction complete after 1s

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

$ terraform state list
aws_iam_user.myiamuser1

$ terraform state show aws_iam_user.myiamuser1
# aws_iam_user.myiamuser1:
resource "aws_iam_user" "myiamuser1" {
    arn           = "arn:aws:iam::************:user/mytest1"
    force_destroy = false
    id            = "mytest1"
    name          = "mytest1"
    path          = "/"
    tags          = {}
    tags_all      = {}
    unique_id     = "AIDAWQMQENO3ZMDQRR74V"
}

#
$ ls *.tfstate
terraform.tfstate

$ cat terraform.tfstate | jq
{
  "version": 4,
  "terraform_version": "1.5.2",
  "serial": 13,
  "lineage": "1eedf1e2-1afa-ec85-0534-bb6c8804e7e1",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_iam_user",
      "name": "myiamuser1",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "arn": "arn:aws:iam::************:user/mytest1",
            "force_destroy": false,
            "id": "mytest1",
            "name": "mytest1",
            "path": "/",
            "permissions_boundary": null,
            "tags": {},
            "tags_all": {},
            "unique_id": "AIDAWQMQENO3ZMDQRR74V"
          },
          "sensitive_attributes": [],
          "private": "bnVsbA=="
        }
      ]
    }
  ],
  "check_results": null
}

# iam 사용자 리스트 확인
$ aws iam list-users | jq
{
  "Users": [
    {
      "Path": "/",
      "UserName": "mytest1",
      "UserId": "AIDAWQMQENO3ZMDQRR74V",
      "Arn": "arn:aws:iam::************:user/mytest1",
      "CreateDate": "2023-07-28T06:44:54+00:00"
    }
  ]
}
  • 유형6 - 코드에서 일부 리소스 삭제 -> apply
# 실수로 tfstate 파일 삭제
$ rm -rf terraform.tfstate*

#
$ terraform plan
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_iam_user.myiamuser1 will be created
  + resource "aws_iam_user" "myiamuser1" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "mytest1"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

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


$ terraform plan -refresh=false
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_iam_user.myiamuser1 will be created
  + resource "aws_iam_user" "myiamuser1" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "mytest1"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

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

$ terraform apply -auto-approve
...
Plan: 1 to add, 0 to change, 0 to destroy.
aws_iam_user.myiamuser1: Creating...
╷
│ Error: creating IAM User (mytest1): EntityAlreadyExists: User with name mytest1 already exists.
│       status code: 409, request id: 4f3f74ed-2a57-476c-9a29-379f9e9e4556
│ 
│   with aws_iam_user.myiamuser1,
│   on main.tf line 5, in resource "aws_iam_user" "myiamuser1":
│    5: resource "aws_iam_user" "myiamuser1" {


$ terraform state list

$ cat terraform.tfstate | jq
{
  "version": 4,
  "terraform_version": "1.5.2",
  "serial": 1,
  "lineage": "c09849cf-d47a-7036-ac57-4096a541cad8",
  "outputs": {},
  "resources": [],
  "check_results": null
}


# iam 사용자 리스트 확인
$ aws iam list-users | jq
{
  "Users": [
    {
      "Path": "/",
      "UserName": "mytest1",
      "UserId": "AIDAWQMQENO3ZMDQRR74V",
      "Arn": "arn:aws:iam::************:user/mytest1",
      "CreateDate": "2023-07-28T06:44:54+00:00"
    }
  ]
}

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

 

5.3 워크스페이스 - State를 관리하는 논리적인 가상 공간

이미지 출처 : https://kschoi728.tistory.com/136

 

  • 테라폼 구성 파일은 동일하지만 작업자는 서로 다른 State를 갖는 실제 대상을 프로비저닝 할 수 있다.
  • 워크 스페이스는 기본 default로 정의 된다.
  • 로컬 작업 환경의 워크스페이스를 관리를 위한 CLI명령어로 workspace가 있다.
$ terraform workspace list
* default

 

# main.tf

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

- 모니터링화면
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
 - 실행

terraform init && terraform apply -auto-approve




# [분할/터미널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

#
$ terraform init && terraform apply -auto-approve

$ terraform state list
aws_instance.mysrv1

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

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

# terraform.tfstate에 private 담긴 내용은?
$ cat terraform.tfstate | jq -r '.resources[0].instances[0].private' | base64 -d | jq
{
  "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0": {
    "create": 600000000000,
    "delete": 1200000000000,
    "update": 600000000000
  },
  "schema_version": "1"
}

# 워크스페이스 확인
$ terraform workspace list
* default

# graph 확인
$ terraform graph > graph.dot

 

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

# 새 작업 공간 workspace 생성 : mywork1
$ terraform workspace new mywork1
Created and switched to workspace "mywork1"!
...

$ terraform workspace show
mywork1

# 서브 디렉터리 확인
$ tree terraform.tfstate.d
terraform.tfstate.d
└── mywork1

2 directories, 0 files

# plan 시 어떤 결과 내용이 출력되나요?
$ terraform plan
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_instance.mysrv1 will be created
  + resource "aws_instance" "mysrv1" {
      + ami                                  = "ami-0ea4d4b8dc1e46212"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = (known after apply)
      + 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_lifecycle                   = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t2.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = (known after apply)
      + 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
      + spot_instance_request_id             = (known after apply)
      + subnet_id                            = (known after apply)
      + tags                                 = {
          + "Name" = "t101-week4"
        }
      + tags_all                             = {
          + "Name" = "t101-week4"
        }
      + 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)
    }

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


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


# 워크스페이스 확인
$ terraform workspace list
* default
  mywork1
  
#
$ cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
3.35.173.174
$ cat terraform.tfstate.d/mywork1/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
54.180.83.173

# graph 확인
$ terraform graph > graph.dot


# 새 작업 공간 workspace 생성 : mywork2
$ terraform workspace new mywork2
Created and switched to workspace "mywork2"!
....


# 서브 디렉터리 확인
$ tree terraform.tfstate.d
terraform.tfstate.d
├── mywork1
│   └── terraform.tfstate
└── mywork2

# plan & apply
$ terraform plan && terraform apply -auto-approve

$ cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
3.35.173.174
$ cat terraform.tfstate.d/mywork1/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
54.180.83.173
$ cat terraform.tfstate.d/mywork2/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
3.38.211.38

# workspace 정보 확인
$ terraform workspace show
mywork2

$ terraform workspace list
  default
  mywork1
* mywork2

# 실습 리소스 삭제
$ terraform workspace select default
$ terraform destroy -auto-approve
$ terraform workspace select mywork1
$ terraform destroy -auto-approve
$ terraform workspace select mywork2
$ terraform destroy -auto-approve

장점 단점
하나의 루트 모듈에서 다른 환경을 위한 리소스를 동일한 테라폼 구성으로 프로비저닝 하고 관리 State가 동일한 저장소(로컬 또는 백엔드)에 저장되어 State 접근 권한 관리가 불가능
기존 프로비저닝된 환경에 영향을 주지 않고 변경 사항 실험 가능 모든 환경이 동일한 리소스를 요구하지 않을 수 있으므로 테라폼 구성에 분기 처리가 다수 발생가능
깃의 브랜치 전략처럼 동일한 구성에서 서로 다른 리소스 결과 관리 프로비저닝 대상에 대한 인증 요소를 완벽히 분리하기 어려움
--> 가장 큰 단점은 완벽한 격리가 불가능
=> 해결하기 위해 루트 모듈을 별도로 구성하는 디렉터리 기반의 레이아웃을 사용할 수 있다 or Terraform Cloud 환경의 워크스페이스를 활용

 

 

6. Module

- 테라폼으로 인프라와 서비스를 관리하면 시간이 지날수록 구성이 복잡해지고 관리하는 리소스가 늘어나게 된다. 테라폼의 구성 파일과 디렉터리 구성에는 제약이 없기 때문에 단일 파일 구조상에서 지속적으로 업데이트할 수 있지만, 다음과 같은 문제가 발생한다.

  1. 테라폼 구성에서 원하는 항목을 찾고 수정하는 것이 점점 어려워짐
  2. 리소스들 간의 연관 관계가 복잡해질수록 변경 작업의 영향도를 분석하기 위한 노력이 늘어남
  3. 개발/스테이징/프로덕션 환경으로 구분된 경우 비슷한 형태의 구성이 반복되어 업무 효율이 줄어듦
  4. 새로운 프로젝트를 구성하는 경우 기존 구성에서 취해야 할 리소스 구성과 종속성 파악이 어려움

 

- 모듈은 루트 모듈과 자식 모듈로 구분된다

  • 루트 모듈 : root Module 테라폼을 실행하고 프로비저닝 하는 최상위 모듈
  • 자식 모듈 : chile Module 루트 모듈의 구성에서 호출되는 외부 구성 집합

  • 모듈은 테라폼 구성의 집합이다.
  • 테라폼으로 관리하는 대상의 규모가 커지고 복잡해져 생긴 문제를 보완하고 관리 작업을 수월하게 하기 위한 방안으로 활용
    • 관리성 : 모듈은 서로 연관 있는 구성의 묶음이다. 원하는 구성 요소를 단위별로 쉽게 찾고 업데이트 할 수 있다. 모듈은 다른 구성에서 쉽게 하나의 덩어리로 추가하거나 삭제할 수 있다. 또한 모듈이 업데이트되면 이 모듈을 사용하는 모든 구성에서 일관된 변경 작업을 진행할 수 있다.
    • 캡슐화 : 테라폼 구성 내에서 각 모듈은 논리적으로 묶여져 독립적으로 프로비저닝 및 관리되며, 그 결과는 은닉성을 갖춰 필요한 항목만을 외부에 노출 시킨다.
    • 재사용성 : 구성을 처음부터 작성하는 것에는 시간과 노력이 필요하고 작성 중간에 디버깅과 오류를 수정하는 반복 작업이 발생한다. 테라폼 구성을 모듈화하면 이후에 비슷한 프로비저닝에 이미 검증된 구성을 바로 사용할 수 있다.
    • 일관성과 표준화 : 테라폼 구성 시 모듈을 활용하는 워크플로는 구성의 일관성을 제공하고 서로 다른 환경과 프로젝트에도 이미 검증한 모듈을 적용해 복잡한 구성과 보안 사고를 방지할 수 있다.

6.1 모듈 작성 기본 원칙

- 기본 원칙 : 모듈은 대부분의 프로그래밍 언어에서 쓰이는 라이브러리나 패키지와 역할이 비슷하다

    • 모듈 디렉터리 형식을 terraform-<프로바이더 이름>-<모듈 이름> 형식을 제안한다. 이 형식은 Terraform Cloud, Terraform Enterprise에서도 사용되는 방식으로 1) 디렉터리 또는 레지스트리 이름이 테라폼을 위한 것이고, 2) 어떤 프로바이더의 리소스를 포함하고 있으며, 3) 부여된 이름이 무엇인지 판별할 수 있도록 한다.
    • 테라폼 구성은 궁극적으로 모듈화가 가능한 구조로 작성할 것을 제안한다. 처음부터 모듈화를 가정하고 구성파일을 작성하면 단일 루트 모듈이라도 후에 다른 모듈이 호출할 것을 예상하고 구조화할 수 있다. 또한 작성자는 의도한 리소스 묶음을 구상한 대로 논리적인 구조로 그룹화할 수 있다.
    • 각각의 모듈을 독립적으로 관리하기를 제안한다. 리모트 모듈을 사용하지 않더라도 처음부터 모듈화가 진행된 구성들은 떄로 루트 모듈의 하위 파일 시스템에 존재하는 경우가 있다. 하위 모듈 또한 독립적인 모듈이므로 루트 모듈 하위에 두기보다는 동일한 파일 시스템 레벨에 위치하거나 별도 모듈만을 위한 공간에서 불러오는 것을 권장한다. 이렇게 하면 VCS를 통해 관리하기가 더 수월하다.
    • 공개된 테라폼 레지스트리의 모듈을 참고하기를 제안한다. 대다수의 테라폼 모듈은 공개된 모듈이 존재하고 거의 모든 인수에 대한 변수 처리, 반복문 적용 리소스, 조건에 따른 리소스 활성/비활성 등을 모범 사례로 공개해두었다. 물론 그대로 가져다 사용하는 것보다는 프로비저닝하려는 상황에 맞게 참고하는 것을 권장한다.
    • 작성된 모듈은 공개 또는 비공개로 게시해 팀 또는 커뮤니티와 공유하기를 제안한다. 모듈의 사용성을 높이고 피드백을 통해 더 발전된 모듈을 구성할 수 있는 자극이 된다.

- 모듈을 독립적으로 관리하기 위해 디렉터리 구조를 생성할 때 모듈을 위한 별도 공간을 생성하는 방식으로 진행한다. 특정 루트 모듈 하위에 자식 모듈을 구성하는 경우 단순히 복잡한 코드를 분리하는 요도로 명시되며 종속성이 발생하므로 루트 모듈 사이에 모듈 디렉터리를 지정한다.

구성 예시

6.2 모듈화 해보기

- 모듈화 소개

  • 모듈의 기본적 구조는 테라폼 구성을 입력 변수를 구성하고 결과를 출력하기 위한 구조로 구성 한다.

  • 모듈화 라는 용어는 이런 구조를 재활용하기 위한 템플릿 작업을 말한다.
  • 애플리케이션 개발시에도 자주 사용되는 용어로 테라폼은 작성된 모듈을 다른 루트 모듈에서 가져다 사용하며 이를 통해 재사용성과 표준화 구조를 구성할 수 있다.

  • 기존에 작성된 모듈은 다른 모듈에서 참조해 사용할 수 있다. 사용 방식은 리소스와 비슷하다.
  • 모듈에서 필요한 값은 variable로 선언해 설정하고, 모듈에서 생성된 값 중 외부 모듈에서 참조하고 싶은 값은 output으로 설정한다. 마치 자바 개발 시 getter, setter로 캡슐화된 클래스를 활용하는 것과 비슷하다.

- 모듈 작성실습

  • 가상의 시나리오 : 하나의 프로비저닝 에서 사용자와 패스워드를 여러 번 구성해야 하는 경우
    • random_pet : 이름을 자동으로 생성하고, random_password는 사용자의 패스워드를 설정한다
    • random_password는 random 프로바이더 리소스로 난수 형태로 패스워드를 만들 수 있다.

1. 자식 모듈 작성

- 디렉터리 생성 및 06-module-traning/modules/terraform-random-pwgen/main.tf variable.tf output.tf 파일 생성

mkdir -p 06-module-traning/modules/terraform-random-pwgen

- main.tf

resource "random_pet" "name" {
  keepers = {
    ami_id = timestamp()
  }
}

resource "random_password" "password" {
  length           = var.isDB ? 16 : 10
  special          = var.isDB ? true : false
  override_special = "!#$%*?"
}

- variable.tf

variable "isDB" {
  type        = bool
  default     = false
  description = "패스워드 대상의 DB 여부"
}

- output.tf

output "id" {
  value = random_pet.name.id
}

output "pw" {
  value = nonsensitive(random_password.password.result) 
}

- 자식 모듈 동작 테스트

$ ls *.tf
main.tf         output.tf       variable.tf

$ terraform init && terraform plan
...
Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + id = (known after apply)
  + pw = (known after apply)
  
# 테스트를 위해 apply 시 변수 지정
$ terraform apply -auto-approve -var=isDB=true
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

id = "tidy-grouper"
pw = "N?MCgsyrGvaMxR8q"

# 확인
$ terraform state list
random_password.password
random_pet.name

$ terraform state show random_pet.name
# random_pet.name:
resource "random_pet" "name" {
    id        = "tidy-grouper"
    keepers   = {
        "ami_id" = "2023-07-28T08:31:02Z"
    }
    length    = 2
    separator = "-"
}

$ terraform state show random_password.password
# random_password.password:
resource "random_password" "password" {
    bcrypt_hash      = (sensitive value)
    id               = "none"
    length           = 16
    lower            = true
    min_lower        = 0
    min_numeric      = 0
    min_special      = 0
    min_upper        = 0
    number           = true
    numeric          = true
    override_special = "!#$%*?"
    result           = (sensitive value)
    special          = true
    upper            = true
}

# tfstate에 모듈 정보 확인
$ cat terraform.tfstate | grep module

# graph 확인
$ terraform graph > graph.dot

- 자식 모듈 호출 실습

  • 다수의 리소스를 같은 목적으로 여러 번 반복해서 사용하려면 리소스 수만큼 반복해 구성 파일을 정의해야 하고 이름도 고유하게 설정해줘야 하는 부담이 있지만, 모듈을 활용하면 반복되는 리소스 묶음을 최소화할 수 있다.

- 디렉터리 생성 및 06-module-traning/06-01-basic/main.tf 파일 생성

mkdir -p 06-module-traning/06-01-basic

- main.tf

module "mypw1" {
  source = "../modules/terraform-random-pwgen"
}

module "mypw2" {
  source = "../modules/terraform-random-pwgen"
  isDB   = true
}

output "mypw1" {
  value  = module.mypw1
}

output "mypw2" {
  value  = module.mypw2
}

- 실행 : 자식 모듈을 호출해 반복 재사용하는 루트 모듈의 결과

$ cd 06-module-traning/06-01-basic

$ terraform init && terraform plan && terraform apply -auto-approve
...
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

mypw1 = {
  "id" = "climbing-pug"
  "pw" = "7cAUheslj1"
}
mypw2 = {
  "id" = "hopeful-fox"
  "pw" = "LB8y!OVtRO5ETf18"
}

# 확인
$ terraform state list
module.mypw1.random_password.password
module.mypw1.random_pet.name
module.mypw2.random_password.password
module.mypw2.random_pet.name

# tfstate에 모듈 정보 확인
$ cat terraform.tfstate | grep module
      "module": "module.mypw1",
      "module": "module.mypw1",
      "module": "module.mypw2",
      "module": "module.mypw2",
      
      
# terraform init 시 생성되는 modules.json 파일 확인
$ tree .terraform
.terraform
├── modules
│   └── modules.json
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── random
                └── 3.5.1
                    └── darwin_arm64
                        └── terraform-provider-random_v3.5.1_x5

8 directories, 2 file


## 모듈로 묶여진 리소스는 module이라는 정의를 통해 단순하게 재활용하고 반복 사용할 수 있다.
## 모듈의 결과 참조 형식은 module.<모듈 이름>.<output 이름>으로 정의된다.
cat .terraform/modules/modules.json | jq
{
  "Modules": [
    {
      "Key": "",
      "Source": "",
      "Dir": "."
    },
    {
      "Key": "mypw1",
      "Source": "../modules/terraform-random-pwgen",
      "Dir": "../modules/terraform-random-pwgen"
    },
    {
      "Key": "mypw2",
      "Source": "../modules/terraform-random-pwgen",
      "Dir": "../modules/terraform-random-pwgen"
    }
  ]
}

# graph 확인
terraform graph > graph.dot

6.3 모듈 사용 방식

- 모듈과 프로바이더

  • 모듈에서 사용되는 모든 리소스는 관련 프로바이더의 정의가 필요하다.
  • 여기서 사용자는 프로바이더 정의를 모듈 안 또는 밖 고민

- 유형 1. 자식 모듈에서 프로바이더 정의

  • 모듈에서 사용하는 프로바이더 버전과 구성 상세를 자식 모듈에서 고정하는 방법
    • 프로바이더 버전과 구성에 민감하거나, 루트 모듈에서 프로바이더 정의 없이 자식 모듈이 독립적인 구조일 때 고려할 방법
    • 동일한 프로바이더가 루트와 자식 양쪽 또는 서로 다른 자식 모듈에 버전 조건 합의가 안되면, 오류가 발생하고 모듈에 반복문을 사용할 수 없다는 단점이 있으므로 잘 사용하지 않는다.

- 유형 2. 루트 모듈에서 프로바이더 정의

  • 자식 모듈은 루트 모듈의 프로바이더 구성에 종속되는 방식
  • 디렉터리 구조로는 분리되어 있지만 테라폼 실행 단계에서 동일 계층으로 해석되므로 프로바이더 버전과 구성은 루트 모듈의 설정이 적용된다. 프로바이더를 모듈 내 리소스와 데이터 소스에 일괄 적용하고, 자식 모듈에 대한 반복문 사용에 자유로운 것이 장점이다. 자식 모듈에 특정 프로바이더 구성의 종속성은 반영할 수 없으므로 자식 모듈을 프로바이더 조건에 대해 기록하고, 자식 모듈을 사용하는 루트 모듈에서 정의하는 프로바이더에 맞게 업데이트 해야 한다.
  • 다음은 동일한 모듈에 사용되는 프로바이더 조건이 다른 경우 각 모듈별로 프로바이더를 맵핑하는 방안이다.
  • 리소스와 데이터 소스에 provider 메타인수로 지정하는 방식과 비슷하나 모듈에는 다수의 프로바이더가 사용될 가능성이 있으므로 map 타입으로 구성하는 provider로 정의한다. 실습을 위한 디렉터리 구성의 예는 다음과 같다.

  • 디렉터리 생성 및 06-module-traning/modules/terraform-aws-ec2/main.tf variable.tf output.tf 파일 생성
mkdir -p 06-module-traning/modules/terraform-aws-ec2/

- main.tf

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
}

resource "aws_default_vpc" "default" {}

data "aws_ami" "default" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm*"]
  }
}

resource "aws_instance" "default" {
  depends_on    = [aws_default_vpc.default]
  ami           = data.aws_ami.default.id
  instance_type = var.instance_type

  tags = {
    Name = var.instance_name
  }
}

-variable.tf

variable "instance_type" {
  description = "vm 인스턴스 타입 정의"
  default     = "t2.micro"
}

variable "instance_name" {
  description = "vm 인스턴스 이름 정의"
  default     = "my_ec2"
}

- output.tf

output "private_ip" {
  value = aws_instance.default.private_ip
}

- 작성된 모듈을 사용할 루트 모듈 디렉터리 생성 및 06-module-traning/multi_provider_for_module/main.tf output.tf 파일 생성

mkdir -p 06-module-traning/multi_provider_for_module/

- main.tf

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

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

module "ec2_singapore" {
  source = "../modules/terraform-aws-ec2"
}

module "ec2_seoul" {
  source = "../modules/terraform-aws-ec2"
  providers = {
    aws = aws.seoul
  }
  instance_type = "t3.small"
}

- output.tf

output "module_output_singapore" {
  value = module.ec2_singapore.private_ip
}

output "module_output_seoul" {
  value = module.ec2_seoul.private_ip
}

- 실행 : 프로바이더 구성을 테스트

$ cd 06-module-traning/multi_provider_for_module/
$ terraform init
$ cat .terraform/modules/modules.json | jq
{
  "Modules": [
    {
      "Key": "",
      "Source": "",
      "Dir": "."
    },
    {
      "Key": "ec2_seoul",
      "Source": "../modules/terraform-aws-ec2",
      "Dir": "../modules/terraform-aws-ec2"
    },
    {
      "Key": "ec2_singapore",
      "Source": "../modules/terraform-aws-ec2",
      "Dir": "../modules/terraform-aws-ec2"
    }
  ]
}

$ terraform apply -auto-approve
$ terraform output
Outputs:

module_output_seoul = "172.31.43.150"
module_output_singapore = "172.31.42.158"


$ terraform state list
module.ec2_seoul.data.aws_ami.default
module.ec2_seoul.aws_default_vpc.default
module.ec2_seoul.aws_instance.default
module.ec2_singapore.data.aws_ami.default
module.ec2_singapore.aws_default_vpc.default
module.ec2_singapore.aws_instance.default


$ terraform state show module.ec2_seoul.data.aws_ami.default
# module.ec2_seoul.data.aws_ami.default:
data "aws_ami" "default" {
    architecture          = "x86_64"
    arn                   = "arn:aws:ec2:ap-northeast-2::image/ami-0fe59d64a644afc2e"
    block_device_mappings = [
        {
            device_name  = "/dev/xvda"
            ebs          = {
                "delete_on_termination" = "true"
                "encrypted"             = "false"
                "iops"                  = "0"
                "snapshot_id"           = "snap-0078482d2de7a61a1"
                "throughput"            = "0"
                "volume_size"           = "8"
                "volume_type"           = "gp2"
            }
            no_device    = ""
            virtual_name = ""
        },
    ]
    creation_date         = "2023-07-20T00:59:01.000Z"
    deprecation_time      = "2025-07-20T00:59:01.000Z"
    description           = "Amazon Linux 2 AMI 2.0.20230719.0 x86_64 HVM gp2"
    ena_support           = true
    hypervisor            = "xen"
    id                    = "ami-0fe59d64a644afc2e"
    image_id              = "ami-0fe59d64a644afc2e"
    image_location        = "amazon/amzn2-ami-hvm-2.0.20230719.0-x86_64-gp2"
    image_owner_alias     = "amazon"
    image_type            = "machine"
    include_deprecated    = false
    most_recent           = true
    name                  = "amzn2-ami-hvm-2.0.20230719.0-x86_64-gp2"
    owner_id              = "137112412989"
    owners                = [
        "amazon",
    ]
    platform_details      = "Linux/UNIX"
    product_codes         = []
    public                = true
    root_device_name      = "/dev/xvda"
    root_device_type      = "ebs"
    root_snapshot_id      = "snap-0078482d2de7a61a1"
    sriov_net_support     = "simple"
    state                 = "available"
    state_reason          = {
        "code"    = "UNSET"
        "message" = "UNSET"
    }
    tags                  = {}
    usage_operation       = "RunInstances"
    virtualization_type   = "hvm"

    filter {
        name   = "name"
        values = [
            "amzn2-ami-hvm*",
        ]
    }
    filter {
        name   = "owner-alias"
        values = [
            "amazon",
        ]
    }
}

$ terraform state show module.ec2_singapore.data.aws_ami.default
# module.ec2_singapore.data.aws_ami.default:
data "aws_ami" "default" {
    architecture          = "x86_64"
    arn                   = "arn:aws:ec2:ap-southeast-1::image/ami-01c0d787487683f26"
    block_device_mappings = [
        {
            device_name  = "/dev/xvda"
            ebs          = {
                "delete_on_termination" = "true"
                "encrypted"             = "false"
                "iops"                  = "0"
                "snapshot_id"           = "snap-0f42d7ade812bb056"
                "throughput"            = "0"
                "volume_size"           = "8"
                "volume_type"           = "gp2"
            }
            no_device    = ""
            virtual_name = ""
        },
    ]
    creation_date         = "2023-07-20T00:59:02.000Z"
    deprecation_time      = "2025-07-20T00:59:02.000Z"
    description           = "Amazon Linux 2 AMI 2.0.20230719.0 x86_64 HVM gp2"
    ena_support           = true
    hypervisor            = "xen"
    id                    = "ami-01c0d787487683f26"
    image_id              = "ami-01c0d787487683f26"
    image_location        = "amazon/amzn2-ami-hvm-2.0.20230719.0-x86_64-gp2"
    image_owner_alias     = "amazon"
    image_type            = "machine"
    include_deprecated    = false
    most_recent           = true
    name                  = "amzn2-ami-hvm-2.0.20230719.0-x86_64-gp2"
    owner_id              = "137112412989"
    owners                = [
        "amazon",
    ]
    platform_details      = "Linux/UNIX"
    product_codes         = []
    public                = true
    root_device_name      = "/dev/xvda"
    root_device_type      = "ebs"
    root_snapshot_id      = "snap-0f42d7ade812bb056"
    sriov_net_support     = "simple"
    state                 = "available"
    state_reason          = {
        "code"    = "UNSET"
        "message" = "UNSET"
    }
    tags                  = {}
    usage_operation       = "RunInstances"
    virtualization_type   = "hvm"

    filter {
        name   = "name"
        values = [
            "amzn2-ami-hvm*",
        ]
    }
    filter {
        name   = "owner-alias"
        values = [
            "amazon",
        ]
    }
}

$ cat terraform.tfstate | grep module
    "module_output_seoul": {
    "module_output_singapore": {
      "module": "module.ec2_seoul",
      "module": "module.ec2_seoul",
      "module": "module.ec2_seoul",
            "module.ec2_seoul.aws_default_vpc.default",
            "module.ec2_seoul.data.aws_ami.default"
      "module": "module.ec2_singapore",
      "module": "module.ec2_singapore",
      "module": "module.ec2_singapore",
            "module.ec2_singapore.aws_default_vpc.default",
            "module.ec2_singapore.data.aws_ami.default"
            
# graph 확인
$ terraform graph > graph.dot

# aws cli로 ec2 확인
$ aws ec2 describe-instances --region ap-northeast-2 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text
my_ec2  13.124.41.57    running

$ aws ec2 describe-instances --region ap-southeast-1 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text
my_ec2  13.212.103.230  running

# 실습 완료 후 리소스 삭제
terraform destroy -auto-approve

  • 자식 모듈에서 필요로 하는 프로바이더의 버전이 루트 모듈의 정의와 다른 경우, 테라폼 구성에서 정의한 내용이 서로 호환되지 않아 오류가 발생할 수 있다.

 

- 모듈의 반복문

  • 모듈 또한 리소스에서 반복문을 사용하듯 구성할 수 있다.
  • 모듈이라는 리소스 정의 묶음을 원하는 수량으로 프로비저닝 할 수 있으므로 모듈 없이 구성하는 것과 대비해 리소스 종속성 관리와 유지 보수에 장점이 있다. count를 사용한 반복문 사용은 리소스에서의 사용 방식처럼 module 블록 내에 선언한다.
  • 디렉터리 생성 및 06-module-traning/module_loop_count/main.tf 파일 생성
mkdir -p 06-module-traning/module_loop_count/

- main.tf

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

module "ec2_seoul" {
  count  = 2
  source = "../modules/terraform-aws-ec2"
  instance_type = "t3.small"
}

output "module_output" {
  value  = module.ec2_seoul[*].private_ip   
}

- 실행 : 모듈의 반복문 테스트

$ terraform init
$ cat .terraform/modules/modules.json | jq
{
  "Modules": [
    {
      "Key": "",
      "Source": "",
      "Dir": "."
    },
    {
      "Key": "ec2_seoul",
      "Source": "../modules/terraform-aws-ec2",
      "Dir": "../modules/terraform-aws-ec2"
    }
  ]
}

$ terraform apply -auto-approve

$ terraform output
module_output = [
  "172.31.37.62",
  "172.31.34.126",
]

$ terraform state list
module.ec2_seoul[0].data.aws_ami.default
module.ec2_seoul[0].aws_default_vpc.default
module.ec2_seoul[0].aws_instance.default
module.ec2_seoul[1].data.aws_ami.default
module.ec2_seoul[1].aws_default_vpc.default
module.ec2_seoul[1].aws_instance.default

$ cat terraform.tfstate | grep module
    "module_output": {
      "module": "module.ec2_seoul[0]",
      "module": "module.ec2_seoul[0]",
      "module": "module.ec2_seoul[0]",
            "module.ec2_seoul.aws_default_vpc.default",
            "module.ec2_seoul.data.aws_ami.default"
      "module": "module.ec2_seoul[1]",
      "module": "module.ec2_seoul[1]",
      "module": "module.ec2_seoul[1]",
            "module.ec2_seoul.aws_default_vpc.default",
            "module.ec2_seoul.data.aws_ami.default"
            
# graph 확인
$ terraform graph > graph.dot

# aws cli로 ec2 확인
$ aws ec2 describe-instances --region ap-northeast-2 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text
my_ec2  3.34.3.112      running
my_ec2  15.164.220.184  running

# 실습 완료 후 리소스 삭제
$ terraform destroy -auto-approve

  • 모듈 묶음에 일관된 구성과 구조로 프로비저닝이 되는 경우라면 count가 간편한 방안이지만, 동일한 모듈 구성에 필요한 인수 값이 다르다면 for_each를 활용한다.

- 동일 모듈에 개발과 상용에 대한 입력 변수를 다르게 처리하는 방식

- 06-module-traning/module_loop_count/main.tf 파일 수정

locals {
  env = {
    dev = {
      type = "t3.micro"
      name = "dev_ec2"
    }
    prod = {
      type = "t3.medium"
      name = "prod_ec2"
    }
  }
}

module "ec2_seoul" {
  for_each = local.env
  source = "../modules/terraform-aws-ec2"
  instance_type = each.value.type
  instance_name = each.value.name
}

output "module_output" {
  value  = [
    for k in module.ec2_seoul: k.private_ip
  ]
}

- 실행 : 모듈의 반복문 테스트

$ terraform plan

$ terraform apply -auto-approve

$ terraform output
module_output = [
  "172.31.47.20",
  "172.31.41.205",
]

$ terraform state list
module.ec2_seoul["dev"].data.aws_ami.default
module.ec2_seoul["dev"].aws_default_vpc.default
module.ec2_seoul["dev"].aws_instance.default
module.ec2_seoul["prod"].data.aws_ami.default
module.ec2_seoul["prod"].aws_default_vpc.default
module.ec2_seoul["prod"].aws_instance.default

$ cat terraform.tfstate | grep module
    "module_output": {
      "module": "module.ec2_seoul[\"dev\"]",
      "module": "module.ec2_seoul[\"dev\"]",
      "module": "module.ec2_seoul[\"dev\"]",
            "module.ec2_seoul.aws_default_vpc.default",
            "module.ec2_seoul.data.aws_ami.default"
      "module": "module.ec2_seoul[\"prod\"]",
      "module": "module.ec2_seoul[\"prod\"]",
      "module": "module.ec2_seoul[\"prod\"]",
            "module.ec2_seoul.aws_default_vpc.default",
            "module.ec2_seoul.data.aws_ami.default"
            
# graph 확인
$ terraform graph > graph.dot

# aws cli로 ec2 확인
$ aws ec2 describe-instances --region ap-northeast-2 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text
prod_ec2        43.200.169.96   running
dev_ec2 43.201.154.220  running

# 실습 완료 후 리소스 삭제
$ terraform destroy -auto-approve

6.4 모듈 소스 관리

- 모듈 소스 관리

  • Module블록에 정의된 소스 구성으로 모듈의 코드 위치를 정의. terraform init 수행시 지정된 모듈을 다운로드해 사용한다 - 링크
    • 로컬 디렉터리 경로
    • 테라폼 레지스트리 - Terraform Registry
    • 깃허브 Github
    • 비트버킷 Bitbucket
    • 깃 Generic Git Repository
    • HTTP URLs
    • S3 Bucket
    • GCS Bucket google cloud storage
  • 테라폼 공식 문서 안내와 같이 루트 경로에 모듈을 배치하는 것 외에 패키지 하위 디렉터리 경로를 참조하는 것도 하나의 방안이다.

- 로컬 디렉토리 경로

  • 로컬 경로를 지정할 때는 테라폼 레지스트리와 구분하기 위해 하위 디렉터리는 ./로, 상위 디렉터리는 ../로 시작한다.
  • 대상 모듈은 이미 같은 로컬 파일 시스템에 존재하므로 다운로드 없이 바로 사용한다. 앞서 언급한 대로 재사용성이 고려된다면 상위 디렉터리에 별도 관리하는 것을 권장하고, 항상 루트 모듈과 함께 동작해야 하는 경우 하위 디렉터리에 모듈을 정의한다.
  • 코드 예시) 작업 환경을 기준으로 하는 로컬 경로 상위에 위치한 모듈 경로 선언
module "local_module" {
  source = "../modules/my_local_module"
}

- 테라폼 레지스트리

  • 테라폼 모듈 레지스트리는 테라폼의 프로토콜을 사용해 모듈을 사용하는 방식이다.
  • 공개된 테라폼 모듈을 사용하거나 Terraform Cloud, Terraform Enterprise에서 제공되는 비공개 테라폼 모듈을 사용할 때 설정하는 소스 지정 방식이다.
  • 공개된 모듈 - 링크 aws_vpc
  • 공개된 테라폼 모듈을 source에 선언할 때는 <네임스페이스>/<이름>/<프로바이더> 형태로 설정한다.
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.0"
}

- aws vpc 모듈 실습 - aws_vpc Github SimpleVPC

  • 모듈 다운로드
$ git clone https://github.com/terraform-aws-modules/terraform-aws-vpc/


$ tree terraform-aws-vpc/examples -L 1
terraform-aws-vpc/examples
├── complete
├── ipam
├── ipv6-dualstack
├── ipv6-only
├── issues
├── manage-default-vpc
├── network-acls
├── outpost
├── secondary-cidr-blocks
├── separate-route-tables
├── simple
└── vpc-flow-logs

$ tree terraform-aws-vpc/examples -L 2
terraform-aws-vpc/examples
├── complete
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   ├── variables.tf
│   └── versions.tf
├── ipam
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   ├── variables.tf
│   └── versions.tf
├── ipv6-dualstack
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   ├── variables.tf
│   └── versions.tf
├── ipv6-only
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   ├── variables.tf
│   └── versions.tf
├── issues
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   ├── variables.tf
│   └── versions.tf
├── manage-default-vpc
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   ├── variables.tf
│   └── versions.tf
├── network-acls
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   ├── variables.tf
│   └── versions.tf
├── outpost
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   ├── variables.tf
│   └── versions.tf
├── secondary-cidr-blocks
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   ├── variables.tf
│   └── versions.tf
├── separate-route-tables
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   ├── variables.tf
│   └── versions.tf
├── simple
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   ├── variables.tf
│   └── versions.tf
└── vpc-flow-logs
    ├── README.md
    ├── main.tf
    ├── outputs.tf
    ├── variables.tf
    └── versions.tf

13 directories, 60 files

$ cd terraform-aws-vpc/examples/simple
$ ls *.tf
main.tf         outputs.tf      variables.tf    versions.tf

$ cat main.tf
provider "aws" {
  region = local.region
}

data "aws_availability_zones" "available" {}

locals {
  name   = "ex-${basename(path.cwd)}"
  region = "eu-west-1"

  vpc_cidr = "10.0.0.0/16"
  azs      = slice(data.aws_availability_zones.available.names, 0, 3)

  tags = {
    Example    = local.name
    GithubRepo = "terraform-aws-vpc"
    GithubOrg  = "terraform-aws-modules"
  }
}

################################################################################
# VPC Module
################################################################################

module "vpc" {
  source = "../../"

  name = local.name
  cidr = local.vpc_cidr

  azs             = local.azs
  private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)]

  tags = local.tags
}

- 코드 수정 : 서울 리전 변경, main.tf 파일 내용 확인

# 서울 리전 변경
$ grep 'eu-west-1' main.tf
  region = "eu-west-1"

$ sed -i -e 's/eu-west-1/ap-northeast-2/g' main.tf

# VPC CIDR 변경
$ grep '10.0.0.0' main.tf
vpc_cidr = "10.0.0.0/16"

$ sed -i -e 's/10.0.0.0/10.10.0.0/g' main.tf

# main.tf 파일 내용 확인
cat main.tf
provider "aws" {
  region = local.region
}

data "aws_availability_zones" "available" {}

locals {
  name   = "ex-${basename(path.cwd)}"
  region = "ap-northeast-2"

  vpc_cidr = "10.10.0.0/16"
  azs      = slice(data.aws_availability_zones.available.names, 0, 3)

  tags = {
    Example    = local.name
    GithubRepo = "terraform-aws-vpc"
    GithubOrg  = "terraform-aws-modules"
  }
}

################################################################################
# VPC Module
################################################################################

module "vpc" {
  source = "../../"

  name = local.name
  cidr = local.vpc_cidr

  azs             = local.azs
  private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)]

  tags = local.tags
}

# 모듈 확인
$ tree ../../modules
../../modules
└── vpc-endpoints
    ├── README.md
    ├── main.tf
    ├── outputs.tf
    ├── variables.tf
    └── versions.tf

2 directories, 5 files

- 실행

$ terraform init
$ cat .terraform/modules/modules.json| jq
{
  "Modules": [
    {
      "Key": "",
      "Source": "",
      "Dir": "."
    },
    {
      "Key": "vpc",
      "Source": "../..",
      "Dir": "../.."
    }
  ]
}

# 생성된 리소스들 확인해보자!
$ terraform apply -auto-approve

$ terraform output
cgw_arns = []
cgw_ids = []
database_nat_gateway_route_ids = []
database_route_table_association_ids = []
database_route_table_ids = tolist([
  "rtb-0178779a1a3039ef8",
  "rtb-0dd3739a1f14ffdcb",
  "rtb-00c2a127768449f2d",
])
database_subnet_arns = []
database_subnets = []
database_subnets_cidr_blocks = tolist([])
database_subnets_ipv6_cidr_blocks = tolist([])
default_network_acl_id = "acl-0ae5977350bcc231c"
default_route_table_id = "rtb-0f761c02e5e9b32f1"
default_security_group_id = "sg-090f24d788b14d2dd"
elasticache_route_table_association_ids = []
elasticache_route_table_ids = [
  "rtb-0178779a1a3039ef8",
  "rtb-0dd3739a1f14ffdcb",
  "rtb-00c2a127768449f2d",
]
elasticache_subnet_arns = []
elasticache_subnets = []
elasticache_subnets_cidr_blocks = tolist([])
elasticache_subnets_ipv6_cidr_blocks = tolist([])
intra_route_table_association_ids = []
intra_route_table_ids = []
intra_subnet_arns = []
intra_subnets = []
intra_subnets_cidr_blocks = tolist([])
intra_subnets_ipv6_cidr_blocks = tolist([])
nat_ids = []
nat_public_ips = tolist([])
natgw_ids = []
outpost_subnet_arns = []
outpost_subnets = []
outpost_subnets_cidr_blocks = tolist([])
outpost_subnets_ipv6_cidr_blocks = tolist([])
private_ipv6_egress_route_ids = []
private_nat_gateway_route_ids = []
private_route_table_association_ids = [
  "rtbassoc-0a6df864b79f6acc5",
  "rtbassoc-0e5b915af7be6b33e",
  "rtbassoc-00a1e6ff1aadb7142",
]
private_route_table_ids = [
  "rtb-0178779a1a3039ef8",
  "rtb-0dd3739a1f14ffdcb",
  "rtb-00c2a127768449f2d",
]
private_subnet_arns = [
  "arn:aws:ec2:ap-northeast-2:************:subnet/subnet-02cae28db8b6c76db",
  "arn:aws:ec2:ap-northeast-2:************:subnet/subnet-0200653443d5c11a8",
  "arn:aws:ec2:ap-northeast-2:************:subnet/subnet-08c95b2bc070863b2",
]
private_subnets = [
  "subnet-02cae28db8b6c76db",
  "subnet-0200653443d5c11a8",
  "subnet-08c95b2bc070863b2",
]
private_subnets_cidr_blocks = tolist([
  "10.10.0.0/20",
  "10.10.16.0/20",
  "10.10.32.0/20",
])
private_subnets_ipv6_cidr_blocks = tolist([])
public_route_table_association_ids = []
public_route_table_ids = []
public_subnet_arns = []
public_subnets = []
public_subnets_cidr_blocks = tolist([])
public_subnets_ipv6_cidr_blocks = tolist([])
redshift_public_route_table_association_ids = []
redshift_route_table_association_ids = []
redshift_route_table_ids = tolist([
  "rtb-0178779a1a3039ef8",
  "rtb-0dd3739a1f14ffdcb",
  "rtb-00c2a127768449f2d",
])
redshift_subnet_arns = []
redshift_subnets = []
redshift_subnets_cidr_blocks = tolist([])
redshift_subnets_ipv6_cidr_blocks = tolist([])
this_customer_gateway = {}
vpc_arn = "arn:aws:ec2:ap-northeast-2:************:vpc/vpc-03ac6795fdf6c91dc"
vpc_cidr_block = "10.10.0.0/16"
vpc_enable_dns_hostnames = true
vpc_enable_dns_support = true
vpc_flow_log_cloudwatch_iam_role_arn = ""
vpc_flow_log_destination_arn = ""
vpc_flow_log_destination_type = "cloud-watch-logs"
vpc_id = "vpc-03ac6795fdf6c91dc"
vpc_instance_tenancy = "default"
vpc_ipv6_association_id = ""
vpc_ipv6_cidr_block = ""
vpc_main_route_table_id = "rtb-0f761c02e5e9b32f1"
vpc_owner_id = "************"
vpc_secondary_cidr_blocks = tolist([])

$ terraform state list
data.aws_availability_zones.available
module.vpc.aws_default_network_acl.this[0]
module.vpc.aws_default_route_table.default[0]
module.vpc.aws_default_security_group.this[0]
module.vpc.aws_route_table.private[0]
module.vpc.aws_route_table.private[1]
module.vpc.aws_route_table.private[2]
module.vpc.aws_route_table_association.private[0]
module.vpc.aws_route_table_association.private[1]
module.vpc.aws_route_table_association.private[2]
module.vpc.aws_subnet.private[0]
module.vpc.aws_subnet.private[1]
module.vpc.aws_subnet.private[2]
module.vpc.aws_vpc.this[0]

# 실습 완료 후 리소스 삭제
terraform destroy -auto-approve

- 깃허브

  • 깃의 원격 저장소로 널리 알려진 깃허브는 테라폼 구성에 대한 CI 용도로 사용할 수 있고, 저장된 구성을 테라폼 모듈의 소스로 선언할 수도 있다
  • 6.3에서 사용한 06-module-traning/modules/terraform-aws-ec2/ 를 깃허브에 업로드하는 과정
    1. 깃허브에 로그인
    2. 새로운 깃허브 저장소 생성 [New repository]
      • Owner : 원하는 소유자 선택
      • Repository name : 예시) terraform-module-repo
      • Public 선택
      • Add .gitignore의 드롭박스에서 [Terraform]을 선택
    3. 맨 아래 [Create repository] 클릭
    4. 해당 저장소에 예시) ‘terraform-aws-ec2’ 디렉터리 생성 후 main.tf , variable.tf, output.tf 추가 후 업로드

- main.tf

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

module "ec2_seoul" {
  source = "github.com/bringgreen/terraform-module-repo/terraform-aws-ec2"
  instance_type = "t3.small"
}

- 실행

$ cd module-source-mygithub
$ terraform init

# 아래 디렉터리에 깃허브에 있던 파일이 다운로드 되어 있음을 확인
$ tree .terraform/modules
.terraform/modules
├── ec2_seoul
│   └── terraform-aws-ec2
│       ├── main.tf
│       ├── output.tf
│       └── variable.tf
└── modules.json

# 배포
$ terraform apply -auto-approve
...
Plan: 2 to add, 0 to change, 0 to destroy.
module.ec2_seoul.aws_default_vpc.default: Creating...
module.ec2_seoul.aws_default_vpc.default: Creation complete after 2s [id=vpc-09f86f5b50035b365]
module.ec2_seoul.aws_instance.default: Creating...
module.ec2_seoul.aws_instance.default: Still creating... [10s elapsed]
module.ec2_seoul.aws_instance.default: Creation complete after 13s [id=i-0f09d4df8701c4621]


$ terraform state list
module.ec2_seoul.data.aws_ami.default
module.ec2_seoul.aws_default_vpc.default
module.ec2_seoul.aws_instance.default

# 실습 완료 후 삭제
terraform destroy -auto-approve

'Terraform > Study_과제' 카테고리의 다른 글

7주차 - 워크플로  (0) 2023.08.20
6주차 - 협업  (0) 2023.08.11
3주차 - Terraform 기본 사용 3/3  (0) 2023.07.23
2주차 - Terraform 기본 사용 2/3  (0) 2023.07.15
1주차 - Terraform 기본 사용 1/3  (0) 2023.07.09