Terraform을 공부한 상태다보니 이해가 안가는 부분은 없다. 여기서 subnet_id를 명시한 이유는 나 같은 경우 Default VPC가 없기 때문에 반드시 Subnet을 명시해줘야 한다. 그리고 associate_public_ip_address도 true로 설정하지 않으면 AMI를 만들기 위해 새로 생성되는 EC2의 Public IP가 존재하지 않기 때문에 SSH 접속을 Packer가 할 수 없다. 그래서 'true'로 설정해줘야 한다.
source_ami_filter는 기반이 되는 AMI를 지정하는 부분이다. ubuntu 이미지 가장 최신 버전을 사용하기로 한다.
마지막 부분인 build 부분이다. 이 부분은 빌드의 이름과 어떤 것을 빌드할 것인지에 대한 정보를 기입한다.
우선 init 명령어를 통해 필요한 plugin을 내려받아야 한다. 이 명령어가 잘 수행되면 다음 명령어를 입력한다.
packer build .
이제 빌드를 시작한다. 빌드를 시작하면 다음과 같은 화면이 보일것이다.
과정을 보면 알겠지만 일시적으로 SSH에 접속하기 위해 Key pair와 Security group을 생성하는것이 보인다. 이 부분도 명시해서 가져다가 사용하게 할 수 있지만 이 예제에선 Packer가 직접 만들도록 했다. 그리고 인스턴스가 생성된 후 SSH로 인스턴스에 접속한다. 그 이유는 AMI는 인스턴스에서 필요한 모든 패키지나 기본 설정을 다 한 상태에서의 이미지이기 때문에 그런것들이 있다면 해당 인스턴스에서 설치 작업을 수행해야 하기 때문에 SSH에 접속하는 것이다. 그러나 이 예제에는 따로 설치할 것은 없다. 내가 정의하지 않았으니까. 이렇게 인스턴스가 만들어지고 AMI가 만들어지면 일시적으로 만든 Security group, Key pair, Instance가 삭제된다.
module "security_group" {
source = "tedilabs/network/aws//modules/security-group"
version = "0.24.0"
name = "${local.vpc_name}-provisioner-userdata"
description = "Security Group for SSH."
vpc_id = "vpc-04a888e12b489eb12"
ingress_rules = [
{
id = "ssh"
protocol = "tcp"
from_port = 22
to_port = 22
cidr_blocks = ["0.0.0.0/0"]
description = "Allow SSH from anywhere."
},
{
id = "http"
protocol = "tcp"
from_port = 80
to_port = 80
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTP from anywhere."
}
]
egress_rules = [
{
id = "all/all"
description = "Allow to communicate to the Internet."
protocol = "-1"
from_port = 0
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
]
tags = local.common_tags
}
이제 Userdata를 이용해서 AWS EC2 인스턴스를 만드는 부분이다. 기본적으로 ami, instance_type, subnet_id, associate_public_ip_address는 할당해주었다. 여기서 새롭게 보이는 key_name은 EC2에 SSH로 접속할 때 사용했던 Key pair의 Name이다. 그리고 이제 핵심 부분인 user_data가 있다. 보면 EOT로 시작해서 EOT로 끝나는데 그냥 Multiple 문자열을 받기 위해 저렇게 사용한다. 그리고 첫 부팅시에만 적용될 sudo apt-get update와 nginx를 설치하는 작업을 한다. 그 다음 위에서 설명한 security_group을 지정했다.
이제 이 부분이 핵심이다. null_resource는 이 역시 Terraform에서 지원하는 Provider 중 하나인데 이 녀석은 딱 한가지 Attribute를 가지고 있다. triggers 라는 Attribute인데 이 triggers는 그 안에 지정한 것들 중 하나라도 변경 사항이 생기면 이 null_resource라는 전체 리소스를 재실행한다. 즉 리소스를 다시 만들어낸다. 그리고 이것을 사용해서 실제 provisioner를 사용했다.
triggers에 3개가 있다. instance_id, script, index_file. 우선 instance_id는 위에서 만든 provisioner라는 이름의 리소스를 가진 리소스의 ID가 변경되거나, script, index_file은 files 폴더에 있는 파일의 해시값이 변경된다(파일 내용이 변경됨)면 이 리소스가 재실행된다는 의미이다.
여기에 local-exec는 로컬 PC에서 명령어를 수행하는 부분이다. 의미없지만 잘 수행되는지 Hello World를 echo로 실행한다.
file은 로컬에서 리모트로 파일을 전송한다. 그래서 source랑 destination 속성이 있다. 저 부분은 뭐가 뭔지 한 눈에 알 수 있을것이고 connection 부분이 중요하다. 이 부분이 어떻게 전송할지에 대한 내용이기 때문이다. 우선 SSH로 보내고 유저 이름은 "ubuntu"이다. Host는 위에서 provisioner라는 리소스 이름의 인스턴스 Public IP가 된다. 그리고 private_key가 이제 EC2에 SSH로 접속할 때 사용하는 .pem 파일이다. 이 .pem 파일은 AWS Console에서 EC2 인스턴스 만들 때 Key pair로 사용하는 그 .pem 파일이다.
그리고 remote-exec는 리모트 머신에서 명령어를 수행한다. 그 수행하는 명령어가 바로 script = "실행할 스크립트 파일의 경로" 또는 inline = ["명령어", "명령어"]이다. 이 명령어를 작성하는 방법은 세가지가 있다. script, scripts, inline. script와 inline은 보았고, scripts는 스크립트 파일 한개가 아니라 여러개를 실행할 때 사용한다.
그래서 이 main.tf에 대해 Apply를 하면 기존에 있는 VPC를 사용해서(저 위에 vpc-id가 기존에 있는 내 VPC ID를 붙인 것) 해당 VPC의 Subnet역시 기존에 있는 Subnet에 EC2 두개가 만들어진다. (Userdata로, Provisioner로) 그리고 각 EC2는 Nginx를 설치하는데 Userdata는 설치하는 것까지가 끝이다. 그리고 그것은 딱 한번 최초 부팅시에만 실행될 것이다. 그게 Userdata의 특징이니까. 그러나 provisioner로 만든 EC2는 null_resource를 사용해서 감시하는 3개의 트리거가 발동될 때마다 새롭게 null_resource에 작성한 작업들을 수행하게 된다. 왜냐하면 null_resource에서 작업하기로 하는 HOST가 전부 provisioner로 만든 EC2의 Public IP를 가르키기 때문.
그래서 이 것을 Apply해보자. 다음과 같이 정상적으로 Apply가 됐고 EC2 인스턴스 두개가 생겼다.
AWS Console에 들어가보면 만든 EC2 두개가 보인다.
그리고 각 EC2의 Security도 잘 적용된 상태다.
이 상태에서 저 Outputs에서 알려준 Public IP를 접근해보자. Nginx를 두 EC2 모두 설치했기 때문에 80으로 접속 시 Nginx 화면이 보여야한다.
Userdata로 만든 EC2
Provisioner로 만든 EC2
Provisioner로 만든 EC2는 왜 다를까? null_resource에서 index.html 파일을 Nginx가 기본으로 뿌려주는 경로에 위치시켰기 때문에 내가 만든 이 index.html 파일이 보이는 것이다.
그럼 여기서 한 발 더 나아가서 이 파일(index.html)이 수정되면? => 트리거가 발동 => provisioner로 만든 EC2가 Replace 될 것
files/index.html
다음과 같이 수정해보자.
<h1>Hello Chyoni!</h1>
그 후 Apply 진행. 변경된다는 Plan이 보인다.
작업이 진행되고 다시 provisioner로 띄운 EC2로 들어가보면 다음과 같이 변경 사항이 적용됐음을 확인 가능하다.
예전에는 Terraform Registry에 Provider에 Terraform이 있었는데 지금은 위 문서로 대체된듯하다. 여튼 위 문서를 참조하면 된다.
이 terraform_remote_state의 핵심은 각 워크스페이스 별 상태관리 파일이 있음을 이제 알고 있다. .tfstate 파일로 된. 이 파일을 참조해서 다른 워크스페이스에서 이 상태를 가져다가 사용하겠다는 것이 terraform_remote_state의 핵심이다.
근데 이 때 Backend가 무엇이냐에(local, remote) 따라 작성 방식이 살짝 다르다. 이 내용도 위 문서에 있다.
우선, terraform_remote_state를 Data Source로 사용한다. backend는 Local이고 그 경로는 이 파일의 상위 경로에 있는 network 워크스페이스의 .tfstate 파일이 된다.
그리고 로컬 변수로 vpc_name과 subnet_groups를 만들어주는데 이 각각의 변수값을 위에서 선언한 terraform_remote_state의 DataSource를 사용한다. 다만 여기서 한가지 주의할 점은 remote_state를 가져올 땐 .outputs 을 통해서 가져와야한다.
data.terraform_remote_state.network.outputs.vpc_name 이런식으로 말이다.
그리고 이 로컬 변수를 EC2 리소스에서도 사용한다. 하단 소스를 보면 subnet_id와 tags의 Name을 로컬변수를 가지고 사용했다.
variable "name" {
description = "The name for the AWS account. Used for the account alias."
type = string
}
variable "password_policy" {
description = "Password Policy for the AWS account."
type = object({
minimum_password_length = number
require_numbers = bool
require_symbols = bool
require_lowercase_characters = bool
require_uppercase_characters = bool
allow_users_to_change_password = bool
hard_expiry = bool
max_password_age = number
password_reuse_prevention = number
})
default = {
minimum_password_length = 8
require_numbers = true
require_symbols = true
require_lowercase_characters = true
require_uppercase_characters = true
allow_users_to_change_password = true
hard_expiry = false
max_password_age = 0
password_reuse_prevention = 0
}
}
outputs.tf
이 파일은 모듈에서 만들어내는 outputs을 정의한 파일이다. 특정 테라폼 소스에서 어떤 모듈을 가져다가 사용할 때 모듈이 내뱉는 attribute를 참조해야 하는 경우가 많은데 그 참조할 수 있는 값들은 모듈에서 뱉어내는 output 뿐이다. 그래서 적절한 output이 중요하다.
소스 코드는 다음과 같다.
output "id" {
description = "The AWS Account ID"
value = data.aws_caller_identity.this.account_id
}
output "name" {
description = "Name of the AWS account. The account alias."
value = aws_iam_account_alias.this.account_alias
}
output "signin_url" {
description = "The URL to signin for the AWS account."
value = "https://${var.name}.signin.aws.amazon.com/console"
}
output "password_policy" {
description = "Password Policy for the AWS Account. `expire_passwords` indicates whether passwords in the account expire. Returns `true` if `max_password_age` contains a value greater than 0."
value = aws_iam_account_password_policy.this
}
저번 포스팅에서 잠시 다루어보았던 Terraform Cloud를 좀 더 이해해보는 포스팅이다. 우선 기존에 만들었던 Organizations의 Workspace로 가보자.
Workspace > Settings > General에 가보면 다음과 같은 설정 부분이 있다.
저번엔 이 부분을 우선 Local로 하고 추후에 더 자세히 알아보기로 했었는데 이 부분에 대해 얘기할 시간이다.
Execution Mode: Local
Terraform 명령 수행을 Local에서 실행하겠다는 의미이다. 즉, 작업자 PC에서 실행하겠다는 의미가 된다.
Terraform Cloud는 State 저장 역할만 수행하게 된다.
Execution Mode: Remote
Remote로 설정하게 되면 Terraform Cloud 인프라에 위치한 테라폼 명령어를 실행시키는 Runner를 이용해서 Terraform Code를 수행하게 된다. 즉, 작업자 PC에서는 어떠한 Terraform 관련 코드도 수행되지 않는다.
이건 이 테라폼 수행 코드가 작업자 PC에서만 접근 가능한 경우 Terraform Cloud 인프라에 위치한 Runner는 해당 리소스에 접근할 수 없기 때문에 접근 가능하도록 어떤 설정을 해줘야한다. 그렇기 때문에 저번 게시글에선 Remote로 우선은 작업하지 않은것이다.
이 Remote로 작업을 수행하게 되면 몇가지 장점들이 있다. 우선 Plan을 수행했을 때 자동으로 Apply를 실행할 것인지에 대한 선택을 할 수 있다.
그리고 Terraform Version 또한 지정할 수 있다.
그리고 Working Directory도 지정해줘야 한다.
그리고 가장 핵심 기능 중 하나인 워크스페이스에서 Variables 관리가 가능해진다.
또 하나 좋은 기능인 Run triggers 라는 기능이 있다. 이는 Organizations > Workspace 내부에서 확인할 수 있는데 다음 화면을 확인하자.
이 Run triggers는 어떤 것이냐면 A, B, C, D 라는 Workspace가 있을 때 B라는 워크스페이스는 A, D에 의존하고 있다고 가정해보자. 그럼 A 또는 D가 Apply가 일어나면 B도 자동으로 Apply를 해주는 기능이 Run triggers이다.
그럼 한번 Remote Execution Mode로 작업을 해보자.
Environment Variables 추가
우선, Provider가 AWS Provider이기 때문에 AWS CLI에 IAM User Credential을 추가한 것 처럼 이 Terraform Cloud한테도 Credential을 알려줘야한다. 그 방법은 위에서 보여준 Variables 화면에서 추가할 수 있다.
위 사진처럼 Environment variable로 AWS Credential을 Sensitive로 추가해주자.
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
그리고 내가 이 Terraform Cloud를 사용해서 관리했던 테라폼 main.tf, terraform.tfvars 파일은 다음과 같다.
main.tf
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
]
}
output "developers" {
value = local.developers
}
output "high_level_users" {
value = [
for user in var.users : user if user.level > 5
]
}
terraform.tfvars
users = [
{
name = "john"
level = 7
role = "재무"
is_developer = false
},
{
name = "alice"
level = 1
role = "인턴 개발자"
is_developer = true
},
{
name = "tony"
level = 4
role = "데브옵스"
is_developer = true
},
{
name = "cindy"
level = 9
role = "경영"
is_developer = false
},
{
name = "hoon"
level = 3
role = "마케팅"
is_developer = false
}
]
여기서 terraform.tfvars 파일에 있는 이 Terraform Variable 역시 Cloud에 추가해줘야 한다.
이제 모든 준비는 끝났고 위에서 말한 Working Directory 경로만 지정해주면 된다. 나의 경우 디렉토리 구조가 다음과 같이 되어있다.
그래서 Working Directory는 Remote Backend를 사용하는 loop로 지정하면 된다.
이제 Apply를 한번 해보자. 하기 전에 terraform login 명령어를 통해서 로그인을 먼저 해주자. 토큰은 로그인을 한번이라도 했다면 이 경로에 저장되어 있다.
~/.terraform.d/credentials.tfrc.json
실행이 되면 다음과 비슷한 모습으로 보여질 것이다. 그래서 우선 Plan 부분이 보여지고 마지막에 'yes' 입력하는 프롬프트에서 멈춘다.
'yes'를 입력하고 리소스를 적용하면 다음과 같다. Outputs이 쭉 나오고 AWS Console에서 리소스를 확인할 수 있다.
이번엔 상태 관리에 대한 내용이다. Terraform을 이용해서 어떤 작업을 하면 현재 작업한 상태에 대한 파일이 기록되는 것을 terraform.state 파일을 통해 알았다. 이 파일을 보면 지금 이 Terraform이 관리하고 있는 상태를 보여준다. 굉장히 중요한 파일이다. 이 파일을 기점으로 Apply를 했을 때 변경점을 캐치하거나, 관리하고 있는 리소스를 파악하거나 할 수 있고 Destroy 명령어를 했을 때 무엇을 삭제할지 파악할 수 있기 때문이다.
위 코드를 보면 알겠지만 현재 상태에서 관리하고 있는 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을 걸어서 다른 사람은 변경을 하지 못하게 막는것이 중요하다.
여기까지 하고 나면 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 파일에 넣어버리는 것.
provider "aws" {
region = "ap-northeast-2"
}
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
]
}
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.developer.name, aws_iam_group.employee.name
] : [
aws_iam_group.employee.name
]
}
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
]
}
그리고 변수를 정의한 파일인 terraform.tfvars 파일은 다음과 같다.
users = [
{
name = "john"
level = 7
role = "재무"
is_developer = false
},
{
name = "alice"
level = 1
role = "인턴 개발자"
is_developer = true
},
{
name = "tony"
level = 4
role = "데브옵스"
is_developer = true
},
{
name = "cindy"
level = 9
role = "경영"
is_developer = false
},
{
name = "hoon"
level = 3
role = "마케팅"
is_developer = false
}
]
이런식으로 for 문을 사용할 수 있다.
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
}
}
코드를 보면 이러한 구문이 있다.
for user in var.users : user.name => user
선언한 users 라는 변수에 담긴 각각의 유저 하나씩 루프를 돌리는데 이걸 담는 형태가 Map 형식이기 때문에 Key/Value를 다음과 같이 선언하는 것.
user.name => user
Map이 아닌 배열로 담는 경우 다음과 같이 사용한다.
locals {
developers = [
for user in var.users : user if user.is_developer
]
}
다만, 이 배열에 담을 땐 조건이 따른다. 그래서 if가 있다. 조건 없이 모두 넣으려고 한다면 이렇게 사용하면 된다.
locals {
developers = [
for user in var.users : user
]
}
그리고 위 main.tf 파일에서 한 가지 새로운 개념인 depends_on 이 있다.
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 ] # 이 리소스가 생성되어야만 가능하게 설정
}
이 depends_on이 있는 리소스는 depends_on에 걸려있는 리소스가 생성되어야만 만들어지는 리소스임을 의미한다.
우선, for-each를 사용하기 전 count를 먼저 사용해보자. 이 count는 HCL에서 예전부터 있던 기능인데 이 기능에 대한 문제점을 보완하고자 for-each가 나왔다고 생각하면 된다. 우선 코드를 바로 보자.
# ---------
# count
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_iam_user" "count" {
count = 5
name = "count-user-${count.index}"
}
output "count_user_arns" {
value = aws_iam_user.count.*.arn
}
이 코드에서 "count = 5"를 보면 5개를 만들어낼 것을 예측할 수 있다. 그리고 이 count는 자체적으로 가지는 변수 index라는 것이 있다. 그래서 aws_iam_user를 만들어낼 때 그 이름을 count.index로 변형을 주어 5명의 사용자를 만들어낸다.
그리고 output을 보면 aws_iam_user.count 리소스가 리스트로 만들어지는데 [count-user-0, count-user-2, ..., count-user-9] 이런식으로 말이다. 그러면 그 각각의 유저들의 arn을 output으로 지정한다는 뜻이다.
이 코드를 Init - Apply 해보자. Apply 결과는 다음과 같다. 잘 만들어졌지만 다음 사진이 바로 count의 문제점이다.
인덱스로 각각의 리소스를 나타내기 때문에 그래서 0번이 누구인지, 1번이 누구인지 알아보기가 상당히 까다롭고, 만약 아래처럼 0, 1, 2, 3, 4 총 5명의 유저가 있을 때 2번 유저를 삭제하면 3, 4이 한칸씩 앞으로 옮겨지게된다. 이러면 또 골머리가 아파진다.
AWS Console에서 확인해보면 다음과 같이 잘 만들어졌음을 확인할 수 있다.
For-Each
For-Each는 Set/Map을 지원한다. Set은 리스트인데 유니크한 값만을 가지는 리스트이고, Map은 {"key": "value", ...} 이러한 형식이다. 바로 코드를 보자. 다음은 Set을 사용한 For-Each문이다.
그래서 각각의 value가 {...} 이 부분인데 이것 전체를 가져온다는 의미가 values(aws_iam_user.for_each_set)이다. 그래서 그 모양은 [{...}, {...}, ..] 이렇게 될 것이다. 그리고 그 각각의 value가 가지는 모든것을 의미하는 '*'에서 arn을 찍어낸다.
Apply를 해보자. 결과는 다음과 같다.
AWS Console에서도 확인해보자. 잘 만들어졌다.
다음은 Set이 아닌 Map으로 ForEach를 사용해보자. 뭐 크게 달라지는 건 없다. Map이니까 데이터 모양새가 좀 다를것이고 더 많은 정보가 들어갈 수 있을 것 같다.