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

Check out Port for yourself ➜ 

Automate AWS account creation with GitLab

Implement with AI

Send this guide to your coding agent.

Prerequisite: Install Port MCP

Open plan mode. Implement this Port guide in my org via MCP:

https://docs.port.io/guides/all/create-new-aws-account-gitlab/

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. Diff the guide's data model (blueprints, properties, relations, actions, agents, automations, integrations, secrets) against mine.
3. Propose adaptations for gaps, reusing existing blueprints/relations over guide-named duplicates.
4. Flag what needs a UI click, credential, or secret from me, testing MCP capability empirically before ruling anything out.
5. 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.
- 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, 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:
- Confirm the guide's expected output exists and runs in Port.
- Summarize adaptations, seeded data, what was mocked or skipped, remaining UI steps, and how to verify.

This guide demonstrates how to build a Port workflow that provisions new AWS accounts using GitLab CI/CD and Terraform.

The workflow collects the account request from a developer, uses an AI node to validate it against the accounts already in your catalog, and then routes the request. Approved requests trigger a GitLab pipeline that runs Terraform against AWS Organizations. Rejected requests are recorded in the catalog with the reason, so denied requests leave an audit trail instead of disappearing.

Port triggers the pipeline through your installed GitLab integration, so there is no trigger token to mint and no webhook URL to copy.

Open Beta

Port workflows are currently in open beta and available to all users. Workflows may undergo changes without prior notice.

Common use cases

  • Multi-account strategy: Enable teams to quickly spin up dedicated AWS accounts for different projects or environments.
  • Development environments: Provide developers with isolated AWS accounts for testing and development.
  • Client onboarding: Streamline the process of creating dedicated AWS accounts for new clients or business units.
  • Compliance and security: Maintain standardized account configurations with proper IAM roles and policies.
  • Cost management: Enable better cost tracking and resource isolation through dedicated accounts.

Prerequisites

This guide assumes you have:

  • Completed the onboarding process.
  • A GitLab integration installed in your Port organization, with a token that can trigger pipelines. A legacy PAT needs the api scope. For a fine-grained PAT, see the GitLab integration action prerequisites.
  • A GitLab project that will hold the Terraform configuration and pipeline.
  • AWS Organizations access with permissions to create new accounts.
  • AWS IAM credentials for Terraform operations.
Self-hosted GitLab integrations

If you run the GitLab integration self-hosted through Helm or Docker, actions processing is disabled by default and must be enabled explicitly before Port can trigger pipelines. See enable actions processing.

Set up data model

We'll create a blueprint to represent AWS accounts in your Port catalog.

  1. Go to the Builder page in Port.

  2. Click on + Blueprint.

  3. Click on the {...} button in the top right corner, and choose Edit JSON.

  4. Add this JSON schema:

    AWS account blueprint (Click to expand)
    {
    "identifier": "awsAccount",
    "description": "This blueprint represents an AWS account in our context lake.",
    "title": "AWS account",
    "icon": "AWS",
    "schema": {
    "properties": {
    "account_name": {
    "type": "string",
    "title": "Account name",
    "description": "The name of the AWS account."
    },
    "email": {
    "type": "string",
    "format": "email",
    "title": "Root email",
    "description": "The root email address for the account. Must be globally unique across AWS."
    },
    "role_name": {
    "type": "string",
    "title": "IAM role name",
    "description": "The name of the IAM role created for cross-account management."
    },
    "account_id": {
    "type": "string",
    "title": "AWS account ID",
    "description": "The account ID assigned by AWS once the account is created."
    },
    "purpose": {
    "type": "string",
    "title": "Purpose",
    "description": "Why this account was requested."
    },
    "requested_by": {
    "type": "string",
    "format": "user",
    "title": "Requested by"
    },
    "rejection_reason": {
    "type": "string",
    "title": "Rejection reason",
    "description": "Populated when a request is rejected during validation."
    },
    "status": {
    "type": "string",
    "title": "Status",
    "enum": [
    "active",
    "rejected"
    ],
    "enumColors": {
    "active": "green",
    "rejected": "red"
    }
    }
    },
    "required": [
    "account_name",
    "email"
    ]
    },
    "relations": {}
    }
  5. Click Save to create the blueprint.

Relationship to Port's AWS integration

If you also run Port's AWS integration, it ingests AWS accounts into its own blueprint. The awsAccount blueprint above tracks the request and its outcome, including rejected requests that never became real accounts. Keep both, or relate them, depending on how you want to model provisioning history.

Set up the GitLab project

The workflow calls a GitLab pipeline to run Terraform, because creating an AWS account requires signed AWS API calls that a workflow cannot make directly.

Add GitLab CI/CD variables

In your GitLab project, go to Settings > CI/CD > Variables and add the following variables:

  • TF_USER_AWS_KEY - AWS Access Key ID with Organizations permissions.
  • TF_USER_AWS_SECRET - AWS Secret Access Key.
  • TF_USER_AWS_REGION - AWS region for resource creation, for example us-east-1.
No Port credentials needed

The workflow writes to your catalog with a native UPSERT_ENTITY node, so the pipeline never calls Port's API. You do not need PORT_CLIENT_ID or PORT_CLIENT_SECRET here unless you add the optional account ID capture.

Set up Terraform configuration

Create the following Terraform files in your GitLab project.

Main Terraform configuration

Create main.tf:

Terraform main configuration (Click to expand)
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}

provider "aws" {
region = var.region
}

resource "aws_organizations_account" "account" {
name = var.name
email = var.email
role_name = var.role_name
close_on_deletion = true

lifecycle {
ignore_changes = [role_name]
}
}
close_on_deletion closes real accounts

With close_on_deletion = true, running terraform destroy closes the AWS account rather than just removing it from state. Review your state management before using this in production.

Create Terraform variables

Create variables.tf. The variable names here must match the names the pipeline passes with -var:

Terraform variables configuration (Click to expand)
variable "region" {
description = "AWS region where resources will be created"
type = string
default = "us-east-1"
}

variable "name" {
description = "Name of the AWS account to be created"
type = string
}

variable "email" {
description = "Root email to attach to the AWS account"
type = string
}

variable "role_name" {
description = "Name of the IAM role to attach"
type = string
default = "OrganizationAccountAccessRole"
}

Create Terraform outputs

Create outputs.tf:

Terraform outputs configuration (Click to expand)
output "account_id" {
value = aws_organizations_account.account.id
}

output "account_name" {
value = aws_organizations_account.account.name
}

output "email" {
value = aws_organizations_account.account.email
}

output "role_name" {
value = aws_organizations_account.account.role_name
}

Add the GitLab pipeline

Create .gitlab-ci.yml in your project. Port passes the account details as CI/CD variables, so the pipeline reads them directly with $ACCOUNT_NAME, $EMAIL, and $ROLE_NAME:

GitLab CI/CD pipeline (Click to expand)
stages:
- terraform

image:
name: hashicorp/terraform:light
entrypoint:
- '/usr/bin/env'
- 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'

variables:
AWS_ACCESS_KEY_ID: ${TF_USER_AWS_KEY}
AWS_SECRET_ACCESS_KEY: ${TF_USER_AWS_SECRET}
AWS_DEFAULT_REGION: ${TF_USER_AWS_REGION}

create-aws-account:
stage: terraform
rules:
- if: $CI_PIPELINE_SOURCE == "api"
script:
- echo "Creating AWS account ${ACCOUNT_NAME}..."
- terraform init
- terraform apply -auto-approve
-var "name=${ACCOUNT_NAME}"
-var "email=${EMAIL}"
-var "role_name=${ROLE_NAME}"

Port reports the pipeline's outcome back to the workflow automatically, so the pipeline does not need to fetch a Port token, post logs, or patch a run status.

Build the workflow

The diagram below shows the workflow you are about to create. Hover a node to inspect its configuration.

  1. Go to the Workflows page in Port.

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

  3. Click on the {...} button in the top right corner.

  4. Copy and paste the workflow JSON below into the editor to replace the example workflow:

    Create AWS account workflow (Click to expand)
    {
    "identifier": "create_aws_account",
    "title": "Create an AWS account",
    "icon": "AWS",
    "description": "Validate an AWS account request and provision it with GitLab CI/CD and Terraform",
    "allowAnyoneToViewRuns": true,
    "nodes": [
    {
    "identifier": "trigger",
    "title": "Request an AWS account",
    "icon": "AWS",
    "description": "Collect the details for the new AWS account",
    "config": {
    "type": "SELF_SERVE_TRIGGER",
    "userInputs": {
    "properties": {
    "account_name": {
    "icon": "AWS",
    "title": "Account name",
    "description": "The desired name for the new AWS account",
    "type": "string"
    },
    "email": {
    "title": "Root email",
    "description": "The root email for the account. Must not be used by any existing AWS account",
    "type": "string",
    "format": "email"
    },
    "role_name": {
    "title": "IAM role name",
    "description": "The cross-account management role to create",
    "type": "string",
    "default": "OrganizationAccountAccessRole"
    },
    "purpose": {
    "title": "Purpose",
    "description": "What this account will be used for",
    "type": "string"
    }
    },
    "required": [
    "account_name",
    "email",
    "purpose"
    ],
    "order": [
    "account_name",
    "email",
    "purpose",
    "role_name"
    ]
    },
    "contexts": [
    {
    "on": "CREATE_ENTITY",
    "blueprintIdentifier": "awsAccount"
    }
    ],
    "published": true
    },
    "variables": {}
    },
    {
    "identifier": "validate_request",
    "title": "Validate the request",
    "icon": "DefaultProperty",
    "description": "Check the request against existing accounts in the catalog",
    "config": {
    "type": "AI",
    "userPrompt": "A user requested a new AWS account.\n\nAccount name: {{ .outputs.trigger.account_name }}\nRoot email: {{ .outputs.trigger.email }}\nPurpose: {{ .outputs.trigger.purpose }}\n\nList the existing entities of the awsAccount blueprint, then decide whether to approve this request.\n\nReject the request if any of the following is true:\n- An existing awsAccount entity already uses this root email. AWS requires a globally unique root email per account.\n- An existing awsAccount entity already uses this account name.\n- The stated purpose is empty or does not describe a legitimate workload.\n\nSet normalized_account_name to the account name in lowercase, with every character that is not a letter or a digit replaced by a single hyphen.\n\nAlways explain your decision in reason.",
    "systemPrompt": "You are an AWS account provisioning reviewer. Call list_entities at most twice, then return the structured verdict immediately. Do not call any other tools and do not loop. Approve only when you are confident the request is valid. When in doubt, reject and explain why.",
    "tools": [
    "list_blueprints",
    "list_entities"
    ],
    "outputSchema": {
    "type": "object",
    "properties": {
    "approved": {
    "type": "boolean"
    },
    "reason": {
    "type": "string"
    },
    "normalized_account_name": {
    "type": "string"
    }
    },
    "required": [
    "approved",
    "reason",
    "normalized_account_name"
    ]
    }
    },
    "variables": {}
    },
    {
    "identifier": "check_approval",
    "title": "Check validation result",
    "icon": "DefaultProperty",
    "description": "Route the request based on the validation verdict",
    "config": {
    "type": "CONDITION",
    "outlets": [
    {
    "identifier": "approved",
    "title": "Approved",
    "expression": "(.outputs.validate_request.response | fromjson | .approved) == true"
    },
    {
    "identifier": "rejected",
    "title": "Rejected",
    "expression": "(.outputs.validate_request.response | fromjson | .approved) != true"
    }
    ]
    },
    "variables": {}
    },
    {
    "identifier": "provision_account",
    "title": "Provision the AWS account",
    "icon": "GitLab",
    "description": "Trigger the GitLab pipeline that runs Terraform",
    "config": {
    "type": "INTEGRATION_ACTION",
    "integrationProvider": "gitlab-v2",
    "deferIntegrationInstallation": true,
    "integrationInvocationType": "trigger_pipeline",
    "integrationActionExecutionProperties": {
    "project": "<YOUR_GITLAB_PROJECT_PATH>",
    "ref": "main",
    "pipelineVariables": {
    "ACCOUNT_NAME": "{{ .outputs.validate_request.response | fromjson | .normalized_account_name }}",
    "EMAIL": "{{ .outputs.trigger.email }}",
    "ROLE_NAME": "{{ .outputs.trigger.role_name }}"
    },
    "reportPipelineStatus": true
    }
    },
    "variables": {}
    },
    {
    "identifier": "record_account",
    "title": "Record the account",
    "icon": "AWS",
    "description": "Create the AWS account entity in the catalog",
    "config": {
    "type": "UPSERT_ENTITY",
    "blueprintIdentifier": "awsAccount",
    "mapping": {
    "identifier": "{{ .outputs.validate_request.response | fromjson | .normalized_account_name }}",
    "title": "{{ .outputs.trigger.account_name }}",
    "properties": {
    "account_name": "{{ .outputs.trigger.account_name }}",
    "email": "{{ .outputs.trigger.email }}",
    "role_name": "{{ .outputs.trigger.role_name }}",
    "purpose": "{{ .outputs.trigger.purpose }}",
    "requested_by": "{{ .workflowRun.trigger.by.email }}",
    "status": "active"
    }
    }
    },
    "variables": {}
    },
    {
    "identifier": "record_rejection",
    "title": "Record the rejection",
    "icon": "AWS",
    "description": "Track the denied request and why it was denied",
    "config": {
    "type": "UPSERT_ENTITY",
    "blueprintIdentifier": "awsAccount",
    "mapping": {
    "identifier": "{{ .outputs.trigger.account_name | ascii_downcase | gsub(\"[^a-z0-9]+\"; \"-\") }}",
    "title": "{{ .outputs.trigger.account_name }}",
    "properties": {
    "account_name": "{{ .outputs.trigger.account_name }}",
    "email": "{{ .outputs.trigger.email }}",
    "purpose": "{{ .outputs.trigger.purpose }}",
    "requested_by": "{{ .workflowRun.trigger.by.email }}",
    "rejection_reason": "{{ .outputs.validate_request.response | fromjson | .reason }}",
    "status": "rejected"
    }
    }
    },
    "variables": {}
    }
    ],
    "connections": [
    {
    "sourceIdentifier": "trigger",
    "targetIdentifier": "validate_request"
    },
    {
    "sourceIdentifier": "validate_request",
    "targetIdentifier": "check_approval"
    },
    {
    "sourceIdentifier": "check_approval",
    "targetIdentifier": "provision_account",
    "sourceOutletIdentifier": "approved"
    },
    {
    "sourceIdentifier": "check_approval",
    "targetIdentifier": "record_rejection",
    "sourceOutletIdentifier": "rejected"
    },
    {
    "sourceIdentifier": "provision_account",
    "targetIdentifier": "record_account"
    }
    ]
    }
  5. Replace <YOUR_GITLAB_PROJECT_PATH> with your GitLab project path, for example my-group/aws-accounts. You can also use the numeric project ID.

  6. Click Save to save the workflow.

Connect the GitLab integration

The provision_account node uses deferIntegrationInstallation, so the workflow saves without an installation ID and Port resolves it for you.

After publishing, open the setup checklist in the top-right of the workflow graph and click Connect on the GitLab item. Once the integration is connected, the node is satisfied with no further edits.

Secrets are not available in integration actions

Integration action nodes cannot reference organization secrets or encrypted user inputs. Your AWS credentials belong in GitLab's CI/CD variables, as configured above, not in the workflow.

Let's test it!

Test 1: a valid request

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

  2. Click on the Create an AWS account workflow.

  3. Fill in the account details:

    • Account name: A descriptive name for the new AWS account.
    • Root email: A unique email address that no existing AWS account uses.
    • Purpose: What the account is for.
    • IAM role name: The management role to create. Leave the default if you are unsure.
  4. Click on Execute.

  5. Verify that:

    • The validate_request node approves the request.
    • The GitLab pipeline runs and Terraform creates the account.
    • The provision_account node turns green once the pipeline completes.
    • An awsAccount entity appears in your catalog with status active.
    • The account exists in your AWS Organizations console.

Test 2: a duplicate request

  1. Trigger the workflow again with the same root email you used in test 1.

  2. Verify that:

    • The validate_request node rejects the request.
    • The workflow follows the rejected branch and never triggers the pipeline.
    • The entity's rejection_reason explains that the email is already in use.

Capture the AWS account ID (optional)

The default workflow records the request details but not the account ID that AWS assigns, because that value only exists after Terraform runs. To bring it back into the catalog, let the pipeline report the node result itself.

  1. In the provision_account node, set reportPipelineStatus to false and add the node run identifier to pipelineVariables:

    "pipelineVariables": {
    "ACCOUNT_NAME": "{{ .outputs.validate_request.response | fromjson | .normalized_account_name }}",
    "EMAIL": "{{ .outputs.trigger.email }}",
    "ROLE_NAME": "{{ .outputs.trigger.role_name }}",
    "PORT_NODE_RUN_ID": "{{ .workflowNodeRun.identifier }}"
    },
    "reportPipelineStatus": false
  2. Add PORT_CLIENT_ID and PORT_CLIENT_SECRET to your GitLab CI/CD variables. See Port API credentials.

  3. Report the result from the same job that runs Terraform. The Terraform state and the terraform binary are only available inside that job, so a separate reporting job would not be able to read the outputs:

    Report the account ID back to Port (Click to expand)
    create-aws-account:
    stage: terraform
    rules:
    - if: $CI_PIPELINE_SOURCE == "api"
    before_script:
    - apk add --no-cache curl jq
    script:
    - terraform init
    - terraform apply -auto-approve
    -var "name=${ACCOUNT_NAME}"
    -var "email=${EMAIL}"
    -var "role_name=${ROLE_NAME}"
    - |
    ACCOUNT_ID=$(terraform output -raw account_id)

    ACCESS_TOKEN=$(curl -s -X POST \
    -H 'Content-Type: application/json' \
    -d "{\"clientId\": \"${PORT_CLIENT_ID}\", \"clientSecret\": \"${PORT_CLIENT_SECRET}\"}" \
    'https://api.port.io/v1/auth/access_token' | jq -r '.accessToken')

    curl -X PATCH \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer ${ACCESS_TOKEN}" \
    -d "{\"status\": \"COMPLETED\", \"result\": \"SUCCESS\", \"output\": {\"account_id\": \"${ACCOUNT_ID}\"}, \"links\": [\"${CI_PIPELINE_URL}\"]}" \
    "https://api.port.io/v1/workflows/nodes/runs/${PORT_NODE_RUN_ID}"
    after_script:
    - |
    if [ "$CI_JOB_STATUS" != "success" ]; then
    ACCESS_TOKEN=$(curl -s -X POST \
    -H 'Content-Type: application/json' \
    -d "{\"clientId\": \"${PORT_CLIENT_ID}\", \"clientSecret\": \"${PORT_CLIENT_SECRET}\"}" \
    'https://api.port.io/v1/auth/access_token' | jq -r '.accessToken')

    curl -X PATCH \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer ${ACCESS_TOKEN}" \
    -d "{\"status\": \"COMPLETED\", \"result\": \"FAILED\", \"links\": [\"${CI_PIPELINE_URL}\"]}" \
    "https://api.port.io/v1/workflows/nodes/runs/${PORT_NODE_RUN_ID}"
    fi
  4. Add the account ID to the record_account node's mapping:

    "account_id": "{{ .outputs.provision_account.account_id }}"
The pipeline owns the node result

With reportPipelineStatus set to false, Port stops tracking the pipeline and the node stays in progress until your pipeline patches it. The after_script above covers the failure case, so a failed Terraform run does not leave the node hanging. If you restructure this job, keep a failure path that reports FAILED.

Conclusion

You now have a single workflow that validates AWS account requests against your catalog, provisions approved ones through GitLab CI/CD and Terraform, and records both approvals and rejections in Port.

To extend it further:

  • Add an input node so a platform engineer signs off before provisioning.
  • Add a WEBHOOK node to notify the requesting team in Slack.
  • Relate the awsAccount blueprint to your teams or cost center blueprints for ownership and chargeback.
  • Explore more workflow examples for inspiration.