Deploy Azure Resource using Terraform
In the following guide, you are going to create a self-service action in Port that executes a GitHub workflow to deploy a storage account in Azure using Terraform templates.
- Standardized Deployments: Ensure consistency and minimize errors by defining your storage account infrastructure as code using Terraform templates.
- Streamlined Provisioning: Rapidly create new storage accounts on-demand, empowering users without requiring deep Azure cloud expertise.
Prerequisites
- Azure Subscription: An active Azure subscription is required to deploy the storage account.
- Port Actions Knowledge: Understanding how to create and use Port actions is necessary. Learn the basics here.
- GitHub Repository: A repository to store your GitHub workflow file for this action.
GitHub Secrets
To successfully execute this workflow, we will add the following secrets to the GitHub repository containing the workflow:
1. GitHub Action Secrets
- Navigate to your GitHub repository's "Settings" tab.
- Select "Secrets" and then "Actions" from the side menu.
- Create the following secrets:
PORT_CLIENT_ID
: Your Port Client ID learn more.PORT_CLIENT_SECRET
: Your Port Client Secret learn more.
2. Azure Cloud Credentials
For secure Azure interactions, we'll use a Service Principal. If you need help creating one, follow this guide
- Once you have your Service Principal, create these GitHub Action secrets:
ARM_CLIENT_ID
: Service Principal Application (Client) IDARM_CLIENT_SECRET
: Service Principal PasswordARM_SUBSCRIPTION_ID
: Your Azure Subscription IDARM_TENANT_ID
: Your Azure Tenant IDAZURE_RESOURCE_GROUP
; Your Azure resource group
Port Configuration
Import Azure resources into your Port account using the Azure Exporter
- Create the
azureStorage
blueprint.- Head to the Builder page.
- Click on the
+ Blueprint
button. - Click on the
{...} Edit JSON
button. - Copy and paste the following JSON configuration into the editor.
Port Blueprint: Azure Storage Account
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": {}
}
- To create the Port action:
- Head to the self-service page.
- Click on the
+ New Action
button. - Click on the
{...} Edit JSON
button. - Copy and paste the following JSON configuration into the editor.
Port Action: Create Azure Storage
<GITHUB-ORG>
- your GitHub organization or user name.<GITHUB-REPO-NAME>
- your GitHub repository name.
{
"identifier": "service_create_azure_storage",
"title": "Create Azure Storage",
"icon": "Github",
"description": "Execute a workflow that terraforms an azure resource",
"trigger": {
"type": "self-service",
"operation": "CREATE",
"userInputs": {
"properties": {
"storage_name": {
"title": "Storage Name",
"icon": "Azure",
"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": "GITHUB",
"org": "<GITHUB-ORG>",
"repo": "<GITHUB-REPO-NAME>",
"workflow": "terraform-azure.yml",
"workflowInputs": {
"storage_name": "{{ .inputs.\"storage_name\" }}",
"storage_location": "{{ .inputs.\"storage_location\" }}",
"port_context": {
"entity": "{{ .entity }}",
"blueprint": "{{ .action.blueprint }}",
"runId": "{{ .run.id }}",
"trigger": "{{ .trigger }}"
}
},
"reportWorkflowStatus": true
},
"requiredApproval": false,
"publish": true
}
GitHub Workflow
-
Create the following Terraform templates in a
terraform
folder at the root of your GitHub repository:tipFork our example repository to get started.
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.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.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 theendpoint
property when creating the Port entity.
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 = ">= 1.1.0"
}
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
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.
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"
}
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.
- If you use the EU region of Port (https://app.getport.io), your API URL is
https://api.getport.io
. - If you use the US region of Port (https://app.us.getport.io), your API URL is
https://api.us.getport.io
.
output.tf
output "endpoint_url" {
value = azurerm_storage_account.storage_account.primary_web_endpoint
}
- Create a workflow file under
.github/workflows/terraform-azure.yml
with the following content:
GitHub workflow script
name: "Deploy Azure Resource"
on:
workflow_dispatch:
inputs:
storage_name:
required: true
type: string
storage_location:
required: true
type: string
port_context:
required: true
description: >-
Action and general context (blueprint, run id, etc...)
env:
TF_LOG: INFO
TF_INPUT: false
jobs:
terraform:
name: "Terraform Infrastructure Change Management"
runs-on: ubuntu-latest
defaults:
run:
shell: bash
# We keep Terraform files in the terraform directory.
working-directory: ./terraform
# working-directory: ./
steps:
- name: Checkout the repository to the runner
uses: actions/checkout@v4
- name: Setup Terraform with specified version on the runner
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.6.0
- name: Terraform init
id: init
run: terraform init
# run: terraform init -backend-config="bucket=$BUCKET_TF_STATE" # Use this option instead if using a storage backend
- name: Terraform format
id: fmt
run: terraform fmt -check
- name: Terraform validate
id: validate
run: terraform validate
- name: Run Terraform Plan and Apply (Azure)
id: plan-azure
env:
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
TF_VAR_port_client_id: ${{ secrets.PORT_CLIENT_ID }}
TF_VAR_port_client_secret: ${{ secrets.PORT_CLIENT_SECRET }}
TF_VAR_port_run_id: ${{ fromJson(inputs.port_context).runId }}
TF_VAR_resource_group_name: ${{ secrets.AZURE_RESOURCE_GROUP }}
run: |
terraform plan \
-input=false \
-out=tfazure-${GITHUB_RUN_NUMBER}.tfplan \
-var="storage_account_name=${{ github.event.inputs.storage_name }}" \
-var="location=${{ github.event.inputs.storage_location }}" \
-target=azurerm_storage_account.storage_account
terraform apply -auto-approve -input=false tfazure-${GITHUB_RUN_NUMBER}.tfplan
- name: Terraform Azure Status
if: steps.plan-azure.outcome == 'failure'
run: exit 1
- name: Run Terraform Plan and Apply (Port)
id: plan-port
env:
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
TF_VAR_port_client_id: ${{ secrets.PORT_CLIENT_ID }}
TF_VAR_port_client_secret: ${{ secrets.PORT_CLIENT_SECRET }}
TF_VAR_port_run_id: ${{fromJson(inputs.port_context).runId}}
TF_VAR_resource_group_name: arete-resources
run: |
terraform plan \
-input=false \
-out=tfport-${GITHUB_RUN_NUMBER}.tfplan \
-var="storage_account_name=${{ github.event.inputs.storage_name }}" \
-var="location=${{ github.event.inputs.storage_location }}"
terraform apply -auto-approve -input=false tfport-${GITHUB_RUN_NUMBER}.tfplan
- name: Terraform Port Status
if: steps.plan-port.outcome == 'failure'
run: exit 1
- name: Create a log message
uses: port-labs/port-github-action@v1
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
baseUrl: https://api.getport.io
operation: PATCH_RUN
status: "SUCCESS"
runId: ${{fromJson(inputs.port_context).runId}}
logMessage: Created ${{ inputs.storage_name }}
Let's Test It!
- On the self-service page, select the action and fill in the properties.
- Click the execute button to trigger the GitHub workflow.