| name | port-terraform |
| description | Author and manage Port blueprints, entities, scorecards, and self-service actions with the official Terraform provider (port-labs/port-labs). Covers provider setup and auth, every resource type (port_blueprint, port_entity, port_scorecard, port_action, port_webhook, port_integration, port_aggregation_properties), the create/update/delete/import lifecycle, and known limitations that produce broken or silently wrong config. Use when asked to "add a Port resource to Terraform", "manage Port as code", "write a port_blueprint/port_entity/port_action/port_scorecard resource", "import a Port blueprint into Terraform state", or "why did my Port terraform apply not do what I expected". |
| license | MIT |
| compatibility | Claude Code, Cursor, Codex CLI, GitHub Copilot |
| metadata | {"version":"1.0.0","author":"port-labs","repository":"https://github.com/port-labs/port-skills","tags":"port,terraform,iac,reference","summary":"Manage Port resources as code with the Terraform provider"} |
Port Terraform provider
Port's Terraform provider (port-labs/port-labs) lets you define blueprints,
entities, scorecards, self-service actions, webhooks, and integration
mappings as .tf resources, the same way you'd define any other piece of
infrastructure. This skill is reference-only: static knowledge about the
provider's resource schema, drawn from the current v2.x provider and Port's
Terraform docs. It doesn't need a live Port account or Port MCP server to
produce correct HCL, only a real account and API credentials to actually
terraform apply it.
Out of scope: the underlying Port concepts (property types, relation
semantics) are owned by the port-blueprints skill; self-service action
JSON schema and invocation method payload templating aren't covered by a
skill in this repo yet. This skill only covers how those concepts map onto
Terraform arguments. Pulumi is Port's
other IaC provider, briefly noted in Step 1,
but not covered in depth here.
Prerequisites
- Go over the
port-getting-started skill first if this is your first time working with Port.
- The Terraform CLI (or OpenTofu, a drop-in alternative, swap
terraform for tofu).
- A Port account and a
client_id/client_secret pair, from ... menu > Credentials in the Port app, or the API docs.
- No Port MCP server or CLI is required to write or apply the
.tf files this skill produces. If one is connected, use it to check the current live state (list_blueprints) before writing HCL that should match it, and search search_port_knowledge_sources for anything this skill doesn't cover.
Step 1 - Set up the provider
Precondition: no port provider block exists in the working directory yet.
Action: add a terraform block pinning port-labs/port-labs, and a
provider "port" block with credentials from environment variables, then
run terraform init:
terraform {
required_providers {
port = {
source = "port-labs/port-labs"
version = "~> 2.4.0"
}
}
}
provider "port" {
client_id = "" # or set the environment variable PORT_CLIENT_ID
secret = "" # or set the environment variable PORT_CLIENT_SECRET
base_url = "https://api.port.io"
}
terraform init
Fallback: if the user's stack is Python, TypeScript, JavaScript, or Go
instead of HCL, Port also ships a Pulumi provider with the same Entity
resource concept. Point them at
docs.port.io's Pulumi page
instead of forcing Terraform.
Step 2 - Model data with port_blueprint
Precondition: the context lake needs a new type, or an existing blueprint needs
to move under Terraform management.
Action: write a port_blueprint resource with identifier, title, typed
properties, and any relations to other blueprints. See
references/resource-reference.md for
the full property-type and relation argument tables, and
assets/blueprint.tf for a complete example.
Fallback: if a relation's target blueprint doesn't exist yet, define it
first, port_blueprint.relations requires an existing target and Terraform
won't infer creation order across two relations pointing at each other
without an explicit depends_on.
Step 3 - Populate the context lake with port_entity
Precondition: a blueprint exists (via Terraform or otherwise) and you need
to create, update, or manage a specific instance of it.
Action: write a port_entity resource with blueprint, title, an
explicit identifier, and properties/relations matching the blueprint's
schema. See assets/entity.tf for single and many
relations together.
Fallback: if you're bringing an entity that already exists in Port under
Terraform management, don't start from a partial definition, see
Limitations #3 first: unspecified properties
get overridden with empty values, not left alone.
Step 4 - Add scorecards with port_scorecard
Precondition: entities of a blueprint need a maturity or readiness score.
Action: write a port_scorecard resource with blueprint, rules
(identifier, title, level, query), and optionally custom levels.
Each rule's query.conditions is a list of jsonencode()'d condition
objects, not native HCL. See
assets/scorecard.tf.
Fallback: if a rule's level doesn't match one of the scorecard's levels
(or the defaults Basic/Bronze/Silver/Gold when levels is omitted),
terraform apply rejects the rule, define the levels first.
Step 5 - Add self-service actions and automations with port_action
Precondition: users need a form to trigger a backend (self-service action),
or an event in Port needs to trigger one automatically (automation).
Action: write a port_action resource with exactly one of
self_service_trigger / automation_trigger, and exactly one invocation
method block (webhook_method, github_method, gitlab_method,
azure_method, kafka_method, upsert_entity_method, or
integration_method). See
references/resource-reference.md for
the full trigger and invocation-method argument tables, and
assets/action.tf for a self-service action plus an
automation in one file.
Fallback: if a user_properties entity-picker dataset mixes a static rule
with a jq_query rule, don't use the native HCL dataset block, wrap the
whole thing in jsonencode(), see
Limitations #1.
Step 6 - Sync webhooks, integrations, and aggregations
Precondition: you need a custom inbound webhook (port_webhook), to bring
an already-installed Ocean integration's mapping under version control
(port_integration), or to add computed count/average properties across
related entities (port_aggregation_properties).
Action: see the per-resource argument tables in
references/resource-reference.md.
port_integration manages an existing installation only, always
terraform import it first (Step 7).
Fallback: a port_aggregation_properties property returns null or 0
with no error when more than one relation path connects the two blueprints
and path_filter isn't set, see
Limitations #2.
Step 7 - Import existing Port resources into Terraform state
Precondition: a blueprint, entity, scorecard, action, webhook, integration,
or aggregation property already exists in Port and needs to come under
Terraform management.
Action: add a resource block (even an empty one to start), then run
terraform import <resource_type>.<name> "<id>" with the ID format from the
import table,
then terraform plan to check for drift before the next apply.
Fallback: if the resource block doesn't match the imported state,
terraform plan shows the mismatch as drift to fix, and applying it will
overwrite (or empty out) the real Port resource, don't apply until the plan
shows no unexpected changes.
Examples
Complete, standalone .tf files (each includes its own terraform and
provider block):
Common pitfalls
| Symptom | Cause | Fix |
|---|
port_action config rejected, or built from a stale example | Top-level user_properties/operation on port_action is the deprecated v1.x shape | Nest under self_service_trigger or automation_trigger, see Limitations #6 |
| Entity-picker dataset doesn't filter correctly | Mixed static and jq_query rules in a native HCL dataset block | jsonencode() the whole dataset object, see Limitations #1 |
Aggregation property is null/0 with no error | Ambiguous relation path between blueprints | Add path_filter, see Limitations #2 |
| Updating an existing entity wipes properties you didn't touch | port_entity uses create/override, not merge | Reproduce the entity's full current state in the resource body |
terraform destroy fails on a blueprint | Entities exist that weren't created by this Terraform state | Set force_delete_entities = true before destroying |
Changing a property's type is blocked | blueprint_property_type_change_protection defaults to true | Add a new property with the right type, migrate data, remove the old one, don't disable the protection |
Drift wipes a resource right after terraform import | Resource block doesn't match the real imported state | Reconcile the block with terraform plan before the first apply |
Quick reference
- Provider:
provider "port" { client_id, secret, base_url }, or
PORT_CLIENT_ID/PORT_CLIENT_SECRET env vars.
- Resources:
port_blueprint, port_entity, port_scorecard,
port_action, port_webhook, port_integration,
port_aggregation_properties.
- Property groups (blueprint and entity):
string_props, number_props,
boolean_props, object_props, array_props.
- Relation groups (entity only):
single_relations, many_relations.
- Action triggers:
self_service_trigger (CREATE/DAY-2/DELETE) or
automation_trigger (event-based). Exactly one per action.
- Action invocation methods:
webhook_method, github_method,
gitlab_method, azure_method, kafka_method, upsert_entity_method,
integration_method. Exactly one per action.
- Import ID formats: full table in
references/resource-reference.md.
- Full argument reference:
references/resource-reference.md.
Known gotchas: references/limitations.md.