IaC(Infrastructure as Code)

Terraform State

cwchoiit 2024. 3. 7. 15:14
728x90
반응형
SMALL
728x90
SMALL

State

이번엔 상태 관리에 대한 내용이다. Terraform을 이용해서 어떤 작업을 하면 현재 작업한 상태에 대한 파일이 기록되는 것을 terraform.state 파일을 통해 알았다. 이 파일을 보면 지금 이 Terraform이 관리하고 있는 상태를 보여준다. 굉장히 중요한 파일이다. 이 파일을 기점으로 Apply를 했을 때 변경점을 캐치하거나, 관리하고 있는 리소스를 파악하거나 할 수 있고 Destroy 명령어를 했을 때 무엇을 삭제할지 파악할 수 있기 때문이다. 

 

terraform.state

{
  "version": 4,
  "terraform_version": "1.7.4",
  "serial": 3,
  "lineage": "6c566e9f-a596-337a-6760-5e67c5348e36",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_instance",
      "name": "tfec2",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "ami": "ami-097bf0ec147165215",
            "arn": "arn:aws:ec2:ap-northeast-2:135149110460:instance/i-04d23764108510fc6",
            "associate_public_ip_address": false,
            "availability_zone": "ap-northeast-2a",
            "capacity_reservation_specification": [
              {
                "capacity_reservation_preference": "open",
                "capacity_reservation_target": []
              }
            ],
            "cpu_core_count": 1,
            "cpu_options": [
              {
                "amd_sev_snp": "",
                "core_count": 1,
                "threads_per_core": 1
              }
            ],
            "cpu_threads_per_core": 1,
            "credit_specification": [
              {
                "cpu_credits": "standard"
              }
            ],
            "disable_api_stop": false,
            "disable_api_termination": false,
            "ebs_block_device": [],
            "ebs_optimized": false,
            "enclave_options": [
              {
                "enabled": false
              }
            ],
            "ephemeral_block_device": [],
            "get_password_data": false,
            "hibernation": false,
            "host_id": "",
            "host_resource_group_arn": null,
            "iam_instance_profile": "",
            "id": "i-04d23764108510fc6",
            "instance_initiated_shutdown_behavior": "stop",
            "instance_lifecycle": "",
            "instance_market_options": [],
            "instance_state": "running",
            "instance_type": "t2.micro",
            "ipv6_address_count": 0,
            "ipv6_addresses": [],
            "key_name": "",
            "launch_template": [],
            "maintenance_options": [
              {
                "auto_recovery": "default"
              }
            ],
            "metadata_options": [
              {
                "http_endpoint": "enabled",
                "http_protocol_ipv6": "disabled",
                "http_put_response_hop_limit": 2,
                "http_tokens": "required",
                "instance_metadata_tags": "disabled"
              }
            ],
            "monitoring": false,
            "network_interface": [],
            "outpost_arn": "",
            "password_data": "",
            "placement_group": "",
            "placement_partition_number": 0,
            "primary_network_interface_id": "eni-06fd68c8da4d961c6",
            "private_dns": "ip-10-1-254-36.ap-northeast-2.compute.internal",
            "private_dns_name_options": [
              {
                "enable_resource_name_dns_a_record": false,
                "enable_resource_name_dns_aaaa_record": false,
                "hostname_type": "ip-name"
              }
            ],
            "private_ip": "10.1.254.36",
            "public_dns": "",
            "public_ip": "",
            "root_block_device": [
              {
                "delete_on_termination": true,
                "device_name": "/dev/xvda",
                "encrypted": false,
                "iops": 3000,
                "kms_key_id": "",
                "tags": {},
                "tags_all": {},
                "throughput": 125,
                "volume_id": "vol-0d2e2fa43f0cb3483",
                "volume_size": 8,
                "volume_type": "gp3"
              }
            ],
            "secondary_private_ips": [],
            "security_groups": [],
            "source_dest_check": true,
            "spot_instance_request_id": "",
            "subnet_id": "subnet-0b23fd05b5919269e",
            "tags": {
              "Name": "terraform-ec2"
            },
            "tags_all": {
              "Name": "terraform-ec2"
            },
            "tenancy": "default",
            "timeouts": null,
            "user_data": null,
            "user_data_base64": null,
            "user_data_replace_on_change": false,
            "volume_tags": null,
            "vpc_security_group_ids": [
              "sg-0ac56a28dc1c79f74"
            ]
          },
          "sensitive_attributes": [],
          "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMSJ9"
        }
      ]
    }
  ],
  "check_results": null
}

 

위 코드를 보면 알겠지만 현재 상태에서 관리하고 있는 Resources가 무엇인지 기록해 둔다. 그렇기 때문에 현재 상태에서 뭔가 달라지면 그걸 파악할 수 있는거고 삭제하는것도 어떤것을 삭제하는지 알 수 있는 것이다. 

 

근데 이 State를 관리하는 방법이 크게 Local / Remote 방식이 있다. 지금처럼 작업한 폴더 경로 내에 있는 terraform.state 파일로 관리하는 경우를 Local State 라고 하고 이런 방식을 Local Backend 라고 한다.

 

Backend

Backend는 크게 Local, Remote로 분리가 되고 Local은 바로 위에 설명한 내용 그대로이며 Remote는 말 그대로 원격으로 관리한다는 의미이다. 그리고 이 방법의 대표적인 예가 AWS S3이다. 그냥 말 그대로 S3 Bucket에 terraform.state 파일을 관리하는 것이다. 

그리고 또 다른 대표적인 예는 비교적 최신에 나온 Terraform Cloud이다. 이 두가지 모두 한번 사용해보자.

 

Remote Backend는 중요하게 여겨야 할 게 Lock이 되는지 아닌지가 상당히 중요하다. 혼자서 작업하는 경우 Lock이 상관이 없지만 협업하는 경우 나와 다른 누군가가 동시에 작업을 하는 경우 상태가 꼬여버릴 수 있다. 그래서 작업을 할 땐 Lock을 걸어서 다른 사람은 변경을 하지 못하게 막는것이 중요하다.

 

Remote Backend AWS S3 

S3로 상태 관리를 하려면 당연히 버킷이 일단 있어야 한다. 버킷을 만들자.

 

그리고 이 버킷을 사용하기 위해 다음과 같이 main.tf 파일에 이 내용을 작성한다.

terraform {
  backend "s3" {
    bucket = "cwchoiit-terraform-state-s3"
    key = "s3-backend/terraform.tfstate"
    region = "ap-northeast-2"
  }
}

그냥 봐도 다 이해할 수 있다. S3를 Backend로 사용하겠다는 의미이다.

  • bucket: bucket 이름
  • key: bucket 내 저장될 경로
  • region: bucket의 지역

이렇게 backend를 수정했으면 Init을 다시 해줘야 한다. 그래서 Init을 해보자.

 

성공적으로 backend가 s3에 구성됐다고 나온다. 그럼 이제 Apply를 해보자.

Apply가 진행이 되면 이렇게 버킷에 terraform.state 파일이 지정한 경로에 맞게 만들어진다.

 

 

Remote Backend Terraform Cloud

우선, Terraform Cloud를 사용하기 위해선 계정이 필요하다. 다음 링크에서 계정을 생성하자.

 

https://app.terraform.io/session

 

app.terraform.io

계정 만들면 Organization을 만들면 된다. 그리고 main.tf 파일에 다음과 같은 코드를 추가한다.

terraform {
  backend "remote" {
    hostname = "app.terraform.io"
    organization = "cwchoiit-terraform"

    workspaces {
      name = "cwchoiit-terraform-cloud-backend"
    }
  }
}
  • backend는 "remote"
  • hostname은 Terraform Cloud의 hostname
  • organization은 방금 만든 organization
  • workspaces는 원하는 Name

하고 처음으로 terraform init을 실행하면 다음과 같은 에러를 마주하게 된다. 이는 로그인을 하라는 것.

 

로그인을 하면 Terraform Cloud 창이 띄워지고 토큰을 발급하게 된다. 토큰을 발급하고나서 입력해달라는 프롬프트에 입력을 해준다.

 

잘 입력을 하면 다음과 같이 로그인이 잘 된다.

 

이제 다시 Init을 실행한다. 그럼 정상적으로 진행이 되고 Terraform Cloud로 가면 다음과 같이 내가 작성한 Workspace의 이름으로 Workspace가 만들어진다.

그리고 Workspace 설정에 가서 다음과 같은 작업을 해주자.

이 설정은 추후에 좀 더 깊이 알아보자. 지금은 일단 Local로 설정을 하자.

 

 

위에서 만든 토큰을 로컬에 저장하는 방법이 있는데 다음 경로로 Vim을 사용해 들어가보자.

vi ~/.terraformrc

 

그럼 그 곳에 이 코드를 추가한다. 그리고 본인의 토큰을 저 곳에 넣어주면 된다.

credentials "app.terraform.io" {
	token = "yourtoken"
}

 

그럼 이제 Init을 했으니 Apply를 해보자. 아래처럼 Apply가 잘 됐다.

 

State도 정상적으로 만들어졌다.

 

여기까지 하고 나면 State 관리 파일을 Local, Remote 각각에 저장하는 방법을 배운 것이다. 이제 한발 더 나아가서 CLI 명령어 중 'state'라는 명령어가 있다. 이 녀석에 대해서 좀 더 깊게 알아보자.

 

Command 'state'

다음 명령어를 입력해보자.

tf state

 

그럼 다음과 같이 여러 Subcommands를 확인할 수 있다.

하나씩 알아보자. 그 중에서도 꼭 이해하고 있어야 하는것들은 list, mv, rm 정도이다.

state list

이는 현재 관리하는 상태(리소스나 그 외 정보들)를 나열해주는 명령어이다. 위에서 작업한 main.tf 파일에 대한 state list 명령어를 실행해보자. 다음과 같이 현재 관리중인 상태들을 보여준다.

 

state show

이는 상태 내 특정 리소스에 대한 자세한 내용을 보여주는 명령어이다. 역시 명령어를 실행해보자.

 

state mv

이 명령어는 상태에 어떤 변경을 가할 때 사용된다. 우리가 작성했던 main.tf 파일에 약간의 수정을 가해보자.

아래와 같이 resource 두 개를 각각으로 분리해서 작성했던 것을 바꿔보자

 

Old

resource "aws_iam_group" "developer" {
  name = "developer"
}

resource "aws_iam_group" "employee" {
  name = "employee"
}

output "groups" {
  value = [
    aws_iam_group.developer,
    aws_iam_group.employee
  ]
}

 

New

resource "aws_iam_group" "this" {
    for_each = toset(["developer", "employee"])

    name = each.key
  
}

output "groups" {
  value = aws_iam_group.this
}

 

이렇게 두 개의 리소스를 for-each를 사용해서 하나로 만들었다. 이 상태에서 Apply를 실행해보자.

그럼 작성자 입장에서는 동일한 그룹을 만든다고 생각하겠지만, 테라폼 입장에서는 리소스 이름으로 리소스를 관리하기 때문에 이름이 바뀐 상태에서 Apply를 하면 기존에 리소스를 삭제하고 새로운 리소스를 만들어내려고 한다. 다음이 그 결과다.

 

이런 경우 실제 서비스에 어떤 문제를 야기할지 알 수 없는 미지의 세계가 열리게 된다. 이렇게 하면 안된다. 그래서 이 경우 mv를 사용한다.

이 경우 일단 기존 상태를 확인해보자.

 

기존 상태를 확인 후 다음과 같이 변경하자.

tf state mv 'aws_iam_group.developer' 'aws_iam_group.this["developer"]'
tf state mv 'aws_iam_group.employee' 'aws_iam_group.this["employee"]'

 

이렇게 두 개를 잘 변경해 주면 상태를 건강하게 변경할 수 있다. Apply를 해도 뭔가를 지운다거나 다시 만든다거나 하지 않는다.

Outputs 만 변경될 뿐이다.

 

state rm

이 명령어는 리소스를 유지는하되, 테라폼으로 더이상 관리는 하지 않는 경우에 사용하기 적합하다.

예를 들어 다음과 같은 코드가 있다고 가정하자.

terraform {
  backend "remote" {
    hostname = "app.terraform.io"
    organization = "cwchoiit-terraform"

    workspaces {
      name = "cwchoiit-terraform-cloud-backend"
    }
  }
}

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

resource "aws_iam_group" "this" {
    for_each = toset(["developer", "employee"])

    name = each.key
  
}

output "groups" {
  value = aws_iam_group.this
}

variable "users" {
    type = list(any)
}

resource "aws_iam_user" "this" {
  for_each = {
    for user in var.users : user.name => user
  }

  name = each.key

  tags = {
    level = each.value.level
    role = each.value.role
  }
}

resource "aws_iam_user_group_membership" "this" {
  for_each = {
    for user in var.users : user.name => user
  }

  user = each.key

  groups = each.value.is_developer ? [
    aws_iam_group.this["developer"].name, aws_iam_group.this["employee"].name
    ] : [
        aws_iam_group.this["employee"].name
    ]

  depends_on = [ aws_iam_user.this ] # 이 리소스가 생성되어야만 가능하게 설정
}

locals {
  developers = [
    for user in var.users : user if user.is_developer
  ]
}

resource "aws_iam_user_policy_attachment" "developer" {
  for_each = {
    for user in local.developers : user.name => user
  }

  user = each.key
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"

  depends_on = [ aws_iam_user.this ] # 이 리소스가 생성되어야만 가능하게 설정
}

output "developers" {
  value = local.developers
}

output "high_level_users" {
  value = [
    for user in var.users : user if user.level > 5
  ]
}

 

이 때 테라폼으로 더 이상 Policy 관련 리소스를 다루지 않고 싶어져서 아래 코드를 지웠다고 해보자.

resource "aws_iam_user_policy_attachment" "developer" {
  for_each = {
    for user in local.developers : user.name => user
  }

  user = each.key
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"

  depends_on = [ aws_iam_user.this ] # 이 리소스가 생성되어야만 가능하게 설정
}

 

지운 상태에서 Apply를 하면 다음과 같이 해당 리소스를 제거한다는 내용을 알려준다.

 

그러나, 내가 원하는건 이 정책 자체를 해당 유저에게서 삭제하는게 아니고 그저 테라폼으로 관리하는 것을 그만하려고 하는건데 이렇게 삭제를 해버리면 역시나 어떤 파장을 일으킬지 미지의 세계가 펼쳐질것이다. 이럴때 "state rm"을 사용한다.

 

state rm 명령어를 사용하기 전 먼저 해당 리소스에 대해서 파악을 하기 위해 tf state list 명령어를 실행해보자.

내가 원하는 건 맨 하단 두개를 날리면 된다.

tf state rm 'aws_iam_user_policy_attachment.developer["alice"]'

tf state rm 'aws_iam_user_policy_attachment.developer["tony"]'

 

이렇게 리소스 관리 대상에서 제거를 했다. 이제 Apply를 해도 해당 리소스를 관리하지 않기 때문에 더 이상 고려대상이 아니다. 그래서 지우지도 않는다. 

 

state pull / push

Git을 사용해봤다면 동일한 느낌으로 생각하면 될 것 같다. Pull은 Remote State 저장소에서 Local State 저장소로 State를 땡겨오는 거고 Push는 그 반대다. Push는 지금은 그냥 이런것이다 하고 넘어가자. 꽤나 위험한 행위이기 때문에 굳이 지금 다룰 필요가 없다.

 

하단 명령어를 실행해보자.

tf state pull

 

그럼 Remote State 저장소에서 관리하고 있는 상태들을 표준 출력으로 출력해준다.

 

그래서 이 Remote State 저장소에서 관리하고 있는 상태를 로컬로 내려받아 사용하기 위해 .tfstate 파일에 넣어버리는 것.

tf state pull > xxxx.tfstate

 

그래서 저 파일을 Local State 파일로 사용하면 된다.

728x90
반응형
LIST

'IaC(Infrastructure as Code)' 카테고리의 다른 글

Terraform Workspace  (0) 2024.03.08
Terraform Commands (taint / untaint)  (0) 2024.03.08
AWS + Terraform (Loop)  (0) 2024.03.07
AWS + Terraform (Conditions)  (0) 2024.03.07
AWS + Terraform (For-Each)  (0) 2024.03.06