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.