GitHub Actions for Azure and Power Platform VNet Integration Using Terraform
This is a sample repository that shows how to use GitHub Actions workflows (CI/CD) to manage Azure infrastructure and Power Platform Virtual Network support with Terraform.
Note
- Power Platform and the Azure subscription must be in the same Microsoft Entra tenant.
- The single VNet scenario is only supported in specific regions listed under the Supported regions documentation.
- In most cases, you will need to deploy multiple VNets.
- A subnet can only be registered to one enterprise policy (cannot be shared across policies). For single-region geographies, one enterprise policy links to one subnet; for two-region geographies, one enterprise policy links to one subnet per region (primary + secondary). Multiple Power Platform environments can share the same enterprise policy and subnet — size each subnet accordingly (~25–30 IPs per production environment, ~6–10 per nonproduction environment).
Subnet(s) <- 1:1 -> Enterprise Policy <- 1:N -> PPF Environments
- Terraform Azure Provider
- Azure Container Instances Documentation
- Set up virtual network support for Power Platform
- GitHub Actions Documentation
flowchart TD
classDef github fill:#24292e,stroke:#1b1f23,color:#fff,stroke-width:2px
classDef azure fill:#f0f6ff,stroke:#0078d4,color:#000,stroke-width:2px
classDef network fill:#e1f5fe,stroke:#01579b,color:#01579b,stroke-width:1px,stroke-dasharray: 5 5
classDef storage fill:#fff9c4,stroke:#fbc02d,color:#000
classDef compute fill:#e1f5fe,stroke:#0288d1,color:#000
classDef powerplatform fill:#fff3e0,stroke:#e65100,color:#000
classDef rbac fill:#ffebee,stroke:#c62828,color:#c62828,stroke-dasharray: 3 3
subgraph GH ["GitHub Actions"]
direction TB
PR["Pull Request<br/>• Unit Tests<br/>• Terraform Plan"]
PUSH["Push to Main<br/>• Terraform Apply<br/>• Setup VNet PS Script"]
end
class GH github
subgraph RG ["Azure Resource Group (azppf-rg)"]
direction TB
subgraph VNET ["Virtual Network — primary (eastus) azppf-vnet"]
direction LR
NSG(["NSG: azppf-nsg"])
PPFSUB["Subnet: ppf-subnet<br/>10.123.1.0/24<br/>Delegation: PowerPlatform"]
FNSUB["Subnet: fn-subnet<br/>10.123.2.0/24<br/>Delegation: ContainerInstance"]
NSG --> PPFSUB
NSG --> FNSUB
end
class VNET network
subgraph VNET2 ["Virtual Network — secondary (westus) azppf-vnet-secondary"]
direction LR
NSG2(["NSG: azppf-nsg-secondary"])
PPFSUB2["Subnet: ppf-subnet<br/>10.124.1.0/24<br/>Delegation: PowerPlatform"]
NSG2 --> PPFSUB2
end
class VNET2 network
subgraph ACI ["Azure Container Instance (azppf-echo-cg)"]
direction TB
ECHO["Container: mendhak/http-https-echo<br/>0.5 vCPU / 0.5 GB — private IP only<br/>Port 8080"]
end
class ACI compute
end
class RG azure
subgraph PPF ["Power Platform"]
direction TB
EP["Enterprise Policy<br/>azppf-enterprise-policy<br/>primary + secondary VNet"]
ENV["Managed Environment<br/>(Pre-existing)"]
EP --> ENV
end
class PPF powerplatform
GH -- "OIDC / Workload Identity Federation" --> RG
ECHO -- "VNet Injection" --> FNSUB
PPFSUB -- "Subnet Injection (primary)" --> EP
PPFSUB2 -- "Subnet Injection (secondary)" --> EP
1. Create variables.auto.tfvars
This file is gitignored and must be created manually. It holds secrets and deployment-mode flags.
# variables.auto.tfvars # Azure identity — see "How to find AZURE_CLIENT_ID" below tenant_id = "<your-azure-tenant-id>" subscription_id = "<your-azure-subscription-id>" primary_location = "eastus" secondary_location = "westus" # Two region geography - US geography (unitedstates) requires a primary (eastus) + secondary (westus) VNet pair. # Set to true before running setup-powerplatform-vnet.ps1. enable_secondary_vnet = true # Single region geography - Set to false to skip the secondary VNet # enable_secondary_vnet = false
2. How to find required IDs
subscription_id / AZURE_SUBSCRIPTION_ID
az account show --query "{subscriptionId:id, tenantId:tenantId}"tenant_id / AZURE_TENANT_ID
Same command as above, or: Azure Portal → Microsoft Entra ID → Overview → Tenant ID
AZURE_CLIENT_ID (app registration for GitHub Actions OIDC)
# Create the app registration az ad app create --display-name "github-actions-terraform" # The output contains "appId" — that is your AZURE_CLIENT_ID # Create a service principal for it az ad sp create --id <appId> # Grant Contributor on the subscription az role assignment create \ --assignee <appId> \ --role Contributor \ --scope /subscriptions/<subscriptionId>
Then add a federated credential so GitHub Actions can authenticate without a secret:
az ad app federated-credential create \ --id <APP_OBJECT_ID> \ --parameters '{ "name": "github-actions-main", "issuer": "https://token.actions.githubusercontent.com", "subject": "repo:<org>/<repo>:ref:refs/heads/main", "audiences": ["api://AzureADTokenExchange"] }' # Also add one for pull requests (enables the plan job on PRs) az ad app federated-credential create \ --id <APP_OBJECT_ID> \ --parameters '{ "name": "github-actions-pr", "issuer": "https://token.actions.githubusercontent.com", "subject": "repo:<org>/<repo>:pull_request", "audiences": ["api://AzureADTokenExchange"] }'
POWERPLATFORM_ENVIRONMENT_ID
Power Platform admin center → Environments → select your environment → Settings → Details → copy the Environment ID (GUID).
The environment must be a Managed Environment for VNet integration to work.
3. GitHub Secrets and Variables to configure
Go to Settings → Secrets and variables → Actions in your repository and add:
Secrets (sensitive values):
| Secret | Where to get it |
|---|---|
AZURE_CLIENT_ID |
appId from app registration (step 2) |
AZURE_SUBSCRIPTION_ID |
az account show |
AZURE_TENANT_ID |
az account show |
POWERPLATFORM_ENVIRONMENT_ID |
Power Platform admin center — Environments → Settings → Details → Environment ID. Use the full Default-<guid> format (e.g. Default-e25f...). You can also copy it from the environment URL: https://make.powerapps.com/environments/Default-<guid>/... |
Variables (non-sensitive configuration):
| Variable | Example value | Notes |
|---|---|---|
PRIMARY_LOCATION |
japaneast |
Azure region for the resource group and all primary resources |
SECONDARY_LOCATION |
japanwest |
Azure region for the secondary VNet |
ENABLE_SECONDARY_VNET |
true |
Deploy secondary VNet. Set true for multi-region Power Platform geography (e.g. US, Japan). Set false for single-region. |
ENTERPRISE_POLICY_LOCATION |
japan |
Power Platform geography — not an Azure region name. Find it in Power Platform admin center → Environments → Settings → Details → Region. See supported geographies |
4. Run locally
# Creates an execution plan terraform plan -out=tfplan # Applies the saved execution plan terraform apply "tfplan" # or (apply without a saved plan and skip approval) terraform apply -auto-approve # Destroys all managed resources terraform destroy
After apply, run the Power Platform VNet setup:
.\powershellscripts\setup-powerplatform-vnet.ps1 ` -EnvironmentId "<guid-from-ppac-settings>" ` -TenantId "<your-azure-tenant-id>" ` -SubscriptionId "<your-azure-subscription-id>" ` -PolicyLocation "<power-platform-geography>"
To decommission VNet integration and destroy all infrastructure:
# Step 1 — unlink the enterprise policy from Power Platform and delete it. # Must be done BEFORE terraform destroy so the subnets are released first. .\powershellscripts\teardown-powerplatform-vnet.ps1 ` -EnvironmentId "<guid-from-ppac-settings>" ` -TenantId "<your-azure-tenant-id>" ` -SubscriptionId "<your-azure-subscription-id>" # Step 2 — destroy all Terraform-managed Azure resources. terraform destroy
5. Call the Echo Container
The ACI echo container has a private IP only — reachable exclusively from within the VNet.
After terraform apply, the private IP is printed as a Terraform output:
terraform output echo_private_ip
To test the connection between Azure and Power Platform, use a Power Apps Canvas App located in the powerapps directory.
- Navigate to: https://make.powerapps.com
- Import the
HelloAzureSolution.zipsolution file inpowerappsdirectory - Update the
AzureFunctionBaseUrlenvironment variable in the solution withhttps://<echo_private_ip>:8443. - Open the Power App.
- Click the Echo button to call the API via the VNet tunnel. The response from the API will be displayed in the purple area.
> az container logs --name azppf-echo-cg --resource-group azppf-rg Listening on ports 8080 for http, and 8443 for https.
Expand
Architecture
Dataflow
- Create a new branch and check in the needed Terraform code modifications.
- Create a Pull Request (PR) in GitHub once you're ready to merge your changes into your environment.
- A GitHub Actions workflow will trigger to ensure your code is well formatted, internally consistent, and produces secure infrastructure. In addition, a Terraform plan will run to generate a preview of the changes that will happen in your Azure environment.
- Once appropriately reviewed, the PR can be merged into your main branch.
- Another GitHub Actions workflow will trigger from the main branch and execute the changes using Terraform.
- A regularly scheduled GitHub Action workflow should also run to look for any configuration drift in your environment and create a new issue if changes are detected.
Workflows
-
This workflow runs on every commit and is composed of a set of unit tests on the infrastructure code. It runs terraform fmt to ensure the code is properly linted and follows terraform best practices. Next it performs terraform validate to check that the code is syntactically correct and internally consistent. Lastly, checkov, an open source static code analysis tool for IaC, will run to detect security and compliance issues. If the repository is utilizing GitHub Advanced Security (GHAS), the results will be uploaded to GitHub.
-
This workflow runs on every pull request and on each commit to the main branch. The plan stage of the workflow is used to understand the impact of the IaC changes on the Azure environment by running terraform plan. This report is then attached to the PR for easy review. The apply stage runs after the plan when the workflow is triggered by a push to the main branch. This stage will take the plan document and apply the changes after a manual review has signed off if there are any pending changes to the environment.
-
This workflow runs on a periodic basis to scan your environment for any configuration drift or changes made outside of terraform. If any drift is detected, a GitHub Issue is raised to alert the maintainers of the project.
Getting Started
To use these workflows in your environment several prerequisite steps are required:
-
Configure Terraform State Location
Terraform utilizes a state file to store information about the current state of your managed infrastructure and associated configuration. This file will need to be persisted between different runs of the workflow. The recommended approach is to store this file within an Azure Storage Account or other similar remote backend. Normally, this storage would be provisioned manually or via a separate workflow. The Terraform backend block will need updated with your selected storage location (see here for documentation).
-
Create GitHub Environment
The workflows utilizes GitHub Environments and Secrets to store the azure identity information and setup an approval process for deployments. Create an environment named
productionby following these instructions. On theproductionenvironment setup a protection rule and add any required approvers you want that need to sign off on production deployments. You can also limit the environment to your main branch. Detailed instructions can be found here. -
Setup Azure Identity:
An Azure Active Directory application is required that has permissions to deploy within your Azure subscription. Create a separate application for a
read-onlyandread/writeaccounts and give them the appropriate permissions in your Azure subscription. In addition, both roles will also need at leastReader and Data Accesspermissions to the storage account where the Terraform state from step 1 resides. Next, setup the federated credentials to allow GitHub to utilize the identity using OIDC. See the Azure documentation for detailed instructions.For the
read/writeidentity create 1 federated credential as follows:- Set
Entity TypetoEnvironmentand use theproductionenvironment name.
For the
read-onlyidentity create 2 federated credentials as follows:- Set
Entity TypetoPull Request. - Set
Entity TypetoBranchand use themainbranch name.
- Set
-
Add GitHub Secrets
Note: While none of the data about the Azure identities contain any secrets or credentials we still utilize GitHub Secrets as a convenient means to parameterize the identity information per environment.
Create the following secrets on the repository using the
read-onlyidentity:AZURE_CLIENT_ID: The application (client) ID of the app registration in AzureAZURE_TENANT_ID: The tenant ID of Azure Active Directory where the app registration is defined.AZURE_SUBSCRIPTION_ID: The subscription ID where the app registration is defined.
Instructions to add the secrets to the repository can be found here.
Additionally create an additional secret on the
productionenvironment using theread-writeidentity:AZURE_CLIENT_ID: The application (client) ID of the app registration in Azure
Instructions to add the secrets to the environment can be found here. The environment secret will override the repository secret when doing the deploy step to the
productionenvironment when elevated read/write permissions are required.
Security Concerns
[!WARNING]
Terraform plans may contain unencrypted secret information!
The Terraform Plan / Apply GitHub Action workflow uploads the Terraform plan as an artifact. This is to ensure that the changes being approved are exactly those that will be applied in the apply stage. However, it is important to note that the plan file may contain unencrypted secrets (see hashicorp/terraform#29535). For additional security hardening you should consider either:
-
Encrypting / Decrypting the tfplan
Before uploading the tfplan file to GitHub you can leverage a repo secret to encrypt the file. Then after downloading the file could then be decrypted with the same secret. This would prevent anyone from downloading the plan file to access secret information directly. Only those who have the ability to access the secret can decrypt the file.
See example: https://github.com/MatthewWilkes/test-gh-actions-gpg/blob/main/.github/workflows/tf-plan-apply.yml (Thanks @MatthewWilkes)
-
Leverage ephemeral values
Terraform 1.10 added support for defining ephemeral values to protect secret information from being stored in plan or state files. See https://www.hashicorp.com/blog/terraform-1-10-improves-handling-secrets-in-state-with-ephemeral-values
Additional Resources
A companion article detailing how to use GitHub Actions to deploy to Azure using IaC can be found at the DevOps Resource Center.
