GCP IAM by Terraform
1. IAM Create ServiceAccount
시작
최근에 IAM을 Terraform통해 관리 하고 싶다는 요구사항을 받았습니다.
Terraform은 항상 Infra관련 내용만 다뤄보았기 때문에 IAM 관련 부분은 조금 어설프고 낯설었습니다. 하나 하나씩 준비해가면서 느낀 점은 IAM Terraform Code는 Infra를 다루는 것에 비해 매우 간단하다는 것 입니다.
그렇지만 권한을 다루는 작업이기 때문에 코드 작성과 적용 할 때 주의해야 할 점들이 몇 가지 있었습니다. 그 중 하나는 “Authoritative”라는 특성이었습니다. IAM Terraform 관련해서 검색하다 보면 Authoritative 또는 Non-Authoritative라는 단어가 꽤 자주 등장하는데, 이 Authoritative라는 특성이 무엇인지 파악하는 것은 중요합니다.
https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam
처음에 Authoritative에 대한 개념이 없는 상태로 Authoritative한 로직을 이용해 코드 작성 후 Role 부여하고 Apply를 실행하더니, 예상과는 달리 내가 정의한 Role이 추가로 붙는 형태가 아니라 이전에 지니고 있던 Role들을 삭제 후, 내가 정의한 Role만 부여되어있는 형태가 되었습니다.
즉, Owner 권한을 포함해 이전에 부여한 모든 Role이 삭제가 되었고, 내가 정의한 viewer Role만 남게 되는 상황이 되었습니다. 결국엔 스스로 Role추가가 불가한 View 역할만 가지게 되어 아무것도 할 수 없는 상태가 되었고, Organization을 관리하시는 담당자에게 다시 Owner Role 부여를 요청하는 일이 발생했습니다.
즉, 코드는 간단하지만 이러한 주의점이 있으니 코드 작성 시 참고하길 바랍니다.
개요
IAM 관련한 시나리오를 아래와 같이 세웠고, 해당 시나리오를 Terraform 코드로 작성했습니다. 첫번째는 service account 생성 및 Role Binding하는 코드를 살펴 보겠습니다.
1. service account 생성 및 Role Binding
2. service account Key 생성 및 Terraform Cloud에서 확인 방법
3. custom role 생성 및 부여
4. organization Policy 컨트롤
5. dataset access 권한 설정
사용 가능 CASE
A. 단일 및 복수개의 service account 생성 및 Role 부여
코드 구조
코드는 아래와 같은 구조로 작성했습니다.
코드 리뷰
1. ./modules/iam/var.tf
변수들을 받기 위한 파일이며, 해당 파일에서는 변수들의 type 및 default 값을 지정했습니다.
선언하지 않는 (사용하지 않을) 변수에는 default 값을 지정해 줌으로써 terraform plan 또는 apply를 실행 했을 시, 값이 선언되어 있지 않았다는 오류가 발생하지 않도록 하였습니다. (project_id, region은 제외)
######################################
## Default
######################################## Project
variable "project_id" {
type = string
}## Region
variable "region" {
type = string
}######################################
## Service Account
######################################## Service Account ID
variable "names" {
type = set(string)
default = []
}## Service Account Name
variable "display_name" {
type = string
default = ""
}## Service Account Description
variable "description" {
type = string
default = ""
}## Service Account Role
variable "roles" {
type = set(string)
default = []
}
project_id : 리소스를 생성할 Project를 정하는 변수입니다.
region : region을 정하는 변수입니다.
names : 생성할 service account의 고유 이름을 생성하는 변수입니다.
display_name : service account의 보여지는 이름을 생성하는 변수이며 고유하지 않아도 됩니다.
description : 생성 할 service account에 대해 설명을 기술하는 변수입니다.
roles : service account에 부여하고 싶은 role을 기술하는 부분입니다. 롤은 단일 및 복수로 설정이 가능하며 role을 부여하고 싶지 않은 경우 선언을 하지 않으면 됩니다.
2. main.tf
사용 모듈을 정의하고, ./modules/iam/var.tf 에 값을 매칭 하기 위한 파일입니다.
아래 코드의 ★★★ 부분을 확인해보면
복수개의 service account에 복수개의 role을 바인딩 하려고 변수값을 지정해둔 것을 확인 할 수 있습니다. 반대로 service account에 role부여를 원하지 않는 경우에는 roles 변수를 생략해주면 됩니다.
## Define Modules
#create service accounts
module "create_serviceaccounts" {
source = "./modules/iam"
project_id= "kdyoung-int-210504"
region = "asia-northeast3"
names = ["test1-serviceaccount" , "test2-serviceaccount", "test3-serviceaccount"] ★★★ display_name = "Test Service Accounts"
description = "Test Service Accounts"
roles = ["roles/bigtable.viewer","roles/bigquery.resourceViewer"] ★★★
}
3. ./modules/iam/main.tf
실질적으로 동작하는 iam 모듈파일입니다.
아래 코드는 setproduct와 zipmap 함수를 이용해 복수개의 service account에 복수개의 role을 부여 할 수 있도록 작성한 코드입니다.
locals {
## setproduct 함수를 이용
names_roles_combination=setproduct(var.names, var.roles)## zipmap 함수를 이용
names_roles_combination_map = zipmap(
[for names_roles in local.names_roles_combination : "${names_roles[0]}-${names_roles[1]}"],
[for names_roles in local.names_roles_combination : {
name = names_roles[0]
role = names_roles[1]
}]
)
}resource "google_service_account" "service_account" {
for_each = var.names
account_id = each.value
display_name = var.display_name
}resource "google_project_iam_member" "role_binding" {
for_each = local.names_roles_combination_map
role = each.value.role
member = "serviceAccount:${google_service_account.service_account[each.value.name].email}"
}
코드에 대해서 리뷰해보면
setproduct(var.names, var.roles)
일단 위 함수 내용을 풀어서 적어보면 아래와 같은 형태가 됩니다.
그렇다면 setproduct 함수란 어떤 역할을 하는 함수일까?
setproduct(["test1-serviceaccount" , "test2-serviceaccount", "test3-serviceaccount"], ["roles/bigtable.viewer","roles/bigquery.resourceViewer"])
setproduct란 집합 요소의 가능한 모든 조합을 찾게 해주는 함수입니다.
( ( x , y ) * (1 , 2 ) = x1 , x2 , y1 , y2 와 같이 모든 조합을 찾게 해주는 함수)
즉, 위 명시된 setproduct를 실행 하게 되면 집합 요소의 가능한 모든 조합을 찾게 해줌으로 아래와 같은 결과를 가질 수 있게 됩니다.
["test1-serviceaccount" , "roles/bigtable.viewer" ]
["test1-serviceaccount" , "roles/bigquery.resourceViewer" ]
["test2-serviceaccount" , "roles/bigtable.viewer" ]
["test2-serviceaccount" , "roles/bigquery.resourceViewer" ]
["test3-serviceaccount" , "roles/bigtable.viewer" ]
["test3-serviceaccount" , "roles/bigquery.resourceViewer" ]
이렇게 모든 조합을 찾은 값들을 zipmap을 이용해 map 형태의 key, value로 나눠줍니다.
여기서 왜 map 형태로 변경하는 것일까?
for_each문은 set(string) 형태와 map 형태만을 받아들이도록 되어있습니다.즉, 한마디로 해당 조합들을 zipmap을 이용해 map 형태로 변경해 결론적으로는 for_each문을 사용하기 위함입니다. 그럼으로 아래와 같이 zipmap을 사용하였습니다.
zipmap(
[for names_roles in local.names_roles_combination : "${names_roles[0]}-${names_roles[1]}"],
[for names_roles in local.names_roles_combination : {
name = names_roles[0]
role = names_roles[1]
}]
위 코드를 아래와 같이 풀어 써보았습니다.
zipmap([
"test1-serviceaccount-roles/bigtable.viewer" ,
"test1-serviceaccount-roles/bigquery.resourceViewer" ,
"test2-serviceaccount-roles/bigtable.viewer" ,
"test2-serviceaccount-roles/bigquery.resourceViewer" ,
"test3-serviceaccount-roles/bigtable.viewer" ,
"test3-serviceaccount-roles/bigquery.resourceViewer" ,
] ,
[
"name = roles/bigtable.viewer
role = roles/bigquery.resourceViewer",
"name = roles/bigtable.viewer
role = roles/bigquery.resourceViewer",
"name = roles/bigtable.viewer
role = roles/bigquery.resourceViewer",
"name = roles/bigtable.viewer
role = roles/bigquery.resourceViewer",
"name = roles/bigtable.viewer
role = roles/bigquery.resourceViewer",
"name = roles/bigtable.viewer
role = roles/bigquery.resourceViewer"
])
그렇다면 zipmap이라는 함수는 어떤식으로 map형태로 변형해줄까?
zipmap 코드는 왼쪽의 리스트와 오른쪽 리스트가 pair로 map 형태로 묶일 수 있도록 해주는 함수입니다.
즉, 아래와 같은 Map 형태로
오른쪽 6개와 왼쪽 6개가 1:1 매칭 되게 해주는 함수입니다.
결과적으로 zipmap의 결과값은 총 6개 값이 나오며 for_each문을 사용하면 6번의 반복문을 실행 할 수 있게 됩니다.
또한 각 반복문 마다 name, role이라는 key, value 값이 매칭이 되어있어
한개의 service account에 role 부여가 2개
총, 3개 유저가 있음으로 총 6번의 루프가 돌며 로직을 수행 하게 될 것입니다.
< 각 for문의 key 값을 가지고 해당 value 값을 얻을 수 있다는 것이 중요한 포인트 (each.value.name / each.value.role) >
resource "google_project_iam_member" "role_binding" {
for_each = local.names_roles_combination_map
role = each.value.role
member = "serviceAccount:${google_service_account.service_account[each.value.name].email}"
}
정리하자면 service account 한 개 당 role을 2개 부여를 해야 되는 상황임으로
총 루프가 6번 돌아야 된다는 것을 알 수 있습니다.
그럼으로 setproduct이라는 함수를 이용해 6개의 조합을 만들고
zipmap 사용해 해당 함수안에서 for 문을 이용해 왼쪽에는 Title , 오른쪽에는 실질적인 값인 key/value 형태로 나누어 왼쪽, 오른쪽 1:1 매칭해 map 형태로 변환해 줍니다.
최종적으로 map 형태로 변환한 값을 이용해 복수개의 service account에 복수개의 role을 부여 가능하게 for_each문을 통해 작성하였습니다.
google_project_iam_policy VS google_project_iam_member
위에 언급을 한 적이 있지만, 이 둘의 차이는 Authoritative한 로직인지 아닌지입니다.
즉, google_project_iam_policy라는 로직을 사용한다면 내가 정의한 Role들만을 가진 service account 또는 member, group이 될 것이고(이전에 정의되었던 Role은 삭제됨)
Non-authoritative한 google_project_iam_member를 사용하게 되면 내가 정의한 Role이 이전 Role에 추가가 되는 형식이 될 것입니다.
그렇다면 Authoritative한 google_project_iam_policy는 언제 사용하면 좋을까?
해당 로직은 관리자 입장에서 사용하면 좋을 것 같습니다. 예를 들면 우리 팀이 Security팀이라 가정을 해봅시다. 하위에 있는 수많은 project에서 Role들이 계속해서 추가가 될 것이며, 그 Role들 중에는 사용자가 필요한 Role보다 더 큰 Role이 부여가 되어 있는 Role이 있을 수도 있으며, 프로젝트가 끝난 후에도 삭제가 되지 않고 남아있는 Role이 있을 수도 있습니다.
이러한 Role들을 컨트롤 하기 위해 Security차원에서 Security팀에서는 Project마다 필수적으로 필요한 Role만을 남겨두고 이전 Role들을 삭제하고 싶은 경우가 있을 수 있다.
이러한 경우, Project에 꼭 필요한 Role들만 정의를 해두고 Authoritative한 google_project_iam_policy를 사용해 코드 작성 및 정기적으로 배포를 통해 이를 해결 할 수 있을 것이라고 생각합니다.
코드 적용
Terraform Plan
$ terraform planTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ createTerraform will perform the following actions:# module.create_serviceaccounts.google_project_iam_member.role_binding["test1-serviceaccount-roles/bigquery.resourceViewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigquery.resourceViewer"
}# module.create_serviceaccounts.google_project_iam_member.role_binding["test1-serviceaccount-roles/bigtable.viewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigtable.viewer"
}# module.create_serviceaccounts.google_project_iam_member.role_binding["test2-serviceaccount-roles/bigquery.resourceViewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigquery.resourceViewer"
}# module.create_serviceaccounts.google_project_iam_member.role_binding["test2-serviceaccount-roles/bigtable.viewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigtable.viewer"
}# module.create_serviceaccounts.google_project_iam_member.role_binding["test3-serviceaccount-roles/bigquery.resourceViewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigquery.resourceViewer"
}# module.create_serviceaccounts.google_project_iam_member.role_binding["test3-serviceaccount-roles/bigtable.viewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigtable.viewer"
}# module.create_serviceaccounts.google_service_account.service_account["test1-serviceaccount"] will be created
+ resource "google_service_account" "service_account" {
+ account_id = "test1-serviceaccount"
+ disabled = false
+ display_name = "Test Service Accounts"
+ email = (known after apply)
+ id = (known after apply)
+ name = (known after apply)
+ project = (known after apply)
+ unique_id = (known after apply)
}# module.create_serviceaccounts.google_service_account.service_account["test2-serviceaccount"] will be created
+ resource "google_service_account" "service_account" {
+ account_id = "test2-serviceaccount"
+ disabled = false
+ display_name = "Test Service Accounts"
+ email = (known after apply)
+ id = (known after apply)
+ name = (known after apply)
+ project = (known after apply)
+ unique_id = (known after apply)
}# module.create_serviceaccounts.google_service_account.service_account["test3-serviceaccount"] will be created
+ resource "google_service_account" "service_account" {
+ account_id = "test3-serviceaccount"
+ disabled = false
+ display_name = "Test Service Accounts"
+ email = (known after apply)
+ id = (known after apply)
+ name = (known after apply)
+ project = (known after apply)
+ unique_id = (known after apply)
}Plan: 9 to add, 0 to change, 0 to destroy.
Terraform apply
$ terraform applyTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ createTerraform will perform the following actions:# module.create_serviceaccounts.google_project_iam_member.role_binding["test1-serviceaccount-roles/bigquery.resourceViewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigquery.resourceViewer"
}# module.create_serviceaccounts.google_project_iam_member.role_binding["test1-serviceaccount-roles/bigtable.viewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigtable.viewer"
}# module.create_serviceaccounts.google_project_iam_member.role_binding["test2-serviceaccount-roles/bigquery.resourceViewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigquery.resourceViewer"
}# module.create_serviceaccounts.google_project_iam_member.role_binding["test2-serviceaccount-roles/bigtable.viewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigtable.viewer"
}# module.create_serviceaccounts.google_project_iam_member.role_binding["test3-serviceaccount-roles/bigquery.resourceViewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigquery.resourceViewer"
}# module.create_serviceaccounts.google_project_iam_member.role_binding["test3-serviceaccount-roles/bigtable.viewer"] will be created
+ resource "google_project_iam_member" "role_binding" {
+ etag = (known after apply)
+ id = (known after apply)
+ member = (known after apply)
+ project = (known after apply)
+ role = "roles/bigtable.viewer"
}# module.create_serviceaccounts.google_service_account.service_account["test1-serviceaccount"] will be created
+ resource "google_service_account" "service_account" {
+ account_id = "test1-serviceaccount"
+ disabled = false
+ display_name = "Test Service Accounts"
+ email = (known after apply)
+ id = (known after apply)
+ name = (known after apply)
+ project = (known after apply)
+ unique_id = (known after apply)
}# module.create_serviceaccounts.google_service_account.service_account["test2-serviceaccount"] will be created
+ resource "google_service_account" "service_account" {
+ account_id = "test2-serviceaccount"
+ disabled = false
+ display_name = "Test Service Accounts"
+ email = (known after apply)
+ id = (known after apply)
+ name = (known after apply)
+ project = (known after apply)
+ unique_id = (known after apply)
}# module.create_serviceaccounts.google_service_account.service_account["test3-serviceaccount"] will be created
+ resource "google_service_account" "service_account" {
+ account_id = "test3-serviceaccount"
+ disabled = false
+ display_name = "Test Service Accounts"
+ email = (known after apply)
+ id = (known after apply)
+ name = (known after apply)
+ project = (known after apply)
+ unique_id = (known after apply)
}Plan: 9 to add, 0 to change, 0 to destroy.Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.Enter a value: yes--- 생략 ---Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
Console 확인
문제없이 3개의 service account에 2개의 롤들이 각각 부여가 되어있는 것을 확인 할 수 있습니다.
참조 코드
https://github.com/terraform-google-modules/terraform-google-service-accounts