Provision Cloud Resource using Terraform Plan and Apply
This guide, demonstrates how to create two self-service actions in Port to plan and apply a cloud resource such as s3 bucket using Terraform in a GitHub workflow.
The first action generates the Terraform plan for the S3 bucket configuration, while the second action reviews, approves, and applies the configuration to provision the bucket.
Common use casesโ
- High Availability: Safeguard against downtime by reviewing and approving critical infrastructure changes before implementation
- Cost Control: Ensure proposed resource changes align with budget constraints by reviewing and approving them before implementation
- Regulatory Compliance: Adhere to industry regulations by planning and approving infrastructure modifications to ensure compliance with regulatory standards.
Prerequisitesโ
This guide assumes the following:
- You have a Port account and have completed the onboarding process.
- Port's GitHub App intalled.
- Port's AWS integration is installed in your account.
- A GitHub repository to host your Terraform configuration files and GitHub Actions workflows.
Set up data modelโ
If you haven't installed the AWS integration, you'll need to create a blueprint for AWS resources in Port.
However, we highly recommend you install the AWS integration to have these automatically set up for you.
Create the AWS Cloud resource blueprint
-
Go to your Builder page.
-
Click on
+ Blueprint
. -
Click on the
{...}
button in the top right corner, and choose "Edit JSON". -
Add this JSON schema:
Cloud resource blueprint (click to expand)
{
"identifier": "cloudResource",
"description": "This blueprint represents a cloud resource",
"title": "Cloud Resource",
"icon": "AWS",
"schema": {
"properties": {
"type": {
"type": "string",
"description": "Type of the cloud resource (e.g., virtual machine, database, storage, etc.)",
"title": "Type"
},
"provider": {
"type": "string",
"description": "Cloud service provider (e.g., AWS, Azure, GCP)",
"title": "Provider"
},
"region": {
"type": "string",
"description": "Region where the resource is deployed",
"title": "Region"
},
"link": {
"type": "string",
"title": "Link",
"format": "url"
},
"tags": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Custom tags associated with the resource",
"title": "Tags"
},
"status": {
"type": "string",
"description": "Current status of the resource (e.g., running, stopped, provisioning, etc.)",
"title": "Status"
},
"created_at": {
"type": "string",
"description": "Timestamp indicating when the resource was created",
"title": "Created At",
"format": "date-time"
},
"updated_at": {
"type": "string",
"description": "Timestamp indicating when the resource was last updated",
"title": "Updated At",
"format": "date-time"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {}
} -
Click "Save" to create the blueprint.
Implementationโ
To implement this use-case using a GitHub workflow, follow these steps:
Add GitHub secretsโ
In your GitHub repository, go to Settings > Secrets and add the following secrets:
PORT_CLIENT_ID
- Your portclient id
, how to get the credentials.PORT_CLIENT_SECRET
- Your portclient secret
, how to get the credentials.AWS_ACCESS_KEY_ID
- AWS access key ID with permission to create an s3 bucket, how to create an AWS access key.AWS_SECRET_ACCESS_KEY
- AWS secret access key with permission to create s3 bucket, how to create secret access key.AWS_SESSION_TOKEN
- AWS session token How to create an AWS session token.AWS_REGION
- The AWS region where you would like to provision your s3 bucket.MY_GITHUB_TOKEN
- A Classic Personal Access Token with therepo
scope. This token will be used to download the terraform configurations saved to GitHub Artifact.
Create Terraform templatesโ
Create the following Terraform templates (main.tf
and variables.tf
) in a terraform
folder at the root of your GitHub repository:
We recommend creating a dedicated repository for the workflows that are used by Port actions.
main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.43"
}
}
}
provider "aws" {
region = var.aws_region
}
resource "aws_s3_bucket" "example_bucket" {
bucket = var.bucket_name
acl = "private"
tags = {
Name = var.bucket_name
Environment = var.environment
}
}
variables.tf
variable "aws_region" {
description = "The AWS region to deploy the resource to"
default = "eu-west-1"
}
variable "bucket_name" {
description = "The name for the S3 bucket"
}
variable "environment" {
description = "The environment where the resources are deployed"
default = "staging"
}
Create self-service actionโ
We'll create a single Port self-service action that handles both planning and applying terraform resources with an approval gate.
-
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: Plan and Apply Terraform Resource (click to expand)
tip<GITHUB-ORG>
- your GitHub organization or user name.<GITHUB-REPO-NAME>
- your GitHub repository name.
{
"identifier": "terraform_plan_and_apply",
"title": "Plan and Apply Terraform Resource",
"icon": "Terraform",
"description": "Plans a cloud resource on AWS using terraform, waits for approval, then applies the configuration",
"trigger": {
"type": "self-service",
"operation": "CREATE",
"userInputs": {
"properties": {
"bucket_name": {
"type": "string",
"title": "Bucket Name",
"icon": "AWS"
}
},
"required": ["bucket_name"],
"order": ["bucket_name"]
},
"blueprintIdentifier": "cloudResource"
},
"invocationMethod": {
"type": "GITHUB",
"org": "<ENTER-GITHUB-ORG>",
"repo": "<ENTER-GITHUB-REPO-NAME>",
"workflow": "plan-terraform-resource.yaml",
"workflowInputs": {
"bucket_name": "{{ .inputs.\"bucket_name\" }}",
"port_context": {
"blueprint": "{{.action.blueprint}}",
"entity": "{{.entity}}",
"runId": "{{.run.id}}",
"trigger": "{{ .trigger }}"
}
},
"reportWorkflowStatus": true
},
"requiredApproval": true,
"approvalNotification": {
"type": "email"
}
} -
Click on "Save" to create the action.
Create automation to trigger apply workflowโ
Now we'll create an automation that automatically triggers the apply GitHub workflow when the action is approved.
-
Head to the automations page.
-
Click on the
+ Automation
button. -
Copy and paste the following JSON configuration into the editor:
Automation: Auto-trigger Apply Workflow on Approval (click to expand)
tip<GITHUB-ORG>
- your GitHub organization or user name.<GITHUB-REPO-NAME>
- your GitHub repository name.
{
"identifier": "terraform_auto_apply_on_approval",
"title": "Auto-trigger Terraform Apply on Approval",
"description": "Automatically triggers the apply GitHub workflow when the terraform plan action is approved",
"trigger": {
"type": "automation",
"event": {
"type": "RUN_UPDATED",
"actionIdentifier": "terraform_plan_and_apply"
},
"condition": {
"type": "JQ",
"expressions": [
".diff.before.status == \"WAITING_FOR_APPROVAL\"",
".diff.after.status == \"IN_PROGRESS\""
],
"combinator": "and"
}
},
"invocationMethod": {
"type": "GITHUB",
"org": "<ENTER-GITHUB-ORG>",
"repo": "<ENTER-GITHUB-REPO-NAME>",
"workflow": "apply-terraform-resource.yaml",
"workflowInputs": {
"port_run_identifier": "{{ .event.diff.after.id }}",
"bucket_name": "{{ .event.diff.after.properties.bucket_name }}",
"port_context": {
"blueprint": "{{ .event.diff.after.blueprint.identifier }}",
"entity": "{{ .event.diff.after.entity }}",
"runId": "{{ .event.diff.after.id }}",
"trigger": "automation"
}
},
"reportWorkflowStatus": false
},
"publish": true
} -
Click "Save" to create the automation.
Create GitHub workflowsโ
In your GitHub repositoty, we will create two GitHub workflows to plan a terraform resource and apply the configuration.
Plan a terraform resource GitHub workflow
Follow these steps to create the Plan a Terraform Resource
GitHub workflow.
Create a workflow file under .github/workflows/plan-terraform-resource.yaml
with the following content.
GitHub workflow script to plan a cloud resource (click to expand)
name: Plan a Cloud Resource using Terraform
on:
workflow_dispatch:
inputs:
bucket_name:
type: string
required: true
port_context:
required: true
description: >-
Port's payload, including details for who triggered the action and
general context (blueprint, run id, etc...)
jobs:
plan-and-request-approval-for-bucket:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log starting of s3 bucket creation
uses: port-labs/port-github-action@v1
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
operation: PATCH_RUN
runId: ${{fromJson(inputs.port_context).runId}}
logMessage: |
About to create an s3 bucket with name: ${{ github.event.inputs.bucket_name }} ... โด๏ธ
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: '${{ secrets.AWS_ACCESS_KEY_ID }}'
aws-secret-access-key: '${{ secrets.AWS_SECRET_ACCESS_KEY }}'
aws-session-token: '${{ secrets.AWS_SESSION_TOKEN }}'
aws-region: '${{ secrets.AWS_REGION }}'
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.5
- name: Terraform Plan
id: plan
env:
TF_VAR_bucket_name: "${{ github.event.inputs.bucket_name }}"
TF_VAR_aws_region: "${{ secrets.AWS_REGION }}"
run: |
cd terraform
terraform init
terraform validate
terraform plan \
-input=false \
-out=tfplan-${{fromJson(inputs.port_context).runId}}
terraform show -json tfplan-${{fromJson(inputs.port_context).runId}} > tfplan.json
- name: Upload Terraform Plan Artifact
uses: actions/upload-artifact@v4
id: artifact-upload-step
with:
name: tfplan-${{fromJson(inputs.port_context).runId}}
path: terraform/
retention-days: 7 ## change this to preferred number of days to keep the artifact before deletion
- name: Update Port on successful plan and upload of terraform resource
if: ${{ steps.plan.outcome == 'success' && steps.artifact-upload-step.outcome == 'success' }}
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
runId: ${{fromJson(inputs.port_context).runId}}
logMessage: |
โ
S3 bucket planned successfully! Waiting for approval to apply the changes.
- name: Update Port on unsuccessful plan of terraform resource
if: ${{ steps.plan.outcome != 'success' || steps.artifact-upload-step.outcome != 'success' }}
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
runId: ${{fromJson(inputs.port_context).runId}}
status: "FAILURE"
logMessage: |
โ Error occurred while planning or saving terraform resource. Please check the workflow logs.
Approve and apply the terraform resource GitHub workflow
Follow these steps to create the Approve and Apply Terraform Resource
GitHub workflow.
Create a workflow file under .github/workflows/apply-terraform-resource.yaml
with the following content.
GitHub workflow script to approve and apply the terraform configuration (click to expand)
name: Apply Terraform Resource
on:
workflow_dispatch:
inputs:
port_run_identifier:
type: string
required: true
bucket_name:
type: string
required: true
port_context:
required: true
description: >-
Port's payload, including details for who triggered the action and
general context (blueprint, run id, etc...)
jobs:
apply-terraform-resource:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log starting of cloud resource creation
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
runId: ${{fromJson(inputs.port_context).runId}}
logMessage: |
๐ Applying terraform configuration that was approved in run: ${{ github.event.inputs.port_run_identifier }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: '${{ secrets.AWS_ACCESS_KEY_ID }}'
aws-secret-access-key: '${{ secrets.AWS_SECRET_ACCESS_KEY }}'
aws-session-token: '${{ secrets.AWS_SESSION_TOKEN }}'
aws-region: '${{ secrets.AWS_REGION }}'
- name: Download Terraform plan artifact from the previous workflow run
run: |
mkdir terraform-artifact
cd terraform-artifact
# Get the artifact download URL by name
artifact_url=$(curl -sSL \
-H "Authorization: Bearer ${{ secrets.MY_GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts" \
| jq -r --arg artifact_name "tfplan-${{ github.event.inputs.port_run_identifier }}" \
'.artifacts[] | select(.name == $artifact_name) | .archive_download_url')
if [ "$artifact_url" == "null" ] || [ -z "$artifact_url" ]; then
echo "โ Terraform plan artifact not found for run: ${{ github.event.inputs.port_run_identifier }}"
exit 1
fi
# Download and extract the artifact
curl -sSL -H "Authorization: Bearer ${{ secrets.MY_GITHUB_TOKEN }}" \
-o terraform-artifact.zip "$artifact_url"
if [ $? -ne 0 ]; then
echo "โ Failed to download artifact. Exiting."
exit 1
fi
unzip -qq terraform-artifact.zip
if [ $? -ne 0 ]; then
echo "โ Failed to extract artifact. Exiting."
exit 1
fi
- name: List contents of working directory
run: ls -la terraform-artifact
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.5
- name: Make provider binary executable
run: |
cd terraform-artifact
chmod +x .terraform/providers/registry.terraform.io/hashicorp/aws/5.42.0/linux_amd64/terraform-provider-aws_v5.42.0_x5
- name: Terraform apply resource
id: tf-apply
run: |
cd terraform-artifact
terraform apply tfplan-${{ github.event.inputs.port_run_identifier }}
- name: Update Port on status of applying terraform resource (success)
uses: port-labs/port-github-action@v1
if: ${{steps.tf-apply.outcome == 'success'}}
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
baseUrl: https://api.getport.io
operation: PATCH_RUN
runId: ${{fromJson(inputs.port_context).runId}}
logMessage: |
โ
Cloud resource successfully provisioned and available in AWS!
- name: Get current timestamp
id: timestamp
run: echo "::set-output name=current_time::$(date -u +'%Y-%m-%dT%H:%M:%S.%3NZ')"
- name: Create cloud resource in Port
uses: port-labs/port-github-action@v1
if: ${{steps.tf-apply.outcome == 'success'}}
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
baseUrl: https://api.getport.io
operation: UPSERT
identifier: ${{ github.event.inputs.bucket_name }}
blueprint: cloudResource
properties: |-
{
"type": "storage",
"provider": "AWS",
"region": "${{ secrets.AWS_REGION }}",
"link": "https://s3.console.aws.amazon.com/s3/buckets/${{ github.event.inputs.bucket_name }}",
"created_at": "${{ steps.timestamp.outputs.current_time }}"
}
- name: Update Port on status of applying terraform resource (failure)
uses: port-labs/port-github-action@v1
if: ${{steps.tf-apply.outcome != 'success'}}
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
baseUrl: https://api.getport.io
operation: PATCH_RUN
runId: ${{fromJson(inputs.port_context).runId}}
logMessage: |
โ Cloud resource could not be provisioned. Please check the workflow logs for details.
The 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.port.io), your API URL is
https://api.port.io
. - If you use the US region of Port (https://app.us.port.io), your API URL is
https://api.us.port.io
.
Let's test it!โ
Now let's see how the simplified automation-powered workflow works:
-
Head to the self-service page of your portal
-
Trigger the
Plan and Apply Terraform Resource
action (that's it - just one action!): -
The action will execute the planning workflow and then wait for approval. You'll see logs showing:
- Terraform plan execution
- Artifact upload
- Status: "Waiting for approval"
-
Since
requiredApproval
is set totrue
, an email notification will be sent to the approval team: -
Once approved, the automation automatically triggers the apply GitHub workflow. You'll see the apply workflow start running without any manual intervention!
-
The apply workflow downloads the terraform plan artifact and applies the configuration. Head over to your AWS console to view the created bucket: