> For the complete documentation index, see llms.txt.
Skip to main content

Check out Port for yourself โžœย 

Create cloud resources using IaC

Implement with AI

Send this guide to your coding agent.

Prerequisite: Install Port MCP

Open plan mode if your tool supports it; otherwise present the plan below filled in and wait for my approval. Implement this Port guide in my org via MCP:

https://docs.port.io/guides/all/create-cloud-resource-using-iac

Read the raw markdown version at https://docs.port.io/guides/all/create-cloud-resource-using-iac.md - it contains every tab and code block without page markup.

Goal: get the guide's core flow working end-to-end in my org; adapting it to fit my existing setup takes priority over matching the guide 1:1.

Plan:
1. Confirm MCP is connected, in the right org, with sufficient permissions.
2. If the guide offers alternative implementation paths (tabs), pick the one matching my installed integrations and tools, confirm it with me, and implement only that path.
3. Diff the guide's data model (blueprints, properties, relations, workflows, actions, agents, automations, integrations, webhook data sources, secrets) against mine.
4. Propose adaptations for gaps, reusing existing blueprints/relations over guide-named duplicates.
5. Flag what needs a UI click, credential, or secret from me, testing MCP capability empirically before ruling anything out. If the guide has a "Set up via API" section, use it for anything MCP can't do before treating a step as UI-only.
6. Stop on any blocker and give me options. Approving this plan authorizes the writes it lists; pause only for writes beyond what's listed.

Build:
- Extend blueprint schema additively when upserting; don't remove or overwrite existing properties, and treat type conflicts as a blocker, not an auto-fix.
- Never print secret values into the chat or logs; ask me to set them in Port, or write them via the secrets API without echoing them back.
- List any mock data in the plan, minimal and labeled mock; once approved, seed it without re-asking, and tell me what you seeded.
- For anything the guide writes downstream (e.g. a webhook target), use a real entity, not a mock.
- For pages/widgets, use the real page identifier from the app URL, not a guessed slug.
- When you hit a UI step confirmed (not assumed) unsupported via MCP and not covered by the guide's API sections, pause, give exact clicks, then resume via MCP.
- Validate and give links after each meaningful step (only a tool-returned URL, no guessed paths); don't proceed if the last run wasn't a success.

Done:
- Run the guide's "Let's test it" steps where possible (e.g. execute a workflow test run) and confirm the expected output exists in Port.
- Summarize adaptations, seeded data, what was mocked or skipped, remaining UI steps, and how to verify.

This guide shows how to open a pull request in your Git repositoryโ€”directly from Portโ€”to create a new cloud resource using GitOps.

Prerequisitesโ€‹

  • A Port account with permissions to create self-service actions.
  • The Git Integration that is relevant for you needs to be installed.
  • A repository in your Git provider in which you can create a workflow/pipeline. We recommend naming it Port-actions.

Common use casesโ€‹

  • Platform engineers can define the logic of creating a cloud resource, including standard enforcement, security and permission boundaries.
  • Developers can easily create and track cloud resources directly from Port.

Set up data modelโ€‹

To enable tracking of cloud resource definitions in Port, we'll add a new URL property to the existing Repository blueprint. This property will point to the location of IaC templates in your Git repository, letting us seamlessly link infrastructure code to each repository.

Add a resource definitions propertyโ€‹

In this guide we will add a new property to our Repository blueprint, which we can use to access our cloud resource definitions.

  1. Go to your Data model page.

  2. Expand your Repository blueprint, then click on + New property.

  3. Choose URL as the type, fill it like this and click Save:

    New property form with Resource definitions URL

This property is empty for now in all repositories, we will fill it as part of the action we're about to create ๐Ÿ˜Ž

Set up the action's frontendโ€‹

  1. Head to the Self-service page in Port.

  2. Click on the + Action button in the top-right corner :

    Add Action button in self-service toolbar
  3. Fill the basic form with the following:

    • Title: Enter Create s3 bucket

    • Identifier Toggle the switch icon off and type a create_s3_bucket

    • Description: Enter the description (e.g., Create an s3 bucket)

    • Icon: Type s3 and choose the Icon (optional)

    • Operation: Choose Day-2 from the dropdown

    • Blueprint: Choose Repository from the dropdown

      New action basic details form for s3

  4. Click on Next, and add the following inputs: Name and Visibility.

    To create the Name input field:

    • Click on + Input.

    • Enter the Title Name.

    • Select the Type Text.

    • Add the Description (optional).

    • Set Required to True.

    • Click on the Create button.


      New user input form for Name field

    To create the Visibility input field:

    • Click on + Input.

    • Enter the Title Visibility.

    • Choose the Type Select.

    • Set Required to True.

    • Configure Additional Information:

      • Item Type: String.
      • Limit: 1 option.
      • Add Options:
        • Option 1: public (choose a color).
        • Option 2: private (choose a color).
    • Click on the Create button.


      New user input form for Visibility field
  5. Click on Next to configure the Backend.

Now we'll define the backend of the action. Port supports multiple invocation types, depending on the Git provider you are using.

Fill out the form with your values:

  • Replace the Organization and Repository values with your values (this is where the workflow will reside and run).

  • Name the workflow port-create-bucket.yml.

  • Fill out your workflow details:

    GitHub workflow backend form configuration
  • Scroll down to the Configure the invocation payload section.
    This is where you can define which data will be sent to your backend each time the action is executed.

    For this example, we will send some details that our backend needs to know - the inputs, along with the entity and the id of the action run.
    Copy the following JSON snippet and paste it in the payload code box:

    {
    "port_context": {
    "entity": "{{ .entity.identifier }}",
    "runId": "{{ .run.id }}"
    },
    "name": "{{ .inputs.name }}",
    "visibility": "{{ .inputs.visibility }}"
    }

The last step is customizing the action's permissions. For simplicity's sake, we will use the default settings. For more information, see the permissions page. Click Create.

The action's frontend is now ready ๐Ÿฅณ

Set up the action's backendโ€‹

Now we want to write the logic that our action will trigger.

  1. First, let's create the necessary token and secrets. If you've already completed the scaffold a new service guide, you should already have these configured and you can skip this step.

    • Go to your Github tokens page, create a personal access token with repo and admin:org scope, and copy it (this token is needed to create a pull-request from our workflow).

      GitHub personal access token with repo scopes
    • Open the Credentials modal and copy your Client ID and Client secret.

  2. In the repository where your workflow will reside, create 3 new secrets under Settings->Secrets and variables->Actions:

    • ORG_ADMIN_TOKEN - the personal access token you created in the previous step.

    • PORT_CLIENT_ID - the client ID you copied from your Port app.

    • PORT_CLIENT_SECRET - the client secret you copied from your Port app.

      GitHub repository secrets for Port and org token

  3. Now let's create the workflow file that contains our logic. Our workflow will consist of 3 steps:

    • Creating a copy of the template file in the selected service's repository and replacing its variables with the data from the action's input.
    • Creating a pull request in the selected service's repository to add the new resource.
    • Reporting & logging the action result back to Port, and updating the relevant service's Resource definitions property with the URL of the service's resources directory.
  4. Under .github/workflows/, create a new file named port-create-bucket.yml and use the following snippet as its content:

    Github workflow (click to expand)
    name: Create cloud resource
    on:
    workflow_dispatch:
    inputs:
    port_context:
    required: true
    description: Includes the entity identifier, and the action's run id
    name:
    required: true
    description: The name of the new resource
    type: string
    visibility:
    required: true
    description: The visibility of the new resource
    type: string
    jobs:
    createResource:
    runs-on: ubuntu-latest
    steps:
    # Checkout the workflow's repository
    - uses: actions/checkout@v6
    # Checkout the service's repository
    - uses: actions/checkout@v6
    with:
    repository: "${{ fromJson(inputs.port_context).entity }}"
    path: ./targetRepo
    token: ${{ secrets.ORG_ADMIN_TOKEN }}
    - name: Copy template file
    run: |
    mkdir -p ./targetRepo/resources
    cp templates/cloudResource.tf ./targetRepo/resources/${{ inputs.name }}.tf
    - name: Update new file data
    run: |
    sed -i 's/{{ bucket_name }}/${{ inputs.name }}/' ./targetRepo/resources/${{ inputs.name }}.tf
    sed -i 's/{{ bucket_acl }}/${{ inputs.visibility }}/' ./targetRepo/resources/${{ inputs.name }}.tf
    - name: Open a pull request
    uses: peter-evans/create-pull-request@v5
    with:
    token: ${{ secrets.ORG_ADMIN_TOKEN }}
    path: ./targetRepo
    commit-message: Create new resource - ${{ inputs.name }}
    committer: GitHub <noreply@github.com>
    author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
    signoff: false
    branch: new-resource-${{ inputs.name }}
    delete-branch: true
    title: Create new resource - ${{ inputs.name }}
    body: |
    Create new ${{ inputs.visibility }} resource - ${{ inputs.name }}
    draft: false
    create-entity-in-port-and-update-run:
    runs-on: ubuntu-latest
    needs: createResource
    steps:
    - name: UPSERT Entity
    uses: port-labs/port-github-action@v1
    with:
    identifier: ${{ fromJson(inputs.port_context).entity }}
    blueprint: githubRepository
    properties: |-
    {
    "resource_definitions": "${{ github.server_url }}/${{ fromJson(inputs.port_context).entity }}/blob/main/resources/"
    }
    clientId: ${{ secrets.PORT_CLIENT_ID }}
    clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
    baseUrl: https://api.port.io
    operation: UPSERT
    runId: ${{ fromJson(inputs.port_context).runId }}
    - 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.port.io
    operation: PATCH_RUN
    runId: ${{ fromJson(inputs.port_context).runId }}
    logMessage: Pull request created successfully for "${{ inputs.name }}" ๐Ÿš€
Selecting a Port API URL by account region

The port_region, port.baseUrl, portBaseUrl, port_base_url and OCEAN__PORT__BASE_URL parameters select which Port API instance to use:


We will now create a simple .tf file that will serve as a template for our new resource:

  • In your source repository (port-actions for example), create a file named cloudResource.tf under /templates/ (it's path should be /templates/cloudResource.tf).
  • Copy the following snippet and paste it in the file's contents:
cloudResource.tf (click to expand)
cloudResource.tf
resource "aws_s3_bucket" "example" {
provider = aws.bucket_region
name = "{{ bucket_name }}"
acl = "{{ bucket_acl }}"
}

All done! The action is ready to be executed ๐Ÿš€


Execute the actionโ€‹

After creating an action, it will appear in the Self-service page in Port:

Create s3 bucket action card
  1. Click Execute. On the top right corner of the page, click on the action button to view the action progress:

    Action run history for s3 bucket creation

  2. This page provides details about the action run. We can see that the backend returned Success and the pull-request was created successfully:

    Successful s3 bucket action run with PR log

Access the bucket's definition from Portโ€‹

You may have noticed that even though we updated the service's Resource definitions URL, it still leads to a non-existent page. This is because we do not have any resources in the repository yet, let's take care of that:

  1. Merge the pull-request.

  2. Go to the entity page of the service that you executed the action for:

    Service entity page with Resource definitions link
  3. Click on the Resource definitions link to access the service's resources.

All done! You can now create resources for your services directly from Port ๐Ÿ’ช๐Ÿฝ

Example: S3 bucketโ€‹

Use this example when you want to create an AWS S3 bucket with Terraform and report the bucket back to Port as an entity.

This example creates:

  • An s3Bucket blueprint in Port.
  • An AWS S3 bucket.
  • An S3 bucket ACL.
  • A matching Port entity for the created bucket.

Set up the S3 bucket blueprint

Create the s3Bucket blueprint before running the Terraform example.

S3 bucket blueprint API definition (Click to expand)
{
"identifier": "s3Bucket",
"description": "",
"title": "S3 Bucket",
"icon": "Bucket",
"schema": {
"properties": {
"isPrivate": {
"type": "boolean",
"title": "Is private?"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"relations": {}
}

Create the S3 bucket with Terraform

Create a main.tf file with the following Terraform definition. Replace the placeholders for access_key, secret_key, client_id, and secret before running it.

S3 bucket Terraform definition (Click to expand)
terraform {
required_providers {
port = {
source = "port-labs/port-labs"
version = "~> 2.0.3"
}
}
}

provider "aws" {
access_key = "YOUR_ACCESS_KEY_ID"
secret_key = "YOUR_SECRET_ACCESS_KEY"
region = "eu-west-1"
}

provider "port" {
client_id = "YOUR_CLIENT_ID" # or set the environment variable PORT_CLIENT_ID
secret = "YOUR_CLIENT_SECRET" # or set the environment variable PORT_CLIENT_SECRET
base_url = "https://api.port.io"
}

resource "aws_s3_bucket" "port-terraform-example-bucket" {
bucket = "my-port-terraform-example-bucket"
}

resource "aws_s3_bucket_acl" "port-terraform-example-bucket-acl" {
bucket = aws_s3_bucket.port-terraform-example-bucket.id
acl = "private"
}

resource "port_entity" "s3_bucket" {
depends_on = [
aws_s3_bucket.port-terraform-example-bucket
]

identifier = aws_s3_bucket.port-terraform-example-bucket.bucket
title = aws_s3_bucket.port-terraform-example-bucket.bucket
blueprint = "s3Bucket"

properties = {
string_props = {
"isPrivate" = aws_s3_bucket_acl.port-terraform-example-bucket-acl.acl == "private" ? true : false
}
}
}
Selecting a Port API URL by account region

The port_region, port.baseUrl, portBaseUrl, port_base_url and OCEAN__PORT__BASE_URL parameters select which Port API instance to use:

Run the following commands to initialize Terraform, inspect the planned changes, create the infrastructure, and update Port:

terraform init
terraform plan
terraform apply

The depends_on block on the Port entity ensures Terraform creates the S3 bucket before it creates the matching s3Bucket entity in Port.

Update the S3 bucket

The isPrivate property is calculated from the bucket ACL:

properties = {
string_props = {
"isPrivate" = aws_s3_bucket_acl.port-terraform-example-bucket-acl.acl == "private" ? true : false
}
}

To change the bucket ACL, update the ACL resource:

resource "aws_s3_bucket_acl" "port-terraform-example-bucket-acl" {
bucket = aws_s3_bucket.port-terraform-example-bucket.id
acl = "public-read" # Changed from "private"
}

Run terraform apply again. Terraform updates the bucket ACL and updates the isPrivate property on the matching Port entity.

Clean up the S3 bucket example

Run terraform destroy to delete the AWS S3 bucket and the matching Port entity created in this example.

Advanced example: Developer environmentโ€‹

Use this advanced example when you want to create a complete AWS developer environment with Terraform and report it back to Port as an entity.

This example creates:

  • A developerEnvironment blueprint in Port.
  • An S3 bucket that stores generated artifacts.
  • An SQS FIFO queue that receives messages.
  • A Lambda function that is triggered by messages sent to the queue.
  • A matching Port entity for the developer environment.

Set up the developer environment blueprint

Create the developerEnvironment blueprint before running the Terraform example.

Developer environment blueprint API definition (Click to expand)
{
"identifier": "developerEnvironment",
"description": "",
"title": "Developer Environment",
"icon": "Environment",
"schema": {
"properties": {
"bucketUrl": {
"type": "string",
"title": "Bucket URL",
"format": "url"
},
"lambdaUrl": {
"type": "string",
"title": "Lambda URL",
"format": "url"
},
"queueUrl": {
"type": "string",
"title": "Queue URL",
"format": "url"
},
"memorySize": {
"type": "number",
"title": "Memory Size"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"relations": {}
}

Create the developer environment with Terraform

Download the Node.js Lambda template and save the .zip file next to your main.tf file.

Create a main.tf file with the following Terraform definition. Replace the placeholders for access_key, secret_key, client_id, and secret before running it.

Developer environment Terraform definition (Click to expand)
terraform {
required_providers {
port = {
source = "port-labs/port-labs"
version = "~> 2.0.3"
}
}
}

provider "aws" {
access_key = "YOUR_ACCESS_KEY_ID"
secret_key = "YOUR_SECRET_ACCESS_KEY"
region = "eu-west-1"
}

provider "port" {
client_id = "YOUR_CLIENT_ID" # or set the environment variable PORT_CLIENT_ID
secret = "YOUR_CLIENT_SECRET" # or set the environment variable PORT_CLIENT_SECRET
base_url = "https://api.port.io"
}

resource "aws_s3_bucket" "port_terraform_example_dev_env_bucket" {
bucket = "my-port-terraform-example-dev-env-bucket"
}

resource "aws_s3_bucket_acl" "port_terraform_example_dev_env_bucket-acl" {
bucket = aws_s3_bucket.port_terraform_example_dev_env_bucket.id
acl = "private"
}

resource "aws_sqs_queue" "port_terraform_example_dev_env_queue" {
name = "terraform-example-queue.fifo"
fifo_queue = true
content_based_deduplication = true
}

resource "aws_iam_role" "iam_for_lambda" {
name = "iam_for_lambda"

assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}

resource "aws_iam_role_policy_attachment" "attach_sqs_policy_for_lambda_role" {
role = aws_iam_role.iam_for_lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole"
}

resource "aws_lambda_function" "port_terraform_example_dev_env_lambda" {
depends_on = [
aws_sqs_queue.port_terraform_example_dev_env_queue
]

filename = "lambda_function_payload.zip"
function_name = "dev-env-function"
role = aws_iam_role.iam_for_lambda.arn
handler = "index.test"

runtime = "nodejs16.x"
}

resource "aws_lambda_function_url" "example_function_url" {
function_name = aws_lambda_function.port_terraform_example_dev_env_lambda.function_name
authorization_type = "NONE"
}

resource "aws_lambda_event_source_mapping" "event_source_mapping" {
event_source_arn = aws_sqs_queue.port_terraform_example_dev_env_queue.arn
enabled = true
function_name = aws_lambda_function.port_terraform_example_dev_env_lambda.arn
batch_size = 1
}

resource "port_entity" "dev_env" {
depends_on = [
aws_s3_bucket.port_terraform_example_dev_env_bucket,
aws_lambda_function.port_terraform_example_dev_env_lambda,
aws_sqs_queue.port_terraform_example_dev_env_queue,
aws_lambda_function_url.example_function_url
]

identifier = aws_lambda_function.port_terraform_example_dev_env_lambda.function_name
title = aws_lambda_function.port_terraform_example_dev_env_lambda.function_name
blueprint = "developerEnvironment"

properties = {
string_props = {
"bucketUrl" = "https://${aws_s3_bucket.port_terraform_example_dev_env_bucket.bucket_domain_name}"
"queueUrl" = aws_sqs_queue.port_terraform_example_dev_env_queue.id
"lambdaUrl" = aws_lambda_function_url.example_function_url.function_url
}
number_props = {
"memorySize" = aws_lambda_function.port_terraform_example_dev_env_lambda.memory_size
}
}
}
Selecting a Port API URL by account region

The port_region, port.baseUrl, portBaseUrl, port_base_url and OCEAN__PORT__BASE_URL parameters select which Port API instance to use:

Run the following commands to initialize Terraform, inspect the planned changes, create the infrastructure, and update Port:

terraform init
terraform plan
terraform apply

The depends_on block on the Port entity ensures Terraform creates the S3 bucket, Lambda function, SQS queue, and Lambda function URL before it creates the matching developerEnvironment entity in Port.

Update the developer environment

The developer environment entity maps dynamic properties directly from the AWS resources defined in Terraform. If one of those resources changes, the Port entity updates the next time you run terraform apply.

For example, increase the Lambda function memory size:

resource "aws_lambda_function" "port_terraform_example_dev_env_lambda" {
depends_on = [
aws_sqs_queue.port_terraform_example_dev_env_queue
]

filename = "lambda_function_payload.zip"
function_name = "dev-env-function"
role = aws_iam_role.iam_for_lambda.arn
handler = "index.handler"

runtime = "nodejs16.x"

memory_size = 256 # was 128
}

Run terraform apply again. Terraform updates the Lambda function and updates the memorySize property on the matching Port entity.

Clean up the developer environment example

Run terraform destroy to delete the AWS infrastructure and the matching Port entity created in this example.

Integrate daily routinesโ€‹

  • Send a Slack message to relevant people in the organization, notifying them about the new resource.
  • Send a weekly or monthly report for managers and DevOps teams showing the new resources created in this timeframe and their owners.

More relevant guidesโ€‹

An agentic SDLC platform should integrate with GitOps practices seamlessly. Developers should be able to perform routine tasks independently, without creating bottlenecks within the organization.

With Port, platform engineers can design precise and flexible self-service actions for their developers while integrating with many different backends to suit your specific needs.