Atualize variáveis do Azure DevOps com Terraform
Quando você provisiona recursos de nuvem com o Terraform, é comum que eles gerem dados secretos e/ou aleatórios (como um ID, ou uma string de conexão) que você pode precisar usar posteriormente ao implantar sua aplicação. Mas normalmente a infraestrutura e a aplicação são provisionados em momentos separados - inclusive em pipelines separados - então como a gente pode pegar esses dados de infra e passar para o pipeline da aplicação?
Ao invés de digitar esses dados manualmente, veja como você pode atualizar variáveis no Azure DevOps usando o próprio Terraform.
Imagine uma situação ao mesmo tempo simples e típica: você cria uma aplicação em ASP.NET Core e quer hospedá-la no Azure. Para isso, você vai usar o Terraform para provisionar os recursos necessários, como um App Service, um banco de dados SQL e um Application Insights.
Quando o Terraform criar seus recursos, vão ser gerados dados como a string de conexão para o banco de dados e a chave de instrumentação para o Application Insights. Você vai precisar desses dados no momento em que fizer a implantação da aplicação, certo?
Tipicamente, essas informações são mantidas no Azure DevOps na forma de variáveis de pipeline, que são aplicadas no instante da publicação. Mas como esses dados podem mudar a cada novo deployment do seu ambiente, você precisaria ir manualmente no portal do Azure para obter esses dados e atualizar o valor das variáveis.
Será mesmo que não tem jeito melhor de fazer isso? Como atualizar as variáveis do Azure DevOps com esses dados gerados dinamicamente pelo Terraform?
Atualizando o Azure DevOps via Terraform
O Terraform é uma ferramenta de infraestrutura como código (IaC) que permite que você provisione recursos de nuvem de forma declarativa. Para isso, ele usa “provedores” (“providers”), que são componentes especializados que permitem que você interaja com diferentes serviços de nuvem. Por exemplo, o provider azurerm é usado para criar os diversos recursos do Azure via Terraform.
Agora, o Terraform tem também um provider para o Azure DevOps, que permite que você crie e gerencie os próprios componentes do Azure DevOps - como projetos, pipelines (builds e releases) variables groups e muito mais.
Esse módulo ainda é relativamente novo (enquanto escrevo este artigo, ele está na versão 0.10.0) e, portanto, pode não ser muito maduro. Entretanto, é possível fazer muita coisa com ele. No nosso caso, iremos usá-lo para atualizar as variáveis de um grupo de variáveis do Azure DevOps com os dados gerados pelo Terraform. Esse grupo de variáveis, por sua vez, será usado no momento da implantação da aplicação.
Para criar e manter um grupo de variáveis - e seus respectivos valores - no Azure DevOps, temos dois caminhos possíveis:
1.Criar o grupo de variáveis e seus valores diretamente no Azure DevOps. Essa é a forma “tradicional” de manter as variáveis - elas são criadas e mantidas diretamente no Azure DevOps, sem nenhuma dependência externa.
No nosso caso, vamos usar a primeira opção, que é mais simples e direta.
Criando o ambiente da aplicação
Para demonstrar como atualizar as variáveis do Azure DevOps com o Terraform, vamos criar um ambiente de aplicação ASP.NET Core no Azure. Para isso, considere o seguinte código Terraform (simplificado para fins didáticos):
# Resource Group
resource "azurerm_resource_group" "rg" {
name = "rg-demo"
location = "brazilsouth"
}
# App Service Plan
resource "azurerm_service_plan" "web" {
name = "demo-svcplan"
#...
}
# Web app
resource "azurerm_windows_web_app" "web" {
name = "demo-webapp"
service_plan_id = azurerm_service_plan.web.id
#...
}
# Log Analytics Workspace
resource "azurerm_log_analytics_workspace" "la" {
name = "demo-la"
#...
}
# Application Insights
resource "azurerm_application_insights" "ai" {
name = "demo-ai"
workspace_id = azurerm_log_analytics_workspace.la.id
#...
}
# SQL Server
resource "azurerm_mssql_server" "sql" {
name = "demo-sql"
#...
}
# SQL Database
resource "azurerm_mssql_database" "db" {
name = "demo-db"
server_name = azurerm_sql_server.sql.name
#...
}
Esse código cria um grupo de recursos no Azure, que contém um App Service Plan, um App Service, um Application Insights, um Log Analytics Workspace, um SQL Server e um SQL Database.
Quando formos fazer o deployment da nossa aplicação, vamos precisar:
Vamos ver como obter esses dados em tempo de provisionamento dos recursos.
Obtendo os dados dos recursos provisionados
Quando você provisiona no Terraform recursos que produzem dados que só serão conhecidos após o provisionamento, esses dados são tipicamente expostos na forma de atributos exportados. Por exemplo, se você provisionar Application Insights, você pode obter a chave de instrumentação usando o atributo instrumentation_key:
outputs {
# Obtém a chave de instrumentação do Application Insights e expõe como um valor de saída
ai_key = azurerm_application_insights.ai.instrumentation_key
}
Ainda que a gente possa usar o recurso de variáveis de saída do Terraform para expor esses dados, isso não é muito seguro. Afinal, esses dados são sensíveis e não deveriam ser expostos dessa forma - os valores de saída do Terraform são expostos abertamente tanto no console quanto no arquivo de state.
É por isso que a gente vai usar o provedor do Azure DevOps - para pegar esses valores e jogar diretamente numa variável secreta (criptografada) do Azure DevOps, sem precisar expor esse valor de maneira alguma. Prático e seguro!
Recomendados pelo LinkedIn
Criando o grupo de variáveis no Azure DevOps via Terraform
Para criar o grupo de variáveis no Azure DevOps, vamos usar o recurso azuredevops_variable_group do provedor do Azure DevOps. Esse recurso permite que você crie e gerencie grupos de variáveis no Azure DevOps.
No nosso exemplo, assumindo que queremos criar um grupo de variáveis chamado demo-variables num team project pré-existente chamado demo-project, podemos usar o seguinte código:
data "azuredevops_project" "tp" {
name = "demo-project"
}
resource "azuredevops_variable_group" "vg" {
project_id = data.azuredevops_project.tp.id
name = "demo-variables"
allow_access = true
variable {
name = "webapp_name"
value = azurerm_windows_web_app.web.name
}
variable {
name = "svcplan_name"
value = azurerm_service_plan.web.name
}
variable {
name = "ai_key"
secret_value = azurerm_application_insights.ai.instrumentation_key
is_secret = true
}
variable {
name = "ai_conn_string"
secret_value = azurerm_application_insights.ai.connection_string
is_secret = true
}
variable {
name = "db_conn_string"
secret_value = "Data Source=${azurerm_mssql_server.sql.fully_qualified_domain_name};Initial Catalog=${azurerm_mssql_database.db.name};User Id=${azurerm_mssql_database.db.administrator_login}@${azurerm_mssql_server.sql.name};Password=${azurerm_mssql_database.db.administrator_login_password};"
is_secret = true
}
}
Repare que criamos variáveis normais (não-secretas) chamadas webapp_name e svcplan_name, que contém o nome do App Service e do App Service Plan, respectivamente. E, além disso, criamos também as variáveis secretas para guardar a chave de instrumentação do Application Insights (ai_key) e sua string de conexão (ai_conn_string), bem como a string de conexão para o banco de dados (db_conn_string).
Montando a string de conexão do SQL Server
A string de conexão do SQL Server merece uma menção à parte. Isso porque o Azure SQL não oferece um atributo que nos dê a string de conexão pronta - diferentemente do que acontece com o Application Insights. Entre outras coisas, porque a string de conexão depende da linguagem de programação e do framework de acesso a dados que você está usando.
É por isso que, no exemplo anterior, montamos uma string de conexão no formato esperado pelo ASP.NET Core, aplicando os atributos fully_qualified_domain_name, administrator_login e administrator_login_password do SQL Server, e o atributo name do SQL Database.
DICA: Uma dica legal com relação à senha do administrador do SQL Server: ao invés de “chumbar” um valor fixo, você pode usar o recurso random_password do Terraform para gerar uma senha aleatória. E como a senha passa a ser aleatória - ou seja, muda a cada novo deployment - a gente salva essa senha como parte da connection string que foi colocada na variável db_conn_string do Azure DevOps. E ninguém precisa saber qual é a senha que foi gerada! Coloque o trecho de código abaixo para gerar uma senha aleatória para seu SQL Server:
# SQL Server
resource "random_password" "db" {
length = 16
special = true
}
resource "azurerm_mssql_server" "db" {
name = "demo-sql"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = random_password.db.result
}
No nosso exemplo escolhemos usar um nome de usuário hardcoded, mas mesmo o nome de usuário poderia ter sido gerado dinamicamente usando os recursos random_string, random_id, random_uuid ou mesmo o random_pet do Terraform.
Usando as variáveis no pipeline
Quando você rodar o seu Terraform, o resultado no Azure DevOps deve ser algo assim:
Para usar esse grupo de variáveis numa pipeline, basta seguir o procedimento típico:
variables:
- group: demo-variables
Pronto! Agora você pode usar as variáveis normalmente no seu pipeline, como se elas tivessem sido criadas manualmente no Azure DevOps.
Toda vez que sua infraestrutura for alterada, o Terraform irá atualizar os dados das variáveis. Daí, é só fazer um novo deployment da aplicação para pegar os valores atualizados!
DICA: Você sabia que dá para encadear pipelines? Com isso, é possível rodar a pipeline de build/release da aplicação toda vez que a infraestrutura for atualizada! Para saber mais, veja na documentação oficial como criar um gatilho para um recurso do tipo Pipeline.
Conclusão
Neste artigo, vimos como usar o provedor do Azure DevOps para atualizar variáveis do Azure DevOps com o Terraform. Com isso, você pode usar o Terraform para provisionar recursos de nuvem e, ao mesmo tempo, atualizar as variáveis do Azure DevOps com os dados gerados dinamicamente por esses recursos.
Isso é especialmente útil quando você precisa usar esses dados no momento do deployment da aplicação, como é o caso da string de conexão para o banco de dados ou a chave de instrumentação para o Application Insights.
Espero que este artigo tenha sido útil para você. Até a próxima!
Um abraço, Igor