Terraform
Our integration with Terraform allows you to combine the state of your infrastructure with the entities representing them in Port.
By using Port's Terraform provider you make it easy to integrate Port with your existing IaC definitions, every resource provisioned by Terraform can also be reported to the software catalog using the same .tf definition file.
You can view the official registry page for our Terraform provider here
Terraform provider common use cases
Our Terraform provider makes it easy to fill the software catalog with data directly from your IaC definitions, for example:
- Report cloud accounts.
- Report databases.
- Report lambdas and managed Kubernetes services (EKS, AKS, GKE, etc.).
Prerequisites
To install and use Port's Terraform provider, you will need to install the Terraform CLI.
Port's provider is also compatible with OpenTofu, the open-source Terraform alternative. Simply replace terraform commands with tofu in the installation steps below.
Installation
To install the Terraform provider, set your Port credentials as environment variables and create a .tf file with the provider configuration.
export PORT_CLIENT_ID="<PORT_CLIENT_ID>"
export PORT_CLIENT_SECRET="<PORT_CLIENT_SECRET>"
In your .tf file, configure the port-labs/port-labs provider:
terraform {
required_providers {
port = {
source = "port-labs/port-labs"
version = "~> 2.4.0"
}
}
}
provider "port" {
base_url = "https://api.port.io"
}
The port_region, port.baseUrl, portBaseUrl, port_base_url and OCEAN__PORT__BASE_URL parameters select which Port API instance to use:
- EU (app.port.io) →
https://api.port.io - US (app.us.port.io) →
https://api.us.port.io
Then run the following command to install the provider in your Terraform workspace:
terraform init
Terraform definition structure
Port's Terraform provider supports The following resources to ingest data to the catalog:
port_entity
The port_entity resource defines a basic entity:
resource "port_entity" "myEntity" {
identifier = "myEntity" # Entity identifier
title = "My Entity" # Entity title
blueprint = "myBlueprint" # Identifier of the blueprint to create this entity from
# Entity property values
properties = {
...
}
...
# Entity relations
...
}
The following parameters are required:
blueprint- the identifier of the blueprint to create this entity from.title- the title of the entity.- One or more
propertiesschema definitions.
It is also possible to specify the following parameters as part of the port_entity resource:
identifier- the identifier of the entity.- If an
identifieris not provided, an identifier will be autogenerated.
- If an
teams- an array of teams that own the entity.run_id- the run ID of the action that created the entity.
properties schema
The properties schema assigns a specified value to one of the entity's properties.
resource "port_entity" "myEntity" {
identifier = "myEntity" # Entity identifier
title = "My Entity" # Entity title
blueprint = "myBlueprint" # Identifier of the blueprint to create this entity from
properties = {
string_props = {
"myStringProp" = "My string"
}
number_props = {
"myNumberProp" = 7
}
array_props = {
string_items = {
"myArrayProp" = ["a", "b", "c"]
}
}
}
# Entity relations
...
}
Definition
- String
- Number
- Boolean
- Object
- Array
- URL
- User
- Team
- Datetime
- Timer
- YAML
properties = {
string_props = {
"myStringProp" = "My string"
}
}
properties = {
number_props = {
"myNumberProp" = 7
}
}
properties = {
boolean_props = {
"myBooleanProp" = true
}
}
properties = {
object_props = {
"myObjectProp" = jsonencode({ "my" : "object" })
}
}
properties = {
array_props = {
string_props = {
"myArrayProp" = ["a", "b", "c"])
}
}
}
properties = {
string_props = {
"myUrlProp" = "https://example.com"
}
}
properties = {
string_props = {
"myEmailProp" = "me@example.com"
}
}
properties = {
string_props = {
"myUserProp" = "argo-admin"
}
}
properties = {
string_props = {
"myTeamProp" = "argo-admins"
}
}
properties = {
string_props = {
"myDatetimeProp" = "2023-04-18T11:44:15.345Z"
}
}
properties = {
string_props = {
"myUserProp" = "argo-admin"
}
}
properties = {
string_props = {
"myYamlProp" = "myKey: myValue"
}
}
relations schema
The relations schema maps a target entity to the source entity definition:
resource "port_entity" "myEntity" {
identifier = "myEntity" # Entity identifier
title = "My Entity" # Entity title
blueprint = "myBlueprint" # Identifier of the blueprint to create this entity from
# Entity properties
...
relations = {
single_relations = {
"mySingleRelation" = "myTargetEntityIdentifier"
}
}
relations = {
many_relations = {
"myManyRelation" = ["myTargetEntityIdentifier", "myTargetEntityIdentifier2"]
}
}
}
Definition
- Single
- Many
the schema is as follows:
relations {
single_relations = {
# Key-value pair of the relation identifier and the target identifier
"mySingleRelation" = "myTargetEntityIdentifier"
}
}
the schema is as follows:
relations {
many_relations = {
# Key-value pair of the relation identifier and the target identifiers
"myManyRelation" = ["myTargetEntityIdentifier", "myTargetEntityIdentifier2"]
}
}
Ingest data using the Terraform provider
To ingest data to the software catalog using the Terraform provider, you will define port_entity resources in your Terraform definition files:
- Create
- Update
- Delete
To create an entity using Terraform, add a port_entity resource to your .tf definition file:
resource "port_entity" "myEntity" {
identifier = "myEntity"
title = "My Entity"
blueprint = "myBlueprint"
properties = {
"string_props" = {
"myStringProp" = "My string"
}
"number_props" = {
"myNumberProp" = 7
}
"boolean_props" = {
"myBooleanProp" = true
}
"object_props" = {
"myObjectProp" = jsonencode({ "my" : "object" })
}
"array_props" = {
"string_props" = {
"myArrayProp" = ["a", "b", "c"]
}
}
}
}
Then run the following commands to apply your changes and update the catalog:
# To view Terraform's planned changes based on your .tf definition file:
terraform plan
# To apply the changes and update the catalog
terraform apply
After running these commands, you will see your catalog updated with the new entities.
To update an entity using Terraform, update the existing port_entity resource in your .tf definition file and then run terraform apply.
It is also possible to start managing existing entities using Port's Terraform provider, to begin managing an existing entity, add a new port_entity resource to your .tf definition file and make the desired changes:
resource "port_entity" "myExistingEntity" {
identifier = "myExistingEntity"
title = "My Entity"
blueprint = "myBlueprint"
# Entity properties and relations
...
}
Important notes about adding existing entities to the Terraform provider:
- It is important to specify the
identifierof the entity, otherwise terraform will create a new entity with an autogenerated identifier. - Port's Terraform provider uses the create/override strategy, meaning for an existing entity, any properties not defined in the resource definition will be overridden with empty values.
To delete an entity using Terraform, simply remove the port_entity resource defined in your .tf definition file and then run terraform apply.
Import existing data to the Terraform state
- Blueprint
- Entity
- Scorecard
- Aggregation Property
- Actions/Automations
- Webhook
- Integration
To import an existing blueprint to the Terraform state, add a port_blueprint resource to your .tf definition file:
resource "port_blueprint" "myBlueprint" {
...
}
Then run the following command to import the blueprint to the Terraform state:
terraform import port_blueprint.myBlueprint "{blueprintIdentifier}"
To import an existing entity to the Terraform state, add a port_entity resource to your .tf definition file:
resource "port_entity" "myEntity" {
...
}
Then run the following command to import the entity to the Terraform state:
terraform import port_entity.myEntity "{blueprintIdentifier}:{entityIdentifier}"
To import an existing scorecard to the Terraform state, add a port_scorecard resource to your .tf definition file:
resource "port_scorecard" "myScorecard" {
...
}
Then run the following command to import the scorecard to the Terraform state:
terraform import port_scorecard.myScorecard "{blueprintIdentifier}:{scorecardIdentifier}"
To import existing aggregation properties to the Terraform state, add a port_aggregation_properties resource to your .tf definition file:
resource "port_aggregation_properties" "myAggregationProperty" {
...
}
Then run the following command to import the aggregation property to the Terraform state:
terraform import port_aggregation_properties.myAggregationProperty "{blueprintIdentifier}"
Unlike other resources, all aggregation properties of a blueprint are managed in a single Terraform resource, which is why the import command only requires the blueprint identifier.
To import an existing action to the Terraform state, add a port_action resource to your .tf definition file:
resource "port_action" "myAction" {
...
}
Then run the following command to import the action to the Terraform state:
terraform import port_action.myAction "{actionIdentifier}"
To import an existing webhook to the Terraform state, add a port_webhook resource to your .tf definition file:
resource "port_webhook" "myWebhook" {
...
}
Then run the following command to import the webhook to the Terraform state:
terraform import port_webhook.myWebhook "{webhookIdentifier}"
The port_integration resource can be used to bring an already installed integration (self-hosted, Port Hosted Saas, or OAuth SaasOAuth2) under Terraform management, without recreating it.
To import an existing integration to the Terraform state, add a port_integration resource to your .tf definition file:
resource "port_integration" "myIntegration" {
...
}
Then run the following command to import the integration to the Terraform state:
terraform import port_integration.myIntegration "{installationId}"
Use the port_integration resource with terraform import to bring existing integration configuration into Terraform state. After import, update your Terraform resource definition to match the integration configuration before running terraform apply.
Note: Importing is only needed for an integration that already exists. Self-hosted (OnPrem) and Port Hosted (Saas) integrations can also be created entirely from Terraform. OAuth-authorized Port Hosted integrations (SaasOAuth2) are import-only — authorize them in the Port UI first, then import.
Manage integrations
The port_integration resource can create and manage self-hosted (OnPrem) and Port Hosted (Saas) integrations from your .tf definition file — no prior UI install is required for those types. OAuth-authorized Port Hosted integrations (SaasOAuth2) cannot be created via Terraform: authorize them in the Port UI, then import and manage mappings / optional appSpec.
Integrations receive default mappings during provisioning. Because of this, config cannot be set when creating an integration - it would be overwritten. Instead:
- First apply - create the integration without
config. Provisioning sets up default blueprints and mappings. - Second apply - add a
configblock to override the default mappings.
Port Hosted integrations provision asynchronously (Creating → Running). The provider waits for provisioning to complete before continuing. Self-hosted create/update/delete are synchronous.
Resource parameters
| Parameter | Required | Description |
|---|---|---|
installation_id | Yes | The integration installation ID. Must match ^[a-z0-9-]+$ (lowercase letters, numbers, and dashes only). Cannot be changed after creation. |
installation_type | No | OnPrem (default) for self-hosted, Saas for Port Hosted created via Terraform, or SaasOAuth2 for OAuth-authorized Port Hosted (import-only). Cannot be changed after creation. |
installation_app_type | No | Catalog integration tool name (e.g. linear, github-ocean, jira). Required for Port Hosted. Cannot be changed after creation. |
title | No | Display title for the integration. |
spec | Port Hosted only | Port Hosted spec as a JSON string (jsonencode). Supported for Saas and SaasOAuth2 — do not set for self-hosted. For Saas, required and must include integrationSpec (credentials/settings) plus optional appSpec (feature toggles). For SaasOAuth2, optional and may only include appSpec — OAuth credentials cannot be set via Terraform. |
config | No | Integration mapping config as a JSON string. Use jsonencode(). Cannot be set on creation — add it on a subsequent apply. |
create_port_resources_origin | No | Set on creation only. Use Empty to skip default blueprints/mappings, or Port to create them via Port. If omitted, defaults are created. |
version | No | Integration version. By default, the version can change outside of Terraform. Set this only if you want Terraform to control the version. |
webhook_changelog_destination | No | Webhook changelog destination with url (required) and agent (optional). |
kafka_changelog_destination | No | Kafka changelog destination. Set to {}. |
status | Read-only | Provisioning status (e.g. Creating, Running). Relevant for Port Hosted integrations. |
Port Hosted integration example (Saas)
For Port Hosted integrations created via Terraform (installation_type = "Saas"), create organization secrets with port_organization_secret, then reference the secret names (not values) in spec.integrationSpec. Spec fields vary by integration type — check the integration's configuration schema for the exact keys.
Port Hosted example — Linear (click to expand)
variable "linear_api_key" {
type = string
sensitive = true
}
locals {
installation_id = "linear-prod"
integration_type = "linear"
}
resource "port_organization_secret" "linear_api_key" {
secret_name = "linear_api_key"
secret_value = var.linear_api_key
}
resource "port_integration" "linear" {
depends_on = [port_organization_secret.linear_api_key]
installation_id = local.installation_id
installation_app_type = local.integration_type
installation_type = "Saas"
title = "Linear Production"
spec = jsonencode({
integrationSpec = {
linearApiKey = port_organization_secret.linear_api_key.secret_name
}
# Declare appSpec explicitly — Port defaults may differ from the UI.
appSpec = {
scheduledResyncInterval = "12h"
liveEventsEnabled = false
}
})
# Do NOT set config here — provisioning populates default mappings.
# Add config on a subsequent apply to override them.
}
After the first apply completes and the integration is Running, add config to the same resource and apply again to override the default mappings.
The same pattern works for other catalog integrations that use token/secret credentials (for example GitHub Ocean with a PAT, Jira with an API token, Azure DevOps, GitLab v2): set installation_type = "Saas", pass the matching installation_app_type, and put credentials in spec.integrationSpec via organization secret names. For OAuth-authorized installs (Connect in the Port UI), use SaasOAuth2 instead — see below.
OAuth Port Hosted integration example (SaasOAuth2)
OAuth-authorized Port Hosted integrations (for example GitHub App or Jira via Connect) must be installed in the Port UI first. Terraform cannot create them. After authorization, import the installation and manage mappings and optional appSpec toggles — not OAuth credentials.
terraform import port_integration.github_oauth <installation_id>
SaasOAuth2 example - GitHub Ocean after UI OAuth (click to expand)
resource "port_integration" "github_oauth" {
installation_id = "github-oauth-prod"
installation_app_type = "github-ocean"
installation_type = "SaasOAuth2"
# Optional — appSpec only. Do not set integrationSpec.
spec = jsonencode({
appSpec = {
scheduledResyncInterval = "12h"
liveEventsEnabled = true
}
})
# Add config on a subsequent apply after import, once defaults exist.
# config = jsonencode({ ... })
}
Self-hosted integration example
Self-hosted integrations run on your own infrastructure (e.g. an Ocean exporter container) and pull their mapping from Port. Omit installation_type (defaults to OnPrem) and do not set spec.
Example configuration (click to expand)
resource "port_integration" "my_custom_integration" {
installation_id = "my-custom-integration-id"
title = "My Custom Integration"
# config is set on the next apply, after provisioning creates default mappings.
}
After the first apply, add mappings:
resource "port_integration" "my_custom_integration" {
installation_id = "my-custom-integration-id"
title = "My Custom Integration"
config = jsonencode({
createMissingRelatedEntities = true
deleteDependentEntities = true
resources = [
{
kind = "my-custom-kind"
selector = {
query = ".title"
}
port = {
entity = {
mappings = [
{
identifier = "'my-identifier'"
title = ".title"
blueprint = "'my-blueprint'"
properties = {
my_property = 123
}
relations = {}
}
]
}
}
}
]
})
}
For catalog integration types, set installation_app_type to the integrated tool name (e.g. github-ocean, gitlab, K8S EXPORTER) and version if you want to pin a specific integration version. Custom integrations can omit installation_app_type.
Catalog integration example (click to expand)
resource "port_integration" "my_k8s_exporter" {
installation_id = "my-k8s-exporter"
title = "My K8S Exporter with version managed by Terraform"
installation_app_type = "K8S EXPORTER"
# NOTE: version can change outside of Terraform by default.
# Include this only if you explicitly want Terraform to control the version.
version = "1.33.7"
config = jsonencode({
createMissingRelatedEntities = true
deleteDependentEntities = true
resources = [
{
kind = "apps/v1/replicasets"
selector = {
query = ".metadata.namespace | startswith(\"kube\") | not"
}
port = {
entity = {
mappings = [
{
identifier = ".metadata.name"
title = ".metadata.name"
blueprint = "'deploymentConfig'"
properties = {
creationTimestamp = ".metadata.creationTimestamp"
annotations = ".metadata.annotations"
status = ".status"
}
}
]
}
}
}
]
})
}
You can also configure a changelog destination:
Webhook changelog destination example (click to expand)
resource "port_integration" "my_integration_with_webhook" {
installation_id = "my-integration-with-webhook"
title = "My Integration"
installation_app_type = "kafka"
config = jsonencode({
deleteDependentEntities = true
resources = [
{
kind = "ZOMG"
selector = {
query = ".title"
}
port = {
entity = {
mappings = [
{
identifier = "'my-identifier'"
title = ".title"
blueprint = "'my-blueprint'"
properties = {
bla = 123
}
relations = {}
}
]
}
}
}
]
})
webhook_changelog_destination = {
url = "https://example.com/webhook"
agent = true
}
}
Then run the following commands to create the integration and apply its mapping:
terraform plan
terraform apply
The selector.query and entity.mappings.* config properties are jq expressions: use a valid jq expression (e.g. .title), or a quoted, escaped string literal (e.g. 'my-string') for a static value.
To update the integration's config or mappings, change the port_integration resource definition and run terraform apply again.
Integration constraints
configcannot be set on creation. Create first, then addconfigon a subsequent apply.installation_id,installation_app_type, andinstallation_typecannot be changed after creation. To use different values, destroy the resource and create a new one.specis only supported for Port Hosted integrations (installation_type = "Saas"or"SaasOAuth2"). Do not set it on self-hosted integrations.SaasOAuth2is import-only: authorize the integration in the Port UI first, thenterraform import. Terraform cannot create OAuth installs. ForSaasOAuth2,specmay only containappSpec— OAuth-managedintegrationSpeccredentials cannot be set or updated via Terraform.- Port Hosted
specis validated at plan time against the integration type definition in Port. ForSaasOAuth2, plan-time validation runs whenspec(appSpec) is declared in HCL. - A changelog destination (
webhook_changelog_destination/kafka_changelog_destination) can be added or updated, but not removed. The Port API does not support clearing it. To remove a changelog destination, delete and recreate the integration (e.g. taint the resource). terraform destroydeletes the integration from Port, not just from Terraform state. Useterraform state rmif you only want to stop managing an integration with Terraform without deleting it from Port. This is especially relevant for imported resources.- Existing integrations can be brought under Terraform management with
terraform import port_integration.my_integration <installation_id>. This is required forSaasOAuth2.
Before using terraform import to bring data from your Port account into your Terraform state file, make sure your resource definitions match the schema of your resources in Port. If they don't, your state will be deleted in the next terraform apply, since Terraform will try to apply the empty resources and override the imported state, while also updating Port in the process.
Limitations
Mixed static and dynamic values in dataset rules
When defining dataset rules in self-service actions that mix static values with jq_query values, the Terraform provider may not handle them correctly.
To work around this, use jsonencode() for the entire dataset object:
resource "port_action" "myAction" {
# ...action properties
user_properties = {
string_props = {
"myEntityInput" = {
format = "entity"
blueprint = "myBlueprint"
dataset = jsonencode({
combinator = "and"
rules = [
{
property = "$identifier"
operator = "in"
value = {
jqQuery = ".user.relations.teams[].identifier"
}
},
{
property = "status"
operator = "="
value = "active"
}
]
})
}
}
}
}
Aggregation properties require path_filter when multiple relation paths exist
A port_aggregation_properties resource that counts related entities returns null or 0 if more than one relation path exists between the source and target blueprints and path_filter is not specified. terraform apply exits successfully with no error.
Add path_filter to disambiguate which relation path to use:
resource "port_aggregation_properties" "service_props" {
blueprint_identifier = "service"
properties = {
"open_incidents" = {
target_blueprint_identifier = "incident"
method = {
count_entities = true
}
path_filter = [
{
path = ["service"]
}
]
query = jsonencode({
combinator = "and"
rules = [{ property = "status", operator = "=", value = "open" }]
})
}
}
}
path_filter is only required when more than one relation path connects the two blueprints. If there is exactly one direct relation, it can be omitted.