Skip to main content

Create Azure Resource with Terraform

In the following guide, you are going to build a self-service action in Port that executes an Azure pipeline to deploy a storage account in Azure using Terraform templates.

Prerequisites

  • You will need your Port credentials to create the action.
  • You will need your Azure DevOps organization.
  • You will need the name of the webhook that you configured in your Azure pipelines yaml.

Example - creating a storage account

Follow these steps to get started:

  1. Create the following as variables in your Azure Devops project:

    1. Create the Port credentials with the group id port-credentials.
      1. PORT_CLIENT_ID - Port Client ID learn more.
      2. PORT_CLIENT_SECRET - Port Client Secret learn more.
    2. Create the Azure Cloud credentials with id azure-service-principal.
      tip

      Follow this guide to create a service principal in order to get the Azure credentials.

      1. ARM_CLIENT_ID - Azure Client ID (APP ID) of the application.
      2. ARM_CLIENT_SECRET - Azure Client Secret (Password) of the application.
      3. ARM_SUBSCRIPTION_ID - Azure Subscription ID.
      4. ARM_TENANT_ID - The Azure Tenant ID.
  2. Create a Port blueprint with the following JSON definition:

Port Azure Storage Account Blueprint
note

Keep in mind that this can be any blueprint you require; the provided example is just for reference.

{
"identifier": "azureStorage",
"title": "Azure Storage Account",
"icon": "Azure",
"schema": {
"properties": {
"storage_name": {
"title": "Account Name",
"type": "string",
"minLength": 3,
"maxLength": 63,
"icon": "DefaultProperty"
},
"storage_location": {
"icon": "DefaultProperty",
"title": "Location",
"type": "string"
},
"url": {
"title": "URL",
"format": "url",
"type": "string",
"icon": "DefaultProperty"
}
},
"required": [
"storage_name",
"storage_location"
]
},
"mirrorProperties": {},
"calculationProperties": {},
"relations": {}
}
  1. Create a Port action in the self-service page or with the following JSON definition:
Port Action
tip
  • <AZURE-DEVOPS-ORG> - your Azure DevOps organization name, can be found in your Azure DevOps URL: https://dev.azure.com/{AZURE-DEVOPS-ORG};
  • <AZURE-DEVOPS-WEBHOOK-NAME> - the name you gave to the webhook resource in the Azure yaml pipeline file.
{
"identifier": "azureStorage_azure_pipelines_create_azure",
"title": "Azure Pipelines Create Azure",
"icon": "Azure",
"description": "Use azure pipelines to terraform an azure resource ",
"trigger": {
"type": "self-service",
"operation": "CREATE",
"userInputs": {
"properties": {
"storage_name": {
"icon": "Azure",
"title": "Storage Name",
"description": "The Azure Storage Account",
"type": "string"
},
"storage_location": {
"title": "Storage Location",
"icon": "Azure",
"type": "string",
"default": "westus2"
}
},
"required": [
"storage_name"
],
"order": [
"storage_name",
"storage_location"
]
},
"blueprintIdentifier": "azureStorage"
},
"invocationMethod": {
"type": "AZURE_DEVOPS",
"webhook": "<AZURE-DEVOPS-WEBHOOK-NAME>",
"org": "<AZURE-DEVOPS-ORG>",
"payload": {
"{{ spreadValue() }}": "{{ .inputs }}",
"port_context": {
"entity": "{{.entity}}",
"blueprint": "{{.action.blueprint}}",
"runId": "{{.run.id}}",
"trigger": "{{ .trigger }}"
}
}
},
"requiredApproval": false,
"publish": true
}
  1. Create the following Terraform templates in a terraform folder at the root of your GitHub repository:

    tip

    Fork our example repository to get started.

    1. main.tf - This file will contain the resource blocks which define the Storage Account to be created in the Azure cloud and the entity to be created in Port.
    2. variables.tf – This file will contain the variable declarations that will be used in the resource blocks e.g. the Port credentials and Port run id.
    3. output.tf – This file will contain the URL of the Storage Account that needs to be generated on successful completion of an "apply" operation. This URL will be used in the endpoint property when creating the Port entity.
main.tf
main.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0.2"
}
port = {
source = "port-labs/port-labs"
version = "~> 2.0.3"
}
}

required_version = ">= 2.0.3"
}

provider "azurerm" {

features {}
}

provider "port" {
client_id = var.port_client_id
secret = var.port_client_secret
base_url = var.base_url
}

resource "azurerm_storage_account" "storage_account" {
name = var.storage_account_name
resource_group_name = var.resource_group_name

location = var.location
account_tier = "Standard"
account_replication_type = "LRS"
account_kind = "StorageV2"
}

resource "port_entity" "azure_storage_account" {
count = length(azurerm_storage_account.storage_account) > 0 ? 1 : 0
identifier = var.storage_account_name
title = var.storage_account_name
blueprint = "azureStorage"
run_id = var.port_run_id
properties = {
string_props = {
"storage_name" = var.storage_account_name,
"storage_location" = var.location,
"endpoint" = azurerm_storage_account.storage_account.primary_web_endpoint
}
}

depends_on = [azurerm_storage_account.storage_account]
}
variables.tf
note

Replace the default resource_group_name with a resource group from your Azure account. Check this guide to find your resource groups. You may also wish to set the default values of other variables.

variables.tf
variable "resource_group_name" {
type = string
default = "myTFResourceGroup"
description = "RG name in Azure"
}

variable "location" {
type = string
default = "westus2"
description = "RG location in Azure"
}

variable "storage_account_name" {
type = string
description = "Storage Account name in Azure"
default = "demo"
}

variable "port_run_id" {
type = string
description = "The runID of the action run that created the entity"
}

variable "port_client_id" {
type = string
description = "The Port client ID"
}

variable "port_client_secret" {
type = string
description = "The Port client secret"
}

variable "base_url" {
type = string
description = "The Port API URL"
}
Selecting a Port API URL by account region

The baseUrl, port_region, port.baseUrl, portBaseUrl, port_base_url and OCEAN__PORT__BASE_URL parameters are used to select which instance or Port API will be used.

Port exposes two API instances, one for the EU region of Port, and one for the US region of Port.

output.tf
output.tf
output "endpoint_url" {
value = azurerm_storage_account.storage_account.primary_web_endpoint
}
  1. Create an Azure pipeline:
Azure Devops pipelines script
azure-pipelines.yml
trigger: none

pool:
vmImage: "ubuntu-latest"

resources:
webhooks:
- webhook: PortWebhook
connection: PortWebhook

variables:
- group: port-credentials
- group: azure-service-principal
- name: STORAGE_NAME
value: ${{ parameters.PortWebhook.storage_name }}
- name: STORAGE_LOCATION
value: ${{ parameters.PortWebhook.storage_location }}
- name: PORT_RUN_ID
value: ${{ parameters.PortWebhook.port_context.runId }}

jobs:
- job: DeployJob
displayName: 'Deploy to Azure and Port'
steps:
- checkout: self
displayName: 'Checkout repository'

- bash: |
startedAt=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
echo "##vso[task.setvariable variable=startedAt]$startedAt"
echo "Started at $startedAt"
displayName: 'Set Start Time'

- script: |
sudo apt-get update
sudo apt-get install -y jq
displayName: Install jq

# The API call below uses the EU region. To use the US region, replace `api.getport.io` with `api.us.getport.io`
- script: |
accessToken=$(curl -X POST \
-H 'Content-Type: application/json' \
-d '{"clientId": "$(PORT_CLIENT_ID)", "clientSecret": "$(PORT_CLIENT_SECRET)"}' \
-s 'https://api.getport.io/v1/auth/access_token' | jq -r '.accessToken')
echo "##vso[task.setvariable variable=accessToken;isOutput=true]$accessToken"
displayName: 'Fetch Access Token and Run ID'
name: getToken

- bash: |
terraform init -input=false
displayName: 'Initialize configuration'
failOnStderr: true
workingDirectory: 'terraform'

- script: |
terraform validate
displayName: 'Terraform Validate'
workingDirectory: 'terraform'

- script: |
tf_plan_and_apply() {
local plan_type=$1
local target_option=""

if [ "$plan_type" == "azure" ]; then
target_option="-target=azurerm_storage_account.storage_account"
fi

terraform plan \
-input=false \
-out=tf${plan_type}-${BUILD_BUILDNUMBER}.tfplan \
-var="storage_account_name=${STORAGE_NAME}" \
-var="location=${STORAGE_LOCATION}" \
$target_option

terraform apply -auto-approve -input=false tf${plan_type}-${BUILD_BUILDNUMBER}.tfplan
}

tf_plan_and_apply azure
tf_plan_and_apply port
displayName: 'Terraform changes to Azure and Port'
workingDirectory: 'terraform'
env:
TF_VAR_resource_group_name: arete-resources
TF_VAR_port_client_id: $(PORT_CLIENT_ID)
TF_VAR_port_client_secret: $(PORT_CLIENT_SECRET)
TF_VAR_port_run_id: $(PORT_RUN_ID)

- script: |
completedAt=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
terraform_output=$(terraform output endpoint_url | sed 's/"//g')
echo ${terraform_output}

curl -X PATCH \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $(getToken.accessToken)' \
-d '{
"status": "SUCCESS",
"message": {"run_status":"Completed resource creation at $(completedAt)", "url":"$(terraform_output)" }
}' \
"https://api.getport.io/v1/actions/runs/$(PORT_RUN_ID)"
displayName: 'Update Run Status'
workingDirectory: 'terraform'
  1. Trigger the action from the self-service page of your Port application.