개요
테라폼 구조를 어떻게 잡았는지 또 문제들을 어떻게 해결했는지 정리해보면 좋을 것 같아 글을 작성한다. 테라폼에 대한 이해도가 굉장히 적어서 공부를 더 꼼꼼히 다시 하고, 코드를 즉시 변경 해야겠다고 느꼈던 순간이다.
현재 테라폼 구조
현재 테라폼 구조는 아래와 같은 구조로 구성되어 있다.
$ tree .
.
|-- README.md
|-- environment
| |-- dev
| | |-- backend.tf
| | |-- dev.tfvars
| | |-- main.tf
| | |-- terraform.tfstate
| | `-- variables.tf
| `-- prod
| |-- backend.tf
| |-- main.tf
| |-- prod.tfvars
| `-- variables.tf
|-- modules
| |-- global
| | |-- acm
| | | |-- main.tf
| | | |-- outputs.tf
| | | `-- variables.tf
| | |-- route53
| | | |-- main.tf
| | | |-- outputs.tf
| | | `-- variables.tf
| | `-- s3
| | |-- main.tf
| | |-- outputs.tf
| | `-- variables.tf
| |-- networks
| | |-- internet_gateway
| | | |-- main.tf
| | | |-- outputs.tf
| | | `-- variables.tf
| | |-- nat_gateway
| | | |-- main.tf
| | | |-- outputs.tf
| | | `-- variables.tf
| | |-- route_table
| | | |-- main.tf
| | | |-- outputs.tf
| | | `-- variables.tf
| | |-- security_group
| | | |-- main.tf
| | | |-- outputs.tf
| | | `-- variables.tf
| | |-- subnet
| | | |-- main.tf
| | | |-- outputs.tf
| | | `-- variables.tf
| | |-- target_group
| | | |-- main.tf
| | | |-- outputs.tf
| | | `-- variables.tf
| | |-- vpc
| | | |-- main.tf
| | | |-- outputs.tf
| | | `-- variables.tf
| | `-- vpc_endpoint
| | |-- main.tf
| | |-- outputs.tf
| | `-- variables.tf
| |-- rds
| | |-- main.tf
| | |-- outputs.tf
| | `-- variables.tf
| `-- services
| |-- backend-app
| | |-- main.tf
| | |-- outputs.tf
| | `-- variables.tf
| `-- frontend-app
| |-- main.tf
| |-- outputs.tf
| `-- variables.tf
`-- provider.tf
일단 내 경우 dev, prod 로 환경을 이분화 하고자 했다. 공통적으로 해당하는 모듈의 경우 modules 디렉터리를 생성해서 세분화 해주었다. 모듈을 저렇게까지 쪼갠 이유는 아래와 같다.
- 나중에 공통 모듈을 수정해야할 경우가 있을 수 있다. 모듈에 있는 resource block을 수정할 때 혹여 다른 resource block을 건드리거나 영향을 끼칠 수 있는 가능성을 줄이기 위해서
- 세분화 할 시 network 로 뭉뚱그려서 모듈을 구성하는 것 보다 인프라를 파악하기 쉬울것이라 생각했다.
모듈 구조를 쪼개는 것에 대한 참고는 아래 링크를 통해서 했다.
- https://developer.hashicorp.com/terraform/language/modules/develop/structure
- https://www.terraform-best-practices.com/examples/terraform/medium-size-infrastructure
- https://www.reddit.com/r/Terraform/comments/tto5h3/terraform_folder_structure/
- https://blog.devops.dev/how-to-manage-multiple-environments-with-terraform-with-the-use-of-modules-d4ca512d7b4a
구조를 잡고 코드를 작성하면서 몇 가지 문제점이 발생했다.
- 처음 구조를 잡을 때 이해하기로는 각 모듈별로 output을 설정해주면 module.{모듈 이름}.output 이름으로 모듈 간 참조가 가능할 것이라고 생각을 했다. 근데 공통 모듈끼리는 참조가 안되는 것인지 A managed resource "aws_vpc" "vpc" has not been declared in module.subnet. 이런 오류가 발생하기 시작했다.
- 분명 GitHub Actions에 Plan을 했을 때에는 Familiar-terraform/environment/dev 에서 working dir 을 설정하고 진행하는데 main.tf 에 어떠한 변화를 주어도 아무런 변화가 없었음. 추측하기로는 working dir이 변경되지 않아서 루트 디렉토리에서 plan을 실행하고 있는게 아닌가 생각이 들었음
일단 위 문제들을 해결하기 위해서 각 테라폼 블록에 대해 개념 재정리를 했고, 발생하고 있는 오류들에 대해 차근차근 해결하고자 했다.
일단 terraform plan 명령어를 입력했을 시 발생하는 오류는 아래와 같았다.
$ tf plan -var-file="dev.tfvars"
╷
│ Error: Invalid reference
│
│ on ..\..\modules\networks\internet_gateway\main.tf line 4, in resource "aws_internet_gateway" "igw":
│ 4: Name = "${prefix}-igw"
│
│ A reference to a resource type must be followed by at least one attribute access, specifying the resource name.
╵
╷
│ Error: Invalid function argument
│
│ on ..\..\modules\networks\subnet\main.tf line 2, in resource "aws_subnet" "public_subnet":
│ 2: for_each = toset(var.pub_subnet_cidr_and_az)
│ ├────────────────
│ │ while calling toset(v)
│ │ var.pub_subnet_cidr_and_az is a map of string
│
│ Invalid value for "v" parameter: cannot convert map of string to set of any single type.
╵
╷
│ Error: Reference to undeclared input variable
│
│ on ..\..\modules\networks\subnet\main.tf line 4, in resource "aws_subnet" "public_subnet":
│ 4: vpc_id = var.vpc.id
│
│ An input variable with the name "vpc" has not been declared. This variable can be declared with a variable "vpc" {} block.
╵
╷
│ Error: Invalid reference
│
│ on ..\..\modules\networks\subnet\main.tf line 9, in resource "aws_subnet" "public_subnet":
│ 9: Name = "${prefix}-public-subnet"
│
│ A reference to a resource type must be followed by at least one attribute access, specifying the resource name.
╵
╷
│ Error: Invalid function argument
│
│ on ..\..\modules\networks\subnet\main.tf line 14, in resource "aws_subnet" "private_subnet":
│ 14: for_each = toset(var.pri_subnet_cidr_and_az)
│ ├────────────────
│ │ while calling toset(v)
│ │ var.pri_subnet_cidr_and_az is a map of string
│
│ Invalid value for "v" parameter: cannot convert map of string to set of any single type.
╵
╷
│ Error: Reference to undeclared input variable
│
│ on ..\..\modules\networks\subnet\main.tf line 16, in resource "aws_subnet" "private_subnet":
│ 16: vpc_id = var.vpc.id
│
│ An input variable with the name "vpc" has not been declared. This variable can be declared with a variable "vpc" {} block.
╵
╷
│ Error: Invalid reference
│
│ on ..\..\modules\networks\subnet\main.tf line 20, in resource "aws_subnet" "private_subnet":
│ 20: Name = "${prefix}-private-subnet"
│
│ A reference to a resource type must be followed by at least one attribute access, specifying the resource name.
╵
╷
│ Error: Invalid function argument
│
│ on ..\..\modules\networks\subnet\main.tf line 25, in resource "aws_subnet" "private_db_subnet":
│ 25: for_each = toset(var.pri_db_subnet_cidr_and_az)
│ ├────────────────
│ │ while calling toset(v)
│ │ var.pri_db_subnet_cidr_and_az is a map of string
│
│ Invalid value for "v" parameter: cannot convert map of string to set of any single type.
╵
╷
│ Error: Reference to undeclared input variable
│
│ on ..\..\modules\networks\subnet\main.tf line 27, in resource "aws_subnet" "private_db_subnet":
│ 27: vpc_id = var.vpc.id
│
│ An input variable with the name "vpc" has not been declared. This variable can be declared with a variable "vpc" {} block.
╵
╷
│ Error: Invalid reference
│
│ on ..\..\modules\networks\subnet\main.tf line 31, in resource "aws_subnet" "private_db_subnet":
│ 31: Name = "${prefix}-private-db-subnet"
│
│ A reference to a resource type must be followed by at least one attribute access, specifying the resource name.
Error가 싫어요...ㅠㅠ
일단 ${prefix} 같은 경우 단순한 문제이기 때문에 빠르게 해결했다. ${var.prefix} 로 변경해주었다.
그리고 var.vpc.id 같은 경우에도 var.vpc_id로 변경해주었다.
for_each Error의 경우에도 이미 variable 자체가 map이라서 set으로 변경하지 않아도 된다.
The for_each meta-argument accepts a map or a set of strings, and creates an instance for each item in that map or set. Each instance has a distinct infrastructure object associated with it, and each is separately created, updated, or destroyed when the configuration is applied.
for_each 메타 인수는 맵 또는 문자열 집합을 받아들이고 해당 맵 또는 집합의 각 항목에 대한 인스턴스를 만듭니다. 각 인스턴스에는 연결된 고유한 인프라 개체가 있으며, 구성이 적용될 때 각각 개별적으로 생성, 업데이트 또는 소멸됩니다.
https://developer.hashicorp.com/terraform/language/meta-arguments/for_each#basic-syntax
위 에러들을 전부 해결하고 다시 terraform plan 명령어를 실행시켰다.
$ tf plan -var-file="dev.tfvars"
Planning failed. Terraform encountered an error while generating this plan.
╷
│ Error: Retrieving AWS account details: validating provider credentials: retrieving caller identity from STS: operation error STS: GetCallerIdentity, https response error StatusCode: 403, RequestID: 1165667a-25f8-4cbe-b5bb-fff2d2430ae7, api error InvalidClientTokenId: The security token included in the request is invalid.
│
│ with provider["registry.terraform.io/hashicorp/aws"],
│ on main.tf line 1, in provider "aws":
│ 1: provider "aws" {
또 에러 ㅠㅠ
흠 내가 봤을 때 이제 GitHub Actions로 넘어가도 될것 같다. 현재 코드가 local backend를 바라보게 되어있는데, AWS Account에 대해 Credential이 없다는 것이기 때문에 Git 에서 직접 Plan 돌려보면 될것 같다.
또 동일하게 No changes가 출력된다.
아래와 같이 추측해보았다.
- Step 별로 다른 work dir 을 가지고 있거나
- working_directory 설정이 안먹고 있거나
- Init 과정에서 workdir이 변경되었거나
기타 여러 가지 이유가 있을 수 있다고 추측했고, 하나하나 확인 해보았다.
- Step 별로 다른 work dir 을 가지고 있거나
이 경우에는 step 별로 다른 파일, 종속성 등에 의해서 문제가 발생할 것이다.
A job is a set of steps in a workflow that is executed on the same runner. Each step is either a shell script that will be executed, or an action that will be run. Steps are executed in order and are dependent on each other. Since each step is executed on the same runner, you can share data from one step to another. For example, you can have a step that builds your application followed by a step that tests the application that was built. You can configure a job's dependencies with other jobs; by default, jobs have no dependencies and run in parallel with each other. When a job takes a dependency on another job, it will wait for the dependent job to complete before it can run. For example, you may have multiple build jobs for different architectures that have no dependencies, and a packaging job that is dependent on those jobs. The build jobs will run in parallel, and when they have all completed successfully, the packaging job will run.
작업은 동일한 러너에서 실행되는 워크플로우의 단계 집합입니다. 각 단계는 실행될 셸 스크립트이거나 실행될 작업입니다. 단계는 순서대로 실행되며 서로 종속됩니다. 각 단계는 동일한 러너에서 실행되므로 한 단계에서 다른 단계로 데이터를 공유할 수 있습니다. 예를 들어 애플리케이션을 빌드하는 단계 다음에 빌드된 애플리케이션을 테스트하는 단계를 가질 수 있습니다. 작업의 종속성을 다른 작업과 구성할 수 있으며, 기본적으로 작업은 종속성이 없으며 서로 병렬로 실행됩니다. 한 작업이 다른 작업에 종속된 경우 종속된 작업이 완료될 때까지 기다렸다가 실행할 수 있습니다. 예를 들어 종속성이 없는 여러 아키텍처에 대한 빌드 작업과 이러한 작업에 종속된 패키징 작업이 여러 개 있을 수 있습니다. 빌드 작업은 병렬로 실행되며, 모두 성공적으로 완료되면 패키징 작업이 실행됩니다.
https://docs.github.com/ko/actions/about-github-actions/understanding-github-actions#jobs
위 글에서 알수 있듯이 Step은 동일한 러너에서 실행되므로 한 단계에서 다른 단계로 데이터를 공유한다고 되어있다. 하나의 job을 벗어나지 않는 이상 단계 별로 명령을 실행하기 때문에 처음 생각했던 것과는 다르게 (물론 처음에 jobs에 대한 이해를 했을때도 Step이 나눠져있는 건 아닌걸로 이해했음) Step 별로 환경이 나누어져 있진 않다. 결국 이 문제는 명확하게 아닌 것이다.
- working_directory 설정이 안먹고 있거나
조금 이상한건 다른 어떤 글들을 봐도 working_directory를 사용하지 않았다는 것이다.
물론 참고했던 소스 하나하나 다 확인한게 아니라 main.tf 파일이 루트 모듈에 위치해 있을 수도 있다는 생각은 든다.
위 문제 같은 경우 workflow 소스에서 각 Step에 하나하나 working_directory 설정을 진행해보면 된다.
정상적으로 완료가 되었고, run pwd로 출력한 path도 39 line을 보면 정상적으로 environment/dev로 출력되고 있는 모습을 볼 수 있었다.
그랬더니 웬걸 정상적으로 plan이 작동했다. 아무래도 working_directory 문제가 맞았다.
결론
- 결국 output은 공통 모듈 말고 메인 모듈에서 프로비저닝 할 때 module 리소스 블록에서 module.{module name}.{output name} 으로 사용해야한다.
- Jenkins 사용시에도 겪었지만 GitHub Actions를 사용할 때는 항상 working_directory에 유의하며 사용해야 한다. 잘 모르겠으면 pwd와 ls를 통해서 실제 리소스가 생성되는지 등을 확인하면 될것
[참조] :
https://docs.github.com/ko/actions/about-github-actions/understanding-github-actions
https://chloemcateer.medium.com/deploying-terraform-using-github-actions-20937d68160f
'기타 > 토이프로젝트' 카테고리의 다른 글
[Familiar] Terraform output for 반복문 사용하기 (0) | 2024.08.20 |
---|---|
[Familiar] Terraform plan & apply with GitHub Actions (0) | 2024.08.06 |
[Familiar] 테라폼 사용을 위한 Github Actions OIDC 설정 (0) | 2024.08.02 |
[Familiar] 토이 프로젝트 개요 (0) | 2024.08.01 |