Deploy S3 Bucket using Crossplane
This example demonstrates how to deploy Crossplane resources in your Kubernetes Cluster via Port Actions.
We will show you how to utilize Port Actions to open a Merge Request in your Gitlab/GitHub Project, which commits a Crossplane manifest that describes an S3 Bucket in AWS.
We will cover using both GitLab and GitHub Pipelines to create Merge/Pull Requests that commit a file to the project.
To streamline deployments with this example, ensure you have a GitOps tool configured. Otherwise, refer to this guide for an end to end tutorial of creating resources in Kubernetes with Crossplane and ArgoCD.
Prerequisitesโ
- Kubernetes cluster.
- Crossplane installed in your cluster:
- Crossplane Installation Guide.
- Crossplane AWS Quickstart Guide:
- Deploy a Crossplane Provider.
- Deploy a Crossplane ProviderConfig.
 
 
- GitOps mechanism of your choosing which synchronizes files from your Gitlab Project to your Kubernetes cluster.
Port Configurationโ
- Head over to the Builder page to create the following blueprints:
- Click on the + Blueprintbutton.
- Click on the {...} Edit JSONbutton.
- Copy and paste the following JSON configuration into the editor.
- Click Save.
Port S3Bucket Blueprint
{
  "identifier": "s3bucket",
  "title": "S3Bucket",
  "icon": "Crossplane",
  "schema": {
    "properties": {
      "aws_region": {
        "title": "AWS Region",
        "icon": "AWS",
        "type": "string"
      }
    },
    "required": ["aws_region"]
  },
  "mirrorProperties": {},
  "calculationProperties": {},
  "relations": {}
}
- To create the Port action, go to the self-service page:
- Click on the + New Actionbutton.
- Choose the s3bucketblueprint and clickNext.
- Click on the {...} Edit JSONbutton.
- Copy and paste the following JSON configuration into the editor.
- Click Save
 
- Click on the 
- GitHub
- GitLab
Port Action
Make sure to replace <GITHUB_ORG> and <GITHUB_REPO> with your GitHub organization and repository names respectively.
{
  "identifier": "crossplane_s3_bucket",
  "title": "Crossplane S3 Bucket",
  "icon": "Crossplane",
  "userInputs": {
    "properties": {
      "aws_region": {
        "icon": "AWS",
        "title": "AWS Region",
        "type": "string",
        "default": "us-east-1",
        "enum": ["us-east-1", "eu-west-1"],
        "enumColors": {
          "us-east-1": "lightGray",
          "eu-west-1": "lightGray"
        }
      },
      "bucket_name": {
        "title": "Bucket Name",
        "type": "string",
        "description": "Has to be globally unique as per AWS limitations"
      }
    },
    "required": ["aws_region", "bucket_name"],
    "order": ["bucket_name", "aws_region"]
  },
  "invocationMethod": {
    "type": "GITHUB",
    "org": "<GITHUB_ORG>",
    "repo": "<GITHUB_REPO>",
    "workflow": "create-and-push-image.yml",
    "omitUserInputs": false,
    "omitPayload": false,
    "reportWorkflowStatus": true
  },
  "trigger": "CREATE",
  "description": "Creates a crossplane file for a new S3 Bucket",
  "requiredApproval": false
}
Port Action
Make sure to replace the placeholders for <PROJECT_NAME> and <GROUP_NAME> of your crossplane_deployer.
{
  "identifier": "crossplane_s3_bucket",
  "title": "Crossplane S3 Bucket",
  "icon": "Crossplane",
  "userInputs": {
    "properties": {
      "aws_region": {
        "icon": "AWS",
        "title": "AWS Region",
        "type": "string",
        "default": "us-east-1",
        "enum": ["us-east-1", "eu-west-1"],
        "enumColors": {
          "us-east-1": "lightGray",
          "eu-west-1": "lightGray"
        }
      },
      "bucket_name": {
        "title": "Bucket Name",
        "type": "string",
        "description": "Has to be globally unique as per AWS limitations"
      }
    },
    "required": ["aws_region", "bucket_name"],
    "order": ["bucket_name", "aws_region"]
  },
  "invocationMethod": {
    "type": "GITLAB",
    "omitPayload": false,
    "omitUserInputs": false,
    "projectName": "<PROJECT_NAME>",
    "groupName": "<GROUP_NAME>",
    "agent": true
  },
  "trigger": "CREATE",
  "description": "Creates a crossplane file for a new S3 Bucket",
  "requiredApproval": false
}
Git Workflowsโ
- Create a GitHub repository / Gitlab project called crossplane_deployer. If using GitLab configure a Pipeline Trigger Token.
We recommend creating a dedicated repository for your Port action workflows.
- 
Inside your crossplane_deployerrepository, create a folder namedcrossplane-templateson themainbranch.
- 
Create a template file named s3bucket-crossplane.yamlin thecrossplane-templatesdirectory.
s3bucket-crossplane.yaml
# s3bucket-crossplane.yaml
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  name: { { bucket_name } }
spec:
  forProvider:
    region: { { aws_region } }
  providerConfigRef:
    name: default
- GitHub
- GitLab
- 
Create the following GitHub Action secrets: - PORT_CLIENT_ID- Port Client ID learn more. -PORT_CLIENT_SECRET- Port Client Secret learn more. -MY_GITHUB_TOKEN- a Classic Personal Access Token with the repo scope and the following permissions: pull_requests:write (to create PR) and contents:write (to merge PR)
 
- 
Install Port's GitHub app by clicking here. 
 
- 
Create a workflow file under .github/workflows/create-s3-manifest.ymlwith the following content:
GitHub workflow
name: Create New S3 Bucket Crossplane Manifest
on:
  workflow_dispatch:
    inputs:
      bucket_name:
        description: 'Name of the s3 bucket'
        required: true
      aws_region:
        description: 'AWS Region for the cluster'
        required: true
      port_payload:
        required: true
        description: >-
          Port's payload, including details for who triggered the action and
          general context (blueprint, run id, etc...)
jobs:
  create-manifest:
    runs-on: ubuntu-latest
    steps:
      - name: Inform execution of request to create a new manifest
        id: promote
        uses: port-labs/port-github-action@v1
        with:
          clientId: ${{ secrets.PORT_CLIENT_ID }}
          clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
          baseUrl: https://api.getport.io
          operation: PATCH_RUN
          runId: ${{ fromJson(inputs.port_payload).context.runId }}
          logMessage: 'About to create a crossplane manifest for a new s3 bucket...'
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Create crossplane manifest for s3 bucket
        id: create-manifest
        env:
          BUCKET_FILE_PATH: 'manifests/s3bucket'
          CROSSPLANE_TEMPLATE_PATH: 'crossplane-templates/s3bucket-crossplane.yaml'
        run: |
          mkdir -p $BUCKET_FILE_PATH
          BUCKET_FILE_NAME="${BUCKET_FILE_PATH}/s3bucket-${{ inputs.bucket_name }}.yaml"
          cp $CROSSPLANE_TEMPLATE_PATH $BUCKET_FILE_NAME
          sed -i "s|{{ bucket_name }}|${{ inputs.bucket_name }}|g" $BUCKET_FILE_NAME
          sed -i "s|{{ aws_region }}|${{ inputs.aws_region }}|g" $BUCKET_FILE_NAME
          git add $BUCKET_FILE_NAME
      - name: Create Pull Request
        id: create-pr
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.CREATOR_TOKEN }}
          commit-message: Added ${{ inputs.bucket_name }} s3 bucket crossplane manifest
          committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
          author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
          signoff: false
          branch: deployment/${{ fromJson(inputs.port_payload).context.runId }}
          title: '[Deployment] Add ${{ inputs.bucket_name }} s3 bucket crossplane manifest'
          body: |
            This PR is automatically generated by Port.
            It contains the crossplane manifest for the s3 bucket ${{ inputs.bucket_name }}.
            The manifest is generated based on the blueprint: **${{ fromJson(inputs.port_payload).context.blueprint }}**.
            **Run ID**: ${{ fromJson(inputs.port_payload).context.runId }}.
            **Triggered by**: ${{ fromJson(inputs.port_payload).trigger.by.user.email }}.
            **Triggered at**: ${{ fromJson(inputs.port_payload).trigger.at }}.
            **Triggered from**: ${{ fromJson(inputs.port_payload).trigger.origin }}.
            - Auto-generated by [port-actions][1] 
            [1]: https://app.getport.io/organization/run?runId=${{ fromJson(inputs.port_payload).context.runId }}
          labels: |
            deployment
            automated pr
          assignees: ${{ fromJson(inputs.port_payload).trigger.by.user.email }}
      - name: Inform Port about pull request creation status - Success
        if: steps.create-pr.outputs.pull-request-url != ''
        uses: port-labs/port-github-action@v1
        with:
          clientId: ${{ secrets.PORT_CLIENT_ID }}
          clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
          baseUrl: https://api.getport.io
          operation: PATCH_RUN
          runId: ${{ fromJson(inputs.port_payload).context.runId }}
          logMessage: |
            Pull request created successfully. URL: ${{ steps.create-pr.outputs.pull-request-url }}.
      - name: Inform Port about pull request creation status - Failure
        if: steps.create-pr.outputs.pull-request-url == ''
        uses: port-labs/port-github-action@v1
        with:
          clientId: ${{ secrets.PORT_CLIENT_ID }}
          clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
          baseUrl: https://api.getport.io
          operation: PATCH_RUN
          runId: ${{ fromJson(inputs.port_payload).context.runId }}
          logMessage: |
            Failed to create pull request. Please check the logs for more details.
- Create the following variables as GitLab Variables:
- ACCESS_TOKEN- a Personal Access Token with the following scopes:
 - api,- read_api,- read_user,- read_repository,- write_repository.
- PORT_CLIENT_ID- Port Client ID learn more.
- PORT_CLIENT_SECRET- Port Client Secret learn more.
 
- Install Port's Gitlab agent by following our guide here.
Make sure to use your Pipeline Trigger Token while installing Port's Gitlab agent.
- In your crossplane_deployerGitlab Project, create a Gitlab CI file under.gitlab-ci.ymlin themainbranch with the following content:
GitLab CI Script
image: python:3.10.0-alpine
stages: # List of stages for jobs, and their order of execution
  - fetch-port-access-token
  - generate-crossplane-bucket-yaml
  - create-entity
  - update-run-status
fetch-port-access-token: # Example - get the Port API access token and RunId
  stage: fetch-port-access-token
  except:
    - pushes
  before_script:
    - |
      apk update -q
      apk add jq curl -q
  script:
    - |
      accessToken=$(curl -X POST \
        -H 'Content-Type: application/json' \
        -d '{"clientId": "'"$PORT_CLIENT_ID"'", "clientSecret": "'"$PORT_CLIENT_SECRET"'"}' \
        -s 'https://api.getport.io/v1/auth/access_token' | jq -r '.accessToken')
      echo "PORT_ACCESS_TOKEN=$accessToken" >> data.env
      runId=$(cat $TRIGGER_PAYLOAD | jq -r '.port_payload.context.runId')
      blueprintId=$(cat $TRIGGER_PAYLOAD | jq -r '.port_payload.context.blueprint')
      echo "RUN_ID=$runId" >> data.env
      echo "BLUEPRINT_ID=$blueprintId" >> data.env
  artifacts:
    reports:
      dotenv: data.env
generate-crossplane-bucket-yaml:
  variables:
    BUCKET_FILE_PATH: 'manifests'
    CROSSPLANE_TEMPLATE_PATH: 'crossplane-templates/s3bucket-crossplane.yaml'
    BRANCH_NAME: 'add-bucket-$bucket_name-$CI_JOB_ID'
  before_script:
    - |
      apk update -q
      apk add jq curl git -q
  stage: generate-crossplane-bucket-yaml
  except:
    - pushes
  script:
    - |
      BUCKET_FILE_NAME="$BUCKET_FILE_PATH/s3bucket-crossplane-$bucket_name.yaml"
      COMMIT_MESSAGE="Added $bucket_name s3 bucket crossplane manifest"
      mkdir -p $BUCKET_FILE_PATH
      cp $CROSSPLANE_TEMPLATE_PATH $BUCKET_FILE_NAME
      sed -i "s/{{ bucket_name }}/$bucket_name/g" $BUCKET_FILE_NAME
      sed -i "s/{{ aws_region }}/$aws_region/g" $BUCKET_FILE_NAME
      git config --global user.email "gitlab-pipeline[bot]@gitlab.com"
      git config --global user.name "Gitlab Pipeline Bot"
      git add $BUCKET_FILE_NAME
      git commit -m "$COMMIT_MESSAGE"
      git checkout -b $BRANCH_NAME
      git push -o ci-skip https://:${ACCESS_TOKEN}@$CI_SERVER_HOST/$CI_PROJECT_PATH.git $BRANCH_NAME
      # Create Merge Request
      res=$(curl --request POST \
        --header "PRIVATE-TOKEN: ${ACCESS_TOKEN}" \
        --data "source_branch=$BRANCH_NAME" \
        --data "target_branch=main" \
        --data "title=$COMMIT_MESSAGE" \
        --data "remove_source_branch=true" \
        "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests")
      MR_URL=$(echo $res | jq -r '.web_url')
      echo "MR_URL=$MR_URL" >> data.env
  artifacts:
    reports:
      dotenv: data.env
update-run-status:
  stage: update-run-status
  except:
    - pushes
  image: curlimages/curl:latest
  script:
    - |
      curl -X PATCH \
        -H 'Content-Type: application/json' \
        -H "Authorization: Bearer $PORT_ACCESS_TOKEN" \
        -d '{"status":"SUCCESS", "message": {"run_status": "Created Merge Request for '"$bucket_name"' successfully! Merge Request URL: '"$MR_URL"'"}}' \
        "https://api.getport.io/v1/actions/runs/$RUN_ID"
This example focuses on creating either Merge Requests in GitLab or Pull Requests in GitHub. To automatically deploy the manifest to your Kubernetes cluster, you will also need to set up a GitOps tool.
- Trigger the action from the Self-service tab of your Port application.
 You will notice a new Pull Request / Merge Request was created, committing the S3 Bucket manifest.
Next stepsโ
In this example we did not create the Port entity for the S3 bucket.
- You can Connect Port's AWS exporter to make sure all of the properties and entities are automatically ingested from AWS.