Ir para o conteúdo

Preparar Windows Azure para Terraform

O Terraform sempre que aplica alguma alteração, salva esse estado num "backend" (ler mais sobre Terraform Backends), no nosso caso vamos configurar o Terraform para salvar os estados dentro do próprio Azure.

Documentação sobre Terraform Backend no Azure: https://developer.hashicorp.com/terraform/language/backend/azurerm

Para começar temos que configurar o Azure para aceitar operações vindas do Terraform, salvar os estados do Terraform e ter permissões para gerir os restantes recursos.

Comecei por criar o projecto Azure_and_Terraform_Initial_Setup onde existe um script create-storage-and-service-principal-for-terraform.sh que apenas lê como parametro qual é a Subscription ID, e cria:

  • Uma "Azure Storage Account" para salvar os "Terraform States" (Uma conta por ambiente de desenvolvimento)
  • Uma "App registrations" (por cada "environment") dentro do "Main Active Directory"
create-storage-and-service-principal-for-terraform.sh
#!/bin/bash

set -euo pipefail
trap 'echo "Error on line $LINENO"' ERR
echo ""

# Verify if the user is logged in
if ! az account show >/dev/null 2>&1; then
    echo "❌ You are not authenticated, please first execute 'az login'."
    exit 1
fi

# shellcheck disable=SC2034
declare -r AZURE_SUBSCRIPTION_PROD="49989ef6-876a-41d7-86ac-8c68cabc1199" # ID  # TODO(Rui): Hide this ID
# shellcheck disable=SC2034
declare -r AZURE_SUBSCRIPTION_STAGING="49989ef6-876a-41d7-86ac-8c68cabc1199"  # ID  # TODO(Rui): Hide this ID
# shellcheck disable=SC2034
declare -r AZURE_SUBSCRIPTION_DEV="49989ef6-876a-41d7-86ac-8c68cabc1199"  # ID  # TODO(Rui): Hide this ID

# If you delete someone from list, you need to delete manually from Azure:
declare -r ENV_LIST=("dEv" "StAgIng" "pRod")  # Case Insensitive

declare -r AZURE_LOCATION="westeurope"

echo "╔═════════════════════════════════════════════════════════════════════════════╗"

# Create resources and store resources info/keys in environment variables (for each environment [dev, prod, ...])
for ENV in "${ENV_LIST[@]}"
do
  # Convert ENV_LIST to upper and lower case variables:
  ENV=$(echo "${ENV}" | tr '[:upper:]' '[:lower:]')        # Convert ENV to lowercase
  ENV_UPCASE=$(echo "${ENV}" | tr '[:lower:]' '[:upper:]') # Convert ENV to uppercase

  eval AZURE_SUBSCRIPTION='$'"AZURE_SUBSCRIPTION_${ENV_UPCASE}"

  if ! az account show --subscription "${AZURE_SUBSCRIPTION}" &>/dev/null; then
    echo "Error: Subscription ${AZURE_SUBSCRIPTION} not found."
    echo "Available subscriptions:"
    az account list --output table
    exit 1
  fi
  az account set --subscription "${AZURE_SUBSCRIPTION}"

  echo " Environment: ${ENV}"
  echo " Using subscription ID:   '${AZURE_SUBSCRIPTION}'"
  echo " Using subscription NAME: '$(az account show --subscription "${AZURE_SUBSCRIPTION}" | jq -r '.name')'"
  echo ""
  sleep 5s

  az account set --subscription "${AZURE_SUBSCRIPTION}"

  echo "╠────────────────────────── TERRAFORM STATE STORAGE ──────────────────────────╣"

  RESOURCE_GROUP_NAME="terraform-state-${ENV}"
  # STORAGE_ACCOUNT_NAME="primetagterraform${ENV}"
  STORAGE_ACCOUNT_NAME="ptagterraform${ENV}"
  CONTAINER_NAME="terraform-state-${ENV}"

  # No problem if Resource Group already exists, it will return also 'Succeeded' status.
  echo "Creating Resource Group '${RESOURCE_GROUP_NAME}', in location '${AZURE_LOCATION}'..."
  az group create --name "${RESOURCE_GROUP_NAME}" --location "${AZURE_LOCATION}" --subscription "${AZURE_SUBSCRIPTION}"
  eval "RESOURCE_GROUP_NAME_${ENV_UPCASE}=${RESOURCE_GROUP_NAME}"

  # No problem if Storage Account already exists inside our subscription, it will return also 'Succeeded' status,
  # and the ACCOUNT_KEY will be the same.
  echo "Creating Storage Account '${STORAGE_ACCOUNT_NAME}'..."
  az storage account create --name "${STORAGE_ACCOUNT_NAME}" --resource-group "${RESOURCE_GROUP_NAME}" \
    --sku Standard_LRS --encryption-services blob --subscription "${AZURE_SUBSCRIPTION}"
  eval "STORAGE_ACCOUNT_NAME_${ENV_UPCASE}=${STORAGE_ACCOUNT_NAME}"

  echo "Getting storage account key..."
  ACCOUNT_KEY=$(az storage account keys list --resource-group "${RESOURCE_GROUP_NAME}" \
                --account-name "${STORAGE_ACCOUNT_NAME}" --subscription "${AZURE_SUBSCRIPTION}" \
                --query "[0].value" -o tsv)
  eval "ACCOUNT_KEY_${ENV_UPCASE}=${ACCOUNT_KEY}"

  # If "Storage Container" already exists, it will return success,
  # but it will return a JSON with value: "created": false
  echo "Creating Storage Container '${CONTAINER_NAME}'..."
  az storage container create --name "${CONTAINER_NAME}" --account-name "${STORAGE_ACCOUNT_NAME}" \
    --account-key "${ACCOUNT_KEY}" --subscription "${AZURE_SUBSCRIPTION}"
  eval "CONTAINER_NAME_${ENV_UPCASE}=${CONTAINER_NAME}"

  echo "╠────────────────────────────── APP PERMISSIONS ──────────────────────────────╣"

  APP_NAME="terraform-${ENV}"

  echo "Creating Azure Active Directory service principals for automation authentication '${APP_NAME}'..."
  # JSON_CREDENTIAL=$(az ad sp create-for-rbac --name "${APP_NAME}" --years 100)
  JSON_CREDENTIAL=$(az ad sp create-for-rbac --name "${APP_NAME}" --skip-assignment --years 100)
  eval "CREDENTIAL_APP_ID_${ENV_UPCASE}=$(echo "${JSON_CREDENTIAL}"   | jq --raw-output '.appId')"
  eval "CREDENTIAL_PASSWORD_${ENV_UPCASE}=$(echo "${JSON_CREDENTIAL}" | jq --raw-output '.password')"
  eval "CREDENTIAL_TENANT_${ENV_UPCASE}=$(echo "${JSON_CREDENTIAL}"   | jq --raw-output '.tenant')"

  eval APP_ID='$'"CREDENTIAL_APP_ID_${ENV_UPCASE}"

  : """
  If you don't have permissions to assign roles, the Add role assignment option will be disabled.
  To add or remove role assignments, you must have:
  Microsoft.Authorization/roleAssignments/write
  Microsoft.Authorization/roleAssignments/delete permissions, such as User Access Administrator or Owner
  Ensure you have these permissions.
  """
  # Roles: https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#contributor
  echo "Assigning roles for app with NAME '${APP_NAME}'..."
  # az role assignment create --assignee "${APP_ID}" --role "Owner"  # Owner is the most powerful role, should we use it? Or is too risky?
  az role assignment create --assignee "${APP_ID}" --scope "/subscriptions/${AZURE_SUBSCRIPTION}/resourceGroups/${RESOURCE_GROUP_NAME}" --role "Contributor"
  az role assignment create --assignee "${APP_ID}" --scope "/subscriptions/${AZURE_SUBSCRIPTION}/resourceGroups/${RESOURCE_GROUP_NAME}" --role "Network Contributor"
  # az role assignment create --assignee "${APP_ID}" --scope "/subscriptions/${AZURE_SUBSCRIPTION}/resourceGroups/${RESOURCE_GROUP_NAME}" --role "User Access Administrator"  # To allow "azurerm_management_lock"
  az role assignment create --assignee "${APP_ID}" --scope "/subscriptions/${AZURE_SUBSCRIPTION}" --role "User Access Administrator"  # To allow "azurerm_management_lock" outside the terraform state resource group (To avoid this kind of errors: https://gitlab.com/primetag/infrastructure/databases-postgresql-9/-/jobs/11012316267 )
  # az role assignment create --assignee 00000000-0000-0000-0000-000000000000 --role "Storage Account Key Operator Service Role" --scope $id

  echo ""
done
echo "╚═════════════════════════════════════════════════════════════════════════════╝"

echo ""
echo ""

# Print resources info/keys for each environment
echo "╔═════════════════════════════════════════════════════════════════════════════╗"
for ENV in "${ENV_LIST[@]}"
do
  ENV=$(echo "${ENV}" | tr '[:upper:]' '[:lower:]')        # Convert ENV to lowercase
  ENV_UPCASE=$(echo "${ENV}" | tr '[:lower:]' '[:upper:]') # Convert ENV to uppercase

  eval AZURE_SUBSCRIPTION='$'"AZURE_SUBSCRIPTION_${ENV_UPCASE}"
  eval ACCOUNT_KEY='$'"ACCOUNT_KEY_${ENV_UPCASE}"
  eval RESOURCE_GROUP_NAME='$'"RESOURCE_GROUP_NAME_${ENV_UPCASE}"
  eval STORAGE_ACCOUNT_NAME='$'"STORAGE_ACCOUNT_NAME_${ENV_UPCASE}"
  eval CONTAINER_NAME='$'"CONTAINER_NAME_${ENV_UPCASE}"

  eval CREDENTIAL_APP_ID='$'"CREDENTIAL_APP_ID_${ENV_UPCASE}"
  eval CREDENTIAL_PASSWORD='$'"CREDENTIAL_PASSWORD_${ENV_UPCASE}"
  eval CREDENTIAL_TENANT='$'"CREDENTIAL_TENANT_${ENV_UPCASE}"

  echo "ENVIRONMENT: ${ENV}"
  echo "AZURE_SUBSCRIPTION_${ENV_UPCASE}: ${AZURE_SUBSCRIPTION}"
  echo "                         ""$(az account show --subscription "${AZURE_SUBSCRIPTION}" | jq -r '.name')"
  echo "# Storage:"
  echo "RESOURCE_GROUP_NAME_${ENV_UPCASE}: ${RESOURCE_GROUP_NAME}"
  echo "STORAGE_ACCOUNT_NAME_${ENV_UPCASE}: ${STORAGE_ACCOUNT_NAME}"
  echo "CONTAINER_NAME_${ENV_UPCASE}: ${CONTAINER_NAME}"
  echo "ACCOUNT_KEY_${ENV_UPCASE}: ${ACCOUNT_KEY}"
  echo "# Service Principal:"
  echo "CREDENTIAL_APP_ID_${ENV_UPCASE}: ${CREDENTIAL_APP_ID}"
  echo "CREDENTIAL_PASSWORD_${ENV_UPCASE}: ${CREDENTIAL_PASSWORD} [🚨 PROTECT THIS SECRET]"
  echo "CREDENTIAL_TENANT_${ENV_UPCASE}: ${CREDENTIAL_TENANT}"
  echo ""
done
echo "╚═════════════════════════════════════════════════════════════════════════════╝"

az-login.gif

az-configure-terraform.gif

Em caso de erro SubscriptionNotFound

Se surgir a seguinte mensagem de erro:

(SubscriptionNotFound) Subscription xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx was not found.
Code: SubscriptionNotFound
Message: Subscription xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx was not found.

É preciso ir à subscrição, "settings", "Resource providers" e ativar o recurso Microsoft.Storage EnableAzureStorage.png

O script no final imprime os secrets que devem ser salvos:

╔═════════════════════════════════════════════════════════════════════════════╗
ENVIRONMENT: dev
AZURE_SUBSCRIPTION_DEV: 49989ef6-876a-41d7-86ac-8c68cabc1199
                         Azure subscription 1
# Storage:
RESOURCE_GROUP_NAME_DEV: terraform-state-dev
STORAGE_ACCOUNT_NAME_DEV: ptagterraformdev
CONTAINER_NAME_DEV: terraform-state-dev
ACCOUNT_KEY_DEV: dvJCApXtvJvut3vGKozxtzIrx0ntUMVCN6omw+/KAFdkiFu2FTBP61CzHbLqeaYYzEX0vdrbsrJD+AStUGyODA==
# Service Principal:
CREDENTIAL_APP_ID_DEV: d0f26b5a-2476-46f2-92fa-b860ca759231
CREDENTIAL_PASSWORD_DEV: bOb8Q~HNWfGbIPOszZxkQL8i6PpdPx_QyGPTJbmi [🚨 PROTECT THIS SECRET]
CREDENTIAL_TENANT_DEV: b583bbfe-2844-4fd1-90bc-811774b2da82

ENVIRONMENT: staging
AZURE_SUBSCRIPTION_STAGING: 49989ef6-876a-41d7-86ac-8c68cabc1199
                         Azure subscription 1
# Storage:
RESOURCE_GROUP_NAME_STAGING: terraform-state-staging
STORAGE_ACCOUNT_NAME_STAGING: ptagterraformstaging
CONTAINER_NAME_STAGING: terraform-state-staging
ACCOUNT_KEY_STAGING: H6gA5/l2PkuWprLiCF66nMiHWG9OLIH6Mt/4GritcPsJANuAiDWOO5/jv5Skd52C1dIh5oAktLkY+AStbhqGMA==
# Service Principal:
CREDENTIAL_APP_ID_STAGING: 217ce306-a83e-41c4-9b28-f2d3848dbb41
CREDENTIAL_PASSWORD_STAGING: fA_8Q~gbCg~cZhqPzI1gSgrsxV~BdIIeevk~4cTa [🚨 PROTECT THIS SECRET]
CREDENTIAL_TENANT_STAGING: b583bbfe-2844-4fd1-90bc-811774b2da82

ENVIRONMENT: prod
AZURE_SUBSCRIPTION_PROD: 49989ef6-876a-41d7-86ac-8c68cabc1199
                         Azure subscription 1
# Storage:
RESOURCE_GROUP_NAME_PROD: terraform-state-prod
STORAGE_ACCOUNT_NAME_PROD: ptagterraformprod
CONTAINER_NAME_PROD: terraform-state-prod
ACCOUNT_KEY_PROD: rihATlim+xtqqlFJAApIfX868D0DddggJnDsJOnPL53k2d0esyZe99BAjshYkAYP7UvnuNqiMgPG+ASteiUmFQ==
# Service Principal:
CREDENTIAL_APP_ID_PROD: 74140999-7e3f-47b2-a166-7ed14b4f594b
CREDENTIAL_PASSWORD_PROD: HZ68Q~KzpnEUZVhgb6jhKyQr4Td1SIKmWDNIObkI [🚨 PROTECT THIS SECRET]
CREDENTIAL_TENANT_PROD: b583bbfe-2844-4fd1-90bc-811774b2da82

╚═════════════════════════════════════════════════════════════════════════════╝
(Convem automatizares o processo de rotação destes secrets que estão dentro das Registered Apps)

Após executar o script teremos:

  • As app registrations: Serão usadas para permitir que o Terraform controle o Azure az-app-registrations.png

  • Os resource groups para o terraform, com os respectivos service accounts dentro: Será dentro destas service accounts que o Azure salvará os "states" que for aplicando az-resource-groups.png az-service-accounts.png

Este Azure_and_Terraform_Initial_Setup contem outros ficheiros de configuração, mas não são relevantes neste momento, serão discutidos num proximo capitulo