Este artigo foi originalmente publicado no Medium (English only)
O objetivo deste artigo é apresentar um código Terraform que cria múltiplos buckets, em múltiplos locais, e com múltiplas permissões IAM.
Este código foi usado para resolver o problema descrito na seção Declaração do Problema.
A estratégia de implementação pode variar de um caso para o outro e não é discutida aqui em profundidade.
Declaraçāo do Problema
Eu fui confrontado com o seguinte desafio no outro dia: onde temos um ambiente multi-tenant no GCP e queremos usar Infraestrutura como Código (IaC) para gerenciar os recursos criados em projetos e ambientes diferentes - dev/uat/prod.
Requisitos:
Tanto a equipe de TI quanto os tenants devem executar o IaC sempre que necessário.
Os tenants não precisam saber o que é Terraform ou a nomenclatura do GCP.
Cada tenant tem seu próprio repositório do GitHub e a partir dele você precisa gerenciar todos os recursos.
Use as roles predefinidas do GCP.
Crie vários buckets em várias localidades.
Os tenants podem atribuir permissões a contas de serviço do GCP que pertencem a projetos diferentes, ou seja, o tenant1 pode atribuir permissão de visualizador a uma conta de serviço do tenant2.
Exemplo de arquitetura:
Soluçāo Proposta
Criar um módulo Terraform para atender aos requisitos.
Você pode encontrar o código Terraform completo no seguinte repositório do GitHub. O módulo foi testado usando Terraform v1.1.7 com o provedor Terraform Google v4.13.0 no MacOs Monterey 12.2.1.
terraform {
required_version = "~> 1.1.7"
required_providers {
google = {
source = "hashicorp/google"
version = "4.13.0"
}
}
}
Este módulo facilita a criação de um ou mais buckets do GCS e a atribuição de permissões básicas.
Os recursos que este módulo irá criar/acionar são:
Um ou mais buckets do GCS
Zero ou mais permissões IAM para esses buckets
Este módulo foi escrito para atribuir permissões a contas de serviço do GCP. Pode-se seguir a mesma lógica para estender isso a grupos e usuários.
Como funciona
Para fins de exemplo, um módulo Terraform foi criado na subpasta cloud-storage-modules. Em um cenário real, você pode criar o módulo em um repositório GitHub separado e chamá-lo sempre que necessário.
O módulo é configurado para receber as variáveis definidas no arquivo local terraform.tfvars.json. Ele cria os buckets e atribui as IAM roles conforme definido.
Consulte a documentação do módulo Terraform para obter uma explicação detalhada sobre o módulo.
Este módulo não cria IAM roles. Ele utiliza as funções pré-definidas do GCP que já existem.
Como utilizar
O uso básico deste módulo é o seguinte e pode ser encontrado no arquivo main.tf.
terraform.tfvars.json
Este é o arquivo de variáveis usado para criar os recursos. Esta seção descreve como declarar os buckets.
A tabela abaixo descreve as variáveis de entrada para o módulo.
Explicando o módulo gcs_bucket
Esta seção descreve linha por linha como o módulo gcs_bucket funciona.
O rótulo imediatamente após a palavra-chave do módulo é um nome local. O argumento de source é obrigatório para todos os módulos. Isso significa que estamos usando o código Terraform presente na pasta cloud-storage-module. O meta-argumento for_each aceita um mapa e cria uma instância para cada item no mapa. Cada instância possui um objeto de infraestrutura distinto associado a ela, e cada um é criado, atualizado ou destruído separadamente quando a configuração é aplicada. Na prática, o for_each nos permite criar de 1 a N buckets.
module "gcs_bucket" {
for_each = var.gcs_buckets
source = "./cloud-storage-module"
location é a key da variável gcs_buckets no terraform.tfvars.json.
location = each.key
project é a entrada equivalente no terraform.tfvars.json.
project = var.project
name é a combinaçāo da variável name mais location e um random id para formar um nome de bucket único.
name = "${each.value.name}-${each.key}-${random_id.bucket.hex}"
Ambos storage_class e versioning_enabled utilizam uma estrutura condicional simples. Se existe um valor para essas variáveis em terraform.tfvars.json usa-se este valor, caso contrário, utilizamos o valor padrāo que está declarado no arquivo variables.tf.
storage_class = each.value.storage_class != "" ? each.value.storage_class : var.storage_class
versioning_enabled = each.value.versioning_enabled != "" ? each.value.versioning_enabled : var.versioning_enabled
Quando definido em terraform.tfvars.json a configuraçao de gerenciamento de ciclo de vida (lifecycle) é adicionada ao bucket.
lifecycle_policy = each.value.lifecycle_rule
As variáveis internal_tenant_roles e external_tenant_roles referem-se as politicas de IAM para Cloud Storage Bucket dentro e fora do projeto.
internal_tenant_roles_admin = each.value.internal_tenant_roles_admin
internal_tenant_roles_viewer = each.value.internal_tenant_roles_viewer
external_tenant_roles_admin = each.value.external_tenant_roles_admin
external_tenant_roles_viewer = each.value.external_tenant_roles_viewer
}
Explorando a gestão das políticas do IAM
A gestão das políticas do IAM merece um tópico separado, pois não se trata de uma configuração "simples" do Terraform. Um requisito importante para a automação é manter a simplicidade para os tenants e torná-la reutilizável em diferentes ambientes. Portanto, do ponto de vista do tenant, toda a configuração necessária é:
Regras internas ao projeto:
"internal_tenant_roles_admin": {
"objectAdmin": {
"service_accounts": ["platform-infra", "platform-ko"]
}
},
"internal_tenant_roles_viewer": {
"objectViewer": {
"service_accounts": ["viewer-infra", "viewer-ko"]
}
}
Regras externas ao projeto:
"external_tenant_roles_admin": {
"objectAdmin": [
{
"project": "tenant2",
"service_accounts": ["platform-infra", "platform-ko"]
}
]
},
"external_tenant_roles_viewer": {
"objectViewer": [
{
"project": "tenant2",
"service_accounts": ["viewer-infra", "viewer-ko"]
}
]
}
O tenant fornece apenas a role e um nome curto para a conta de serviço. No entanto, a automação precisa completar os nomes com o nome totalmente qualificado do GCP, como:
serviceAccount:platform-infra@tenant1-dev.iam.gserviceaccount.com
Essa transformação é feita localmente no arquivo main.tf no bloco locals. A lógica é semelhante tanto para recursos internos quanto externos.
Um novo objeto <interno/externo>_roles_fully_qualified_<admin/viewer> é criado como resultado de um loop na respectiva variável. Dentro do loop, outro loop é realizado para cada entrada de service_accounts. A função coalesce é utilizada para retornar todos os valores não vazios na entrada respectiva. Para cada entrada válida, ela substitui pelo project correto e o valor (v) é fornecido pelo tenant.
internal_roles_fully_qualified_admin = {
for tenant_role, entities in var.internal_tenant_roles_admin :
tenant_role => {
service_accounts : [for k, v in coalesce(entities["service_accounts"], []) : "serviceAccount:${v}@${var.project}.iamgserviceaccount.com"]
}
}
Particularmente para as roles externas, o flatten é utilizado para integrar a entrada project.
external_roles_fully_qualified_admin = {
for tenant_role, entries in var.external_tenant_roles_admin :
tenant_role => {
service_accounts = flatten([
for entry in entries : [
for k, v in coalesce(entry["service_accounts"], []) :
"serviceAccount:${v}@${entry["project"]}.iam.gserviceaccount.com"
]
])
}
}
Saída do comando Terraform Plan
Esta é a saída do comando terraform plan plano utilizando o arquivo de exemplo terraform.tfvars.json que você pode encontrar no repositório do GitHub.
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:
# random_id.bucket will be created
+ resource "random_id" "bucket" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 8
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {
+ "bucket_id" = "tenant1-dev"
}
}
# module.gcs_bucket["eu"].google_storage_bucket.bucket will be created
+ resource "google_storage_bucket" "bucket" {
+ force_destroy = true
+ id = (known after apply)
+ location = "EU"
+ name = (known after apply)
+ project = "tenant1-dev"
+ requester_pays = false
+ self_link = (known after apply)
+ storage_class = "STANDARD"
+ uniform_bucket_level_access = true
+ url = (known after apply)
+ lifecycle_rule {
+ action {
+ type = "Delete"
}
+ condition {
+ age = 1
+ matches_storage_class = []
+ with_state = (known after apply)
}
}
+ versioning {
+ enabled = true
}
}
# module.gcs_bucket["eu"].google_storage_bucket_iam_member.admin-member-bucket-external["serviceAccount:platform-infra@tenant2-dev.iam.gserviceaccount.com"] will be created
+ resource "google_storage_bucket_iam_member" "admin-member-bucket-external" {
+ bucket = (known after apply)
+ etag = (known after apply)
+ id = (known after apply)
+ member = "serviceAccount:platform-infra@tenant2-dev.iam.gserviceaccount.com"
+ role = "roles/storage.objectAdmin"
}
# module.gcs_bucket["eu"].google_storage_bucket_iam_member.admin-member-bucket-external["serviceAccount:platform-ko@tenant2-dev.iam.gserviceaccount.com"] will be created
+ resource "google_storage_bucket_iam_member" "admin-member-bucket-external" {
+ bucket = (known after apply)
+ etag = (known after apply)
+ id = (known after apply)
+ member = "serviceAccount:platform-ko@tenant2-dev.iam.gserviceaccount.com"
+ role = "roles/storage.objectAdmin"
}
# module.gcs_bucket["eu"].google_storage_bucket_iam_member.admin-member-bucket-internal["serviceAccount:platform-infra@tenant1-dev.iam.gserviceaccount.com"] will be created
+ resource "google_storage_bucket_iam_member" "admin-member-bucket-internal" {
+ bucket = (known after apply)
+ etag = (known after apply)
+ id = (known after apply)
+ member = "serviceAccount:platform-infra@tenant1-dev.iam.gserviceaccount.com"
+ role = "roles/storage.objectAdmin"
}
# module.gcs_bucket["eu"].google_storage_bucket_iam_member.admin-member-bucket-internal["serviceAccount:platform-ko@tenant1-dev.iam.gserviceaccount.com"] will be created
+ resource "google_storage_bucket_iam_member" "admin-member-bucket-internal" {
+ bucket = (known after apply)
+ etag = (known after apply)
+ id = (known after apply)
+ member = "serviceAccount:platform-ko@tenant1-dev.iam.gserviceaccount.com"
+ role = "roles/storage.objectAdmin"
}
#
module.gcs_bucket["eu"].google_storage_bucket_iam_member.viewer-member-bucket-external["serviceAccount:viewer-infra@tenant2-dev.iam.gserviceaccount.com"] will be created
+ resource "google_storage_bucket_iam_member" "viewer-member-bucket-external" {
+ bucket = (known after apply)
+ etag = (known after apply)
+ id = (known after apply)
+ member = "serviceAccount:viewer-infra@tenant2-dev.iam.gserviceaccount.com"
+ role = "roles/storage.objectViewer"
}
# module.gcs_bucket["eu"].google_storage_bucket_iam_member.viewer-member-bucket-external["serviceAccount:viewer-ko@tenant2-dev.iam.gserviceaccount.com"] will be created
+ resource "google_storage_bucket_iam_member" "viewer-member-bucket-external" {
+ bucket = (known after apply)
+ etag = (known after apply)
+ id = (known after apply)
+ member = "serviceAccount:viewer-ko@tenant2-dev.iam.gserviceaccount.com"
+ role = "roles/storage.objectViewer"
}
# module.gcs_bucket["eu"].google_storage_bucket_iam_member.viewer-member-bucket-internal["serviceAccount:viewer-infra@tenant1-dev.iam.gserviceaccount.com"] will be created
+ resource "google_storage_bucket_iam_member" "viewer-member-bucket-internal" {
+ bucket = (known after apply)
+ etag = (known after apply)
+ id = (known after apply)
+ member = "serviceAccount:viewer-infra@tenant1-dev.iam.gserviceaccount.com"
+ role = "roles/storage.objectViewer"
}
# module.gcs_bucket["eu"].google_storage_bucket_iam_member.viewer-member-bucket-internal["serviceAccount:viewer-ko@tenant1-dev.iam.gserviceaccount.com"] will be created
+ resource "google_storage_bucket_iam_member" "viewer-member-bucket-internal" {
+ bucket = (known after apply)
+ etag = (known after apply)
+ id = (known after apply)
+ member = "serviceAccount:viewer-ko@tenant1-dev.iam.gserviceaccount.com"
+ role = "roles/storage.objectViewer"
}
Plan: 10 to add, 0 to change, 0 to destroy.
Conforme podemos ver na saída, um bucket será criado na região da EU no projeto tenant1-dev. As permissões IAM serão atribuídas da seguinte forma:
objectAdmin para as contas de serviço platform-infra e platform-ko, tanto nos projetos tenant1-dev quanto tenant2-dev.
objectViewer para as contas de serviço viewer-infra e viewer-ko, tanto nos projetos tenant1-dev quanto tenant2-dev.
Comments