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 port- client id, how to get the credentials.
- PORT_CLIENT_SECRET- Your port- client 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 the- reposcope. 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 Actionbutton.
- 
Click on the {...} Edit JSONbutton.
- 
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 + Automationbutton.
- 
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 of 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 Resourceaction (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 requiredApprovalis 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: 