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

Check out Port for yourself ➜ 

Create and manage Kubernetes clusters

Implement with AI

Send this guide to your coding agent.

Prerequisite: Install Port MCP

This guide walks you through setting up a workflow that creates and deletes Kubernetes clusters with Crossplane and ArgoCD.

The workflow we will create dispatches GitHub Actions to update a manifests repository. ArgoCD syncs that repository into a management cluster. Crossplane reconciles each claim after you add a composition and cloud credentials. The catalog stays in sync, so the requested cluster is visible in Port from the moment it is requested.

Because every self-service workflow is automatically exposed as a tool through the Port MCP server, the same flow works without the UI: agents discover it with list_self_service_triggers and execute it with trigger_run.

Common use cases​

  • Let developers and agents provision a Kubernetes cluster without writing Crossplane manifests by hand.
  • Keep cluster creation and deletion behind a governed workflow that updates Git, then the catalog.
  • Track requested clusters, their provider, and their status from a single catalog page.

Prerequisites​

This guide assumes the following:

  • You have a Port account and have completed the onboarding process.
  • Port's GitHub integration is installed. The workflow uses it to dispatch GitHub Actions workflows. If you self-host the integration, enable actions processing and set the live events base URL, otherwise the dispatch steps never run or never finish.
  • A Kubernetes management cluster you can reach with kubectl. We will install Crossplane and ArgoCD on it. If you do not have a cluster yet, create one locally with kind.
  • Helm installed on your machine.
  • A GitHub repository for the workflow files, scripts, and infrastructure manifests. Under Settings > Actions > General > Workflow permissions, the repository must allow GITHUB_TOKEN read and write access, so the pipelines can push changes. The Port GitHub App must also have access to this repository. If you create the repository after installing the integration, add it to the app's repository access and resync the integration, so it can dispatch workflows and receive their completion events.
  • A management cluster that is able to pull from that GitHub repository.

How it works​

The setup has four parts:

  1. The control plane - Crossplane and ArgoCD run on a management cluster, with provider manifests and a cluster claim template in your repository.
  2. The backend - GitHub Actions workflows copy a cluster claim template into infra/ or delete it, then push the change.
  3. The workflow - A self-service trigger collects the cluster properties, an integration action dispatches the matching GitHub Actions workflow, and an upsert entity or delete step updates the cluster blueprint in the catalog.
  4. Connect GitOps - An ArgoCD application syncs the infra folder into the management cluster so Crossplane can reconcile each cluster claim.

Set up the control plane​

We will install Crossplane and ArgoCD on the management cluster, then add the provider manifests and the cluster claim template to your GitHub repository. This section runs on the cluster. Port MCP cannot install Helm charts or apply these manifests, so use kubectl here, or an agent that can reach the cluster.

Create these folders in that repository as you go: crossplane-config, compositions, .github/workflows, scripts, argocd, and infra. Commit an empty infra/.gitkeep before you apply the ArgoCD application, so the infra path already exists in Git.

  1. Install Crossplane into the management cluster.
helm repo add crossplane-stable https://charts.crossplane.io/stable

helm repo update

helm upgrade --install crossplane crossplane-stable/crossplane --namespace crossplane-system --create-namespace --wait
  1. Install ArgoCD into the cluster.
kubectl create namespace argocd

kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

kubectl get pods -n argocd

ArgoCD comes with a default user: admin

To get the password, run:

kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

To access the UI, port-forward the API server:

kubectl port-forward svc/argocd-server -n argocd 8080:443

Open https://localhost:8080 to access the API server.

  1. Create the following provider manifests in the crossplane-config folder. These providers manage Kubernetes and Helm objects inside the management cluster. They do not create a cloud cluster. Each provider also needs a ProviderConfig that uses InjectedIdentity, so it authenticates with the service account from its runtime config. The provider package installs that CRD, so save the ProviderConfig files now and apply them in the next step, after the providers are healthy.
Crossplane compositions

Compositions are templates for creating multiple managed resources as a single object. The claim template later in this guide selects a composition by the provider and cluster labels. Defining that composition and the cloud credentials it needs is outside this guide.

crossplane-config/provider-kubernetes-incluster.yaml (click to expand)
crossplane-config/provider-kubernetes-incluster.yaml
---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: crossplane-provider-kubernetes
subjects:
- kind: ServiceAccount
name: crossplane-provider-kubernetes
namespace: crossplane-system
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io

---

apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: crossplane-provider-kubernetes
spec:
serviceAccountTemplate:
metadata:
name: crossplane-provider-kubernetes

---

apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: crossplane-provider-kubernetes
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-kubernetes:v1.4.1
runtimeConfigRef:
name: crossplane-provider-kubernetes
crossplane-config/provider-helm-incluster.yaml (click to expand)
crossplane-config/provider-helm-incluster.yaml
---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: crossplane-provider-helm
subjects:
- kind: ServiceAccount
name: crossplane-provider-helm
namespace: crossplane-system
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io

---

apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: crossplane-provider-helm
spec:
serviceAccountTemplate:
metadata:
name: crossplane-provider-helm

---

apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: crossplane-provider-helm
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-helm:v1.4.0
runtimeConfigRef:
name: crossplane-provider-helm
crossplane-config/provider-kubernetes-config.yaml (click to expand)
crossplane-config/provider-kubernetes-config.yaml
---
apiVersion: kubernetes.crossplane.io/v1alpha1
kind: ProviderConfig
metadata:
name: kubernetes-provider
spec:
credentials:
source: InjectedIdentity
crossplane-config/provider-helm-config.yaml (click to expand)
crossplane-config/provider-helm-config.yaml
---
apiVersion: helm.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
name: helm-provider
spec:
credentials:
source: InjectedIdentity
  1. Apply the provider manifests, wait until the packages are healthy, then apply the ProviderConfig files:
kubectl apply --filename ./crossplane-config/provider-kubernetes-incluster.yaml

kubectl apply --filename ./crossplane-config/provider-helm-incluster.yaml

kubectl wait --for=condition=healthy provider.pkg.crossplane.io --all --timeout=300s

kubectl apply --filename ./crossplane-config/provider-kubernetes-config.yaml

kubectl apply --filename ./crossplane-config/provider-helm-config.yaml
  1. Save the cluster claim template below as compositions/cluster-template.yaml. The GitHub Actions workflow copies this file for each new cluster.
compositions/cluster-template.yaml
---
apiVersion: devopstoolkitseries.com/v1alpha1
kind: ClusterClaim
metadata:
name: NAME
namespace: infra
spec:
id: NAME
compositionSelector:
matchLabels:
provider: PROVIDER
cluster: CLUSTER
parameters:
nodeSize: SIZE
minNodeCount: 1

Add the GitHub Actions pipelines​

The Port workflow dispatches GitHub Actions workflows to update your manifests repository. These pipelines still scaffold the cluster manifests, so we create them first. This section writes files in that repository. You push them together with the rest of the files in Push the manifests.

  1. Create .github/workflows/create-cluster.yaml. It checks out the repository, runs create-cluster.sh, and pushes the new manifest.
Create cluster workflow (click to expand)
.github/workflows/create-cluster.yaml
name: Create a cluster
on:
workflow_dispatch:
inputs:
name:
required: true
description: "The name of the cluster"
provider:
required: true
description: "The provider where the cluster is hosted. Accepted values: aws, azure."
default: "aws"
cluster:
required: true
description: "The cluster composition label, for example eks."
node-size:
required: true
description: "The size of the nodes"
default: "small"
min-node-count:
required: true
description: "The minimum number of nodes (autoscaler might increase this number)"
default: "1"
permissions:
contents: write
concurrency:
group: cluster-manifests
cancel-in-progress: false
jobs:
deploy-app:
runs-on: ubuntu-latest
env:
NAME: ${{ inputs.name }}
PROVIDER: ${{ inputs.provider }}
CLUSTER: ${{ inputs.cluster }}
NODE_SIZE: ${{ inputs.node-size }}
MIN_NODE_COUNT: ${{ inputs.min-node-count }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Create cluster
run: |
chmod +x scripts/create-cluster.sh
./scripts/create-cluster.sh "$NAME" "$PROVIDER" "$CLUSTER" "$NODE_SIZE" "$MIN_NODE_COUNT"
- name: Commit changes
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add .
git commit -m "Create cluster $NAME"
- name: Push changes
uses: ad-m/github-push-action@v1.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
  1. Create scripts/create-cluster.sh. The script copies the claim template into infra/ and replaces the placeholders with the inputs from Port.
scripts/create-cluster.sh
#!/usr/bin/env bash
set -euo pipefail

export NAME="$1"
export PROVIDER="$2"
export CLUSTER="$3"
export NODE_SIZE="$4"
export MIN_NODE_COUNT="$5"

if ! [[ "$MIN_NODE_COUNT" =~ ^[0-9]+$ ]]; then
echo "min-node-count must be a whole number, got: $MIN_NODE_COUNT" >&2
exit 1
fi

FILE_PATH="infra/${NAME}-cluster.yaml"

mkdir -p infra
cp compositions/cluster-template.yaml "$FILE_PATH"
yq --inplace '.metadata.name = strenv(NAME)' "$FILE_PATH"
yq --inplace '.spec.id = strenv(NAME)' "$FILE_PATH"
yq --inplace '.spec.compositionSelector.matchLabels.provider = strenv(PROVIDER)' "$FILE_PATH"
yq --inplace '.spec.compositionSelector.matchLabels.cluster = strenv(CLUSTER)' "$FILE_PATH"
yq --inplace '.spec.parameters.nodeSize = strenv(NODE_SIZE)' "$FILE_PATH"
yq --inplace '.spec.parameters.minNodeCount = env(MIN_NODE_COUNT)' "$FILE_PATH"

ubuntu-latest runners already include yq v4, so the pipeline does not need to install it.

  1. Create .github/workflows/delete-cluster.yaml to delete the cluster file and push the change.
Delete cluster workflow (click to expand)
.github/workflows/delete-cluster.yaml
name: Delete the cluster
on:
workflow_dispatch:
inputs:
name:
required: true
description: "The name of the cluster"
permissions:
contents: write
concurrency:
group: cluster-manifests
cancel-in-progress: false
jobs:
deploy-app:
runs-on: ubuntu-latest
env:
NAME: ${{ inputs.name }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Delete cluster
run: |
rm "infra/${NAME}-cluster.yaml"
- name: Commit changes
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add .
git commit -m "Delete cluster $NAME"
- name: Push changes
uses: ad-m/github-push-action@v1.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
Manifest file name

The delete pipeline removes infra/<name>-cluster.yaml, matching the file name that create-cluster.sh generates.

Set up the data model​

We will create a cluster blueprint so requested clusters appear in the catalog.

  1. Go to the Data model page.
  2. Click the + Blueprint button.
  3. Click the {...} Edit JSON button.
  4. Copy and paste the following JSON configuration, then click Save.
Cluster blueprint (click to expand)
cluster-blueprint.json
{
"identifier": "cluster",
"description": "This blueprint represents a Kubernetes Cluster",
"title": "Cluster",
"icon": "Cluster",
"schema": {
"properties": {
"provider": {
"type": "string",
"title": "Provider",
"default": "aws",
"description": "The provider where the cluster is hosted. Accepted values: aws, azure.",
"enum": ["aws", "azure"]
},
"node-size": {
"type": "string",
"title": "Node Size",
"default": "small",
"description": "The size of the nodes",
"enum": ["small", "medium", "large"]
},
"min-node-count": {
"type": "number",
"title": "Minimum number of nodes",
"default": 1,
"description": "The minimum number of nodes (autoscaler might increase this number)"
},
"kube-config": {
"type": "string",
"title": "Kube config",
"description": "Kube config"
},
"status": {
"type": "string",
"title": "Status",
"description": "The status of the cluster"
}
},
"required": ["provider", "node-size", "min-node-count"]
},
"mirrorProperties": {},
"calculationProperties": {},
"relations": {}
}

Build the workflow​

Now we will create the workflow that dispatches the pipelines and updates the catalog. It defines two self-service triggers:

  • Create a cluster dispatches the create pipeline, then upserts a cluster entity so the new cluster appears in the catalog immediately.
  • Delete a cluster lets you pick an existing cluster entity, dispatches the delete pipeline, then removes the entity from the catalog.

The workflow's title, description, and input descriptions double as its tool definition for AI agents, so they are written as instructions an agent can follow. See expose workflows as tools for the full guidelines.

  1. Go to the workflows page in Port.
  2. Click + Workflow.
  3. Fill out the Create new workflow form, then click Confirm.
  4. Click the {...} button to open the JSON editor and paste the workflow JSON below.
Manage clusters workflow JSON (click to expand)
manage-clusters-workflow.json
{
"identifier": "manage_clusters",
"title": "Manage Kubernetes clusters",
"icon": "Cluster",
"description": "Create or delete a Kubernetes cluster through Crossplane and ArgoCD. Use this tool when a user asks to provision, create, spin up, delete, or tear down a Kubernetes cluster. Creating a cluster requires a name, provider, cluster type, node size, and minimum node count. Deleting a cluster requires the cluster's identifier from the catalog.",
"nodes": [
{
"identifier": "create_cluster_trigger",
"title": "Create a cluster",
"icon": "Cluster",
"config": {
"type": "SELF_SERVE_TRIGGER",
"contexts": [
{
"on": "CREATE_ENTITY",
"blueprintIdentifier": "cluster"
}
],
"userInputs": {
"properties": {
"name": {
"type": "string",
"title": "Name",
"description": "The name of the cluster. Use lowercase letters, digits, and hyphens, for example 'team-dev-cluster'."
},
"provider": {
"type": "string",
"title": "Provider",
"default": "aws",
"description": "The provider where the cluster is hosted. Accepted values: aws, azure.",
"enum": [
"aws",
"azure"
]
},
"cluster": {
"type": "string",
"title": "Cluster type",
"description": "The cluster composition label written into the claim, for example eks."
},
"node-size": {
"type": "string",
"title": "Node Size",
"default": "small",
"description": "The size of the nodes. Accepted values: small, medium, large.",
"enum": [
"small",
"medium",
"large"
]
},
"min-node-count": {
"type": "string",
"title": "Minimum number of nodes",
"default": "1",
"description": "The minimum number of nodes as a string, for example '1'. The autoscaler might increase this number."
}
},
"required": [
"name",
"provider",
"cluster",
"node-size",
"min-node-count"
]
},
"published": true
}
},
{
"identifier": "create_cluster",
"title": "Dispatch create cluster workflow",
"icon": "GitHub",
"config": {
"type": "INTEGRATION_ACTION",
"installationId": "<YOUR_GITHUB_OCEAN_INTEGRATION_ID>",
"integrationProvider": "github-ocean",
"integrationInvocationType": "dispatch_workflow",
"integrationActionExecutionProperties": {
"org": "<GITHUB_ORG>",
"repo": "<GITHUB_REPO>",
"workflow": "create-cluster.yaml",
"workflowInputs": {
"name": "{{ .outputs.trigger.name }}",
"provider": "{{ .outputs.trigger.provider }}",
"cluster": "{{ .outputs.trigger.cluster }}",
"node-size": "{{ .outputs.trigger[\"node-size\"] }}",
"min-node-count": "{{ .outputs.trigger[\"min-node-count\"] }}"
},
"reportWorkflowStatus": true
}
}
},
{
"identifier": "upsert_cluster_entity",
"title": "Add cluster to catalog",
"icon": "Cluster",
"config": {
"type": "UPSERT_ENTITY",
"blueprintIdentifier": "cluster",
"mapping": {
"identifier": "{{ .outputs.trigger.name }}",
"title": "{{ .outputs.trigger.name }}",
"properties": {
"provider": "{{ .outputs.trigger.provider }}",
"node-size": "{{ .outputs.trigger[\"node-size\"] }}",
"min-node-count": "{{ .outputs.trigger[\"min-node-count\"] | tonumber }}",
"status": "Provisioning"
}
}
}
},
{
"identifier": "delete_cluster_trigger",
"title": "Delete a cluster",
"icon": "Cluster",
"config": {
"type": "SELF_SERVE_TRIGGER",
"contexts": [
{
"on": "ENTITY",
"userInput": "cluster"
}
],
"userInputs": {
"properties": {
"cluster": {
"type": "string",
"format": "entity",
"blueprint": "cluster",
"title": "Cluster",
"description": "The cluster to delete. Use the cluster's identifier from the catalog."
}
},
"required": [
"cluster"
]
},
"published": true
}
},
{
"identifier": "delete_cluster",
"title": "Dispatch delete cluster workflow",
"icon": "GitHub",
"config": {
"type": "INTEGRATION_ACTION",
"installationId": "<YOUR_GITHUB_OCEAN_INTEGRATION_ID>",
"integrationProvider": "github-ocean",
"integrationInvocationType": "dispatch_workflow",
"integrationActionExecutionProperties": {
"org": "<GITHUB_ORG>",
"repo": "<GITHUB_REPO>",
"workflow": "delete-cluster.yaml",
"workflowInputs": {
"name": "{{ .outputs.trigger.cluster | if type == \"object\" then .identifier else . end }}"
},
"reportWorkflowStatus": true
}
}
},
{
"identifier": "remove_cluster_entity",
"title": "Remove cluster from catalog",
"icon": "Delete",
"config": {
"type": "WEBHOOK",
"url": "<PORT_API_URL>/v1/blueprints/cluster/entities/{{ .outputs.trigger.cluster | if type == \"object\" then .identifier else . end }}",
"method": "DELETE",
"agent": false,
"synchronized": true
}
}
],
"connections": [
{
"sourceIdentifier": "create_cluster_trigger",
"targetIdentifier": "create_cluster"
},
{
"sourceIdentifier": "create_cluster",
"targetIdentifier": "upsert_cluster_entity"
},
{
"sourceIdentifier": "delete_cluster_trigger",
"targetIdentifier": "delete_cluster"
},
{
"sourceIdentifier": "delete_cluster",
"targetIdentifier": "remove_cluster_entity"
}
]
}
Modification required

Replace the placeholders before you apply the workflow:

  • <YOUR_GITHUB_OCEAN_INTEGRATION_ID> - your GitHub integration installation ID, found on the Connectors page.
  • <GITHUB_ORG> - your GitHub organization or user name, for example my-org.
  • <GITHUB_REPO> - your GitHub repository name, for example cluster-manifests.
  • <PORT_API_URL> - https://api.port.io for an EU organization, or https://api.us.port.io for a US organization. An agent can choose this from its MCP host: mcp.port.io is EU, and mcp.us.port.io is US.
  1. Click Apply changes.

A few details about this workflow:

  • The create_cluster_trigger node uses a CREATE_ENTITY context so it also appears in the cluster blueprint's create flow, and the delete_cluster_trigger node uses an ENTITY context so it appears in the bolt (⚡) menu of every cluster entity with the entity pre-filled.
  • User inputs are referenced with {{ .outputs.trigger.<input> }}. Inputs whose keys contain hyphens, such as node-size and min-node-count, must use bracket notation like {{ .outputs.trigger["node-size"] }}, since dot notation would be interpreted as subtraction in JQ. See data flow for details.
  • .outputs.trigger always resolves to the trigger that fired, so the same alias works for both the create and delete branches of this multi-trigger workflow.
  • The delete branch reads the selected cluster with {{ .outputs.trigger.cluster | if type == "object" then .identifier else . end }}. An entity input can arrive as a full entity object or as a plain identifier, depending on how the run was triggered, and this expression always yields the identifier.
  • With reportWorkflowStatus enabled, each dispatch node waits for the GitHub Actions run to finish, so the catalog nodes only run after the pipeline has committed its changes. Hosted GitHub integrations already send live events. A self-hosted integration must have live events enabled, or this wait never finishes and the catalog steps do not run.
  • The remove_cluster_entity webhook calls Port's own API. Calls to Port's API from workflow nodes are authenticated automatically, so no token is needed.
Secrets and integration actions

The GitHub integration action authenticates through your installed GitHub integration, so no token or secret is required in the node. Integration actions are available for GitHub today. For other providers such as GitLab, Bitbucket, or Azure DevOps, use a webhook node to hit the pipeline trigger URL, and pass credentials with {{ .secrets["<SECRET_NAME>"] }}.

Connect GitOps​

We will add an ArgoCD application that syncs the GitHub repository into the management cluster. This section changes both the repository and the cluster. Finish it, and push the manifests, before you treat a workflow run as a test of the Port workflow.

The cluster manifests are ClusterClaim resources from the devopstoolkitseries.com/v1alpha1 API. ArgoCD can apply them only after the management cluster has the following, which are outside this guide:

  • A composite resource definition (XRD) for devopstoolkitseries.com/v1alpha1 that offers a ClusterClaim. The crossplane-stable chart installs Crossplane v2, which supports claims only through a legacy XRD defined with apiextensions.crossplane.io/v1.
  • A composition whose labels match the form. provider is aws or azure, and cluster is the type you enter, for example eks.
  • A Crossplane cloud provider package with credentials for that provider.

The Port workflow and the GitHub Actions pipelines work without them, so we can test the self-service flow before Crossplane is fully configured. Until that checklist is in place, the claim is committed to Git, the production-infra application in ArgoCD reports a sync error for it because the ClusterClaim kind does not exist yet, and the catalog entity stays at Provisioning. Manage EKS clusters shows one way to supply a real composition.

  1. If your manifests repository is private, give ArgoCD read access with a GitHub token that can read the repository:
kubectl create secret generic manifests-repo --namespace argocd \
--from-literal=type=git \
--from-literal=url=https://github.com/<GITHUB_ORG>/<GITHUB_REPO> \
--from-literal=username=<GITHUB_USERNAME> \
--from-literal=password=<GITHUB_TOKEN>

kubectl label secret manifests-repo --namespace argocd argocd.argoproj.io/secret-type=repository
  1. Confirm infra/.gitkeep is already on the default branch, then save the application below as argocd/apps.yaml.
ArgoCD application (click to expand)
Customize repoURL

Replace <GITHUB_ORG> and <GITHUB_REPO> in repoURL with your manifests repository. The application uses ArgoCD's built-in default project and creates the infra namespace that the cluster claims target.

argocd/apps.yaml
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: production-infra
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/<GITHUB_ORG>/<GITHUB_REPO>
targetRevision: HEAD
path: infra
destination:
server: https://kubernetes.default.svc
namespace: infra
syncPolicy:
automated:
selfHeal: true
prune: true
allowEmpty: true
syncOptions:
- CreateNamespace=true

Apply the application:

kubectl apply --filename argocd/apps.yaml

Push the manifests​

Commit and push these files to the repository's default branch before you run the workflow. GitHub only dispatches a workflow_dispatch workflow that already exists on the default branch, and this workflow does not pass a branch ref:

  • .github/workflows/create-cluster.yaml.
  • .github/workflows/delete-cluster.yaml.
  • scripts/create-cluster.sh.
  • compositions/cluster-template.yaml.
  • infra/.gitkeep.
  • argocd/apps.yaml.

Execute the workflow​

Trigger the workflow from the Port UI, the API, or an AI agent:

  1. Go to the self-service page, find Create a cluster, and fill in the cluster properties.

  2. Click Execute. Follow the run from the workflow's runs view.

To remove a cluster, open the bolt (⚡) menu on its entity and run Delete a cluster, or run the trigger from the self-service page.

Once triggered, the pieces we set up in this guide come together:

  • The workflow dispatches the matching GitHub Actions workflow, which updates the infra folder of your repository.
  • ArgoCD synchronizes that manifest into the management cluster.
  • The catalog reflects the change: creation upserts a cluster entity with status Provisioning, and deletion removes it.

A cloud cluster appears only after the composition checklist in Connect GitOps is in place. Until then, ArgoCD reports a sync error for the claim and the catalog entity stays at Provisioning.

We can now request and remove clusters from Port.

Next steps​