Como usar o az-cli num local-exec do Terraform no Azure Pipelines

Se você já tentou usar a CLI do Azure num local-exec do Terraform a partir das tasks de Terraform no Azure Pipelines, deve ter se deparado com um erro indicando que era preciso fazer login antes de usar a CLI. Neste post, vou mostrar como fazer isso.

Existem algumas poucas circunstâncias onde o provedor (azurerm) do Azure para o Terraform não atende às nossas necessidades. Nesses casos, uma alternativa é fazer uma chamada direta à linha de comando do Azure (a famosa az-cli) a partir de um local-exec do Terraform.

O problema é que, se você tentar fazer isso a partir de uma task de Terraform no Azure Pipelines, vai se deparar com um erro indicando que é preciso fazer login antes de usar a CLI.

TL;DR: para usar a CLI do Azure num local-exec a partir de uma task do Azure Pipelines, é preciso incluir uma chamada ao comando az login no script do local-exec. Continua lendo que vou te mostrar como faz.

Nosso exemplo

Para entendermos o problema, considere este script Terraform bem simples. Eu coloquei uma chamada ao az-cli pedindo apenas para ele listar os grupos de recursos da minha assinatura. Imagine que, aqui, você poderia colocar qualquer comando do az-cli para resolver seu problema específico.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
terraform {
  required_version = ">= 1.4.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.59.0"
    }
  }
  backend "azurerm" {}
}

provider "azurerm" {
  features {}
}

resource "null_resource" "demo" {
  provisioner "local-exec" {
    command = "az group list"
  }
}

Vou chamar esse meu script a partir de um pipeline do Azure DevOps também bem simples, configurado da seguinte forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pool:
  vmImage: 'ubuntu-latest'

steps:
- task: TerraformInstaller@0
  inputs:
    terraformVersion: '1.4.2'

- task: TerraformTaskV4@4
  displayName: 'Terraform init'
  inputs:
    provider: 'azurerm'
    command: 'init'
    backendServiceArm: '...'
    # ...

- task: TerraformTaskV4@4
  displayName: 'Terraform plan'
  inputs:
    provider: 'azurerm'
    command: 'plan'
    commandOptions: '-input=false -out tf.plan'
    environmentServiceNameAzureRM: '...'
    

- task: TerraformTaskV4@4
  displayName: 'Terraform apply'
  inputs:
    provider: 'azurerm'
    command: 'apply'
    environmentServiceNameAzureRM: '...'

Aqui temos duas service connections diferentes: uma para acessar a storage account com o arquivo de estado do Terraform (usada na task de Init) e outra para o provisionamento do ambiente em si (usada nas tasks de Plan e Apply).

O problema

Quando executado, esse pipeline falha na task de Apply com o seguinte erro:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
null_resource.demo: Creating...
null_resource.demo: Provisioning with 'local-exec'...
null_resource.demo (local-exec): Executing: ["/bin/sh" "-c" "az group list"]
null_resource.demo (local-exec): ERROR: Please run 'az login' to setup account.
╷
│ Error: local-exec provisioner error
│ 
│   with null_resource.demo,
│   on main.tf line 29, in resource "null_resource" "demo":
│   29:   provisioner "local-exec" {
│ 
│ Error running command 'az group list': exit status 1. Output: ERROR: Please
│ run 'az login' to setup account.
│ 
╵

Isso acontece porque a task do Terraform, apesar de estar conectada ao Azure através da service connection configurada, não repassa esse login para o contexto do local-exec. O que precisamos fazer é, de alguma forma, tentar acessar os dados da service connection (que já está autenticada) e usá-los para fazer o login no az-cli.

A solução

Ao examinar o código-fonte da task de Terraform, reparei que ela extrai os dados de autenticação da service connection e repassa para o Terraform através de variáveis de ambiente (as chamadas a process.env no código abaixo):

1
2
3
4
5
6
7
8
9
10
11
12
var serviceprincipalid = tasks.getEndpointAuthorizationParameter(command.serviceProvidername, "serviceprincipalid", true);
var serviceprincipalkey = tasks.getEndpointAuthorizationParameter(command.serviceProvidername, "serviceprincipalkey", true);

process.env['ARM_SUBSCRIPTION_ID']  = tasks.getEndpointDataParameter(command.serviceProvidername, "subscriptionid", false);
process.env['ARM_TENANT_ID']        = tasks.getEndpointAuthorizationParameter(command.serviceProvidername, "tenantid", false);

if(serviceprincipalid && serviceprincipalkey) {
    process.env['ARM_CLIENT_ID']        = tasks.getEndpointAuthorizationParameter(command.serviceProvidername, "serviceprincipalid", true);
    process.env['ARM_CLIENT_SECRET']    = tasks.getEndpointAuthorizationParameter(command.serviceProvidername, "serviceprincipalkey", true); 
} else {
    process.env['ARM_USE_MSI'] = 'true';
}

Ou seja, se eu conseguir acessar essas variáveis de ambiente a partir do meu local-exec, posso usá-las para fazer o login no az-cli.

Para isso, vou usar o comando az login com a opção --service-principal e passar os valores das variáveis de ambiente ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID e ARM_SUBSCRIPTION_ID como parâmetros:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
resource "null_resource" "demo" {
  provisioner "local-exec" {
    command = <<-EOT
      # Efetua o login no Azure
      if [ -n "$ARM_CLIENT_ID" ] && [ -n "$ARM_CLIENT_SECRET" ]; then
        az login --service-principal -u "$ARM_CLIENT_ID" -p "$ARM_CLIENT_SECRET" -t "$ARM_TENANT_ID"
      else
        az login --identity
      fi
      az account set --subscription "$ARM_SUBSCRIPTION_ID"

      # Executa o(s) comando(s) desejado(s)
      az group list

    EOT
  }
}

Seguindo a lógica da task, estamos verificando se há um client ID e um client secret definidos na service connection. Se sim, fazemos o login do service principal usando esses dados; caso contrário, assumimos que está sendo usada uma managed identity e fazemos o login usando o comando az login --identity.

DICA: Para não precisar digitar sempre todos esses comandos, você pode encapsular essa lógica de login num shellscript e chamá-lo a partir do seu local-exec. Para isso, crie um arquivo chamado azcli-login.sh na mesma pasta do seu arquivo main.tf (não se esqueça do chmod +x antes do commit e push) e mova os comandos para lá:

1
2
3
4
5
6
if [ -n "$ARM_CLIENT_ID" ] && [ -n "$ARM_CLIENT_SECRET" ]; then
  az login --service-principal -u "$ARM_CLIENT_ID" -p "$ARM_CLIENT_SECRET" -t "$ARM_TENANT_ID"
else
  az login --identity
fi
az account set --subscription "$ARM_SUBSCRIPTION_ID"

Agora, no seu script Terraform, é só chamar o shellscript:

1
2
3
4
5
6
7
8
9
10
11
12
resource "null_resource" "demo" {
  provisioner "local-exec" {
    command = <<-EOT
      # Efetua o login no Azure
      ./azcli-login.sh

      # Executa o(s) comando(s) desejado(s)
      az group list

    EOT
  }
}

Conclusão

Com essa solução, conseguimos fazer o login no az-cli dentro de um local-exec, reaproveitando o contexto de login da nossa task do Terraform no Azure DevOps. Isso nos permite usar o az-cli para contornar algumas limitações do provedor de Terraform para o Azure.

O que achou? Tem alguma dúvida ou sugestão? Deixe um comentário abaixo!

Um abraço,
Igor



07/06/2023 | Por Igor Abade V. Leite | Em Técnico | Tempo de leitura: 3 mins.

Postagens relacionadas