IDP port e Crossplane composition no Amazon EKS [Lab Session]

Paulo Ponciano
8 min readOct 22, 2023

Seguindo em mais um lab, este de certa forma é uma continuação do Serverless com Crossplane composition no EKS + GitOps [Lab Session]. Vamos adicionar mais alguns elementos, um deles é o Internal Developer Portal (IDP) port. O IDP nos permite abstrair muitos elementos subjacentes, proporcionando aos consumidores maior foco em suas entregas finais.

É claro, ainda é necessário manter esses 'elementos subjacentes' assim como a própria abstração dos mesmos. Esse normalmente é o trabalho do time de Engenharia de Plataforma.

Architecture and combination of elements

Vamos utilizar a mesma composition do crossplane que entrega uma arquitetura serverless na AWS. Desta vez, o input das informações não será diretamente em um yaml de claim e sim no IDP port.

Com crossplane, o yaml utilizado para o claim já é bem enxuto, utilizando um IDP isso fica ainda mais simples para o consumidor.

Repositório.

Port blueprints e actions

No DevPortal Builder do port, vamos criar os blueprints Cluster, Namespace e Order (stack serverless AWS). Blueprints são utilizados como modelos de dados dos nossos elementos e permite a composição dos catálogos.

app.getport.io
  1. Os arquivos json utilizados estão em port/blueprints do repositório. Cluster blueprint:
{
"identifier": "cluster",
"description": "This blueprint represents a Kubernetes Cluster",
"title": "Cluster",
"icon": "Cluster",
"schema": {
"properties": {},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"relations": {}
}

Namespace blueprint:

{
"identifier": "namespace",
"description": "This blueprint represents a k8s Namespace",
"title": "Namespace",
"icon": "Environment",
"schema": {
"properties": {
"creationTimestamp": {
"type": "string",
"title": "Created",
"format": "date-time",
"description": "When the Namespace was created"
},
"labels": {
"type": "object",
"title": "Labels",
"description": "Labels of the Namespace"
}
}
// Removed for brevity

Order blueprint:

{
"identifier": "order",
"description": "This blueprint represents an Order App",
"title": "Order",
"icon": "Microservice",
"schema": {
"properties": {
"provider": {
"icon": "AWS",
"title": "Provider",
"description": "The provider where the app is deployed",
"type": "string",
"default": "n/a",
"enum": [
"default",
"n/a"
],
"enumColors": {
"default": "lightGray",
"n/a": "lightGray"
}
}
// Removed for brevity

Resultado:

2. Agora vamos criar as actions do blueprint Order. As actions serão responsáveis por acionar o GitHub Actions em eventos de CREATE e DELETE. Podemos editar o blueprint e inserir o json port/actions do repositório em 'Actions':

Este é o trecho em que informamos o workflow que será acionado em cada action:

// Removed for brevity
"invocationMethod": {
"type": "GITHUB",
"omitPayload": true,
"omitUserInputs": false,
"reportWorkflowStatus": true,
"org": "paulofponciano",
"repo": "EKS-Crossplane-ArgoCD",
"workflow": "create-order-app.yaml"
},
"trigger": "CREATE",
"description": "Create an Order App",
"requiredApproval": false
}
// Removed for brevity

Na primeira integração, é necessário conectar o port ao GitHub para que ele possa acionar os workflows — GitHub app installation:

Agora em Self-Service já temos as actions de Create e Delete para a composition do crossplane:

GitHub Actions

Os workflows do GitHub Actions serão responsáveis por receber os inputs enviados no payload do port quando as actions são executadas. Esses inputs são passados no manifesto de claim da composition do crossplane e é feito o commit desse manifesto em um repositório monitorado pelo ArgoCD (deployed_infra):

  1. .github/workflows/create-order-app.yaml
name: Create an order app
on:
workflow_dispatch:
inputs:
name:
required: true
description: "The name of the app"
provider:
required: true
description: "The provider where the app is deployed"
default: "aws"
region:
required: true
description: "The provider region"
default: "us-east-1"
ownerName:
required: true
description: "The owner name"
default: "ownerName"
projectName:
required: true
description: "The project name"
default: "projectName"
jobs:
deploy-app:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
persist-credentials: false
fetch-depth: 0

- name: Create an order app
run: |
chmod +x scripts/create-app-order.sh
./scripts/create-app-order.sh ${{ inputs.name }} ${{ inputs.provider }} ${{ inputs.region }} ${{ inputs.ownerName }} ${{ inputs.projectName }}

- name: Commit changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add .
git commit -a -m "Create app order ${{ inputs.name }}"

- name: push
uses: ad-m/github-push-action@master

2. scripts/create-app-order.sh

NAME=$1
PROVIDER=$2
REGION=$3
OWNER=$4
PROJECT=$5

FILE_PATH=deployed_infra/${NAME}.yaml

cp crossplane_compositions/order/order-claim-port.yaml $FILE_PATH
yq --inplace ".metadata.name = \"${NAME}\"" $FILE_PATH
yq --inplace ".spec.resourceConfig.providerConfigName = \"${PROVIDER}\"" $FILE_PATH
yq --inplace ".spec.resourceConfig.region = \"${REGION}\"" $FILE_PATH
yq --inplace ".spec.resourceConfig.tags.ownerName = \"${OWNER}\"" $FILE_PATH
yq --inplace ".spec.resourceConfig.tags.projectName = \"${PROJECT}\"" $FILE_PATH

3. .github/workflows/delete-order-app.yaml

name: Delete an order app
on:
workflow_dispatch:
inputs:
name:
required: true
description: "The name of the app"
jobs:
delete-app:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
persist-credentials: false
fetch-depth: 0

- name: Delete an order app
run: |
rm deployed_infra/${{ inputs.name }}.yaml

- name: Commit changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add .
git commit -a -m "Delete app order ${{ inputs.name }}"

- name: push
uses: ad-m/github-push-action@master

ArgoCD

Criamos uma application no ArgoCD, definindo como source o repo/path onde teremos o commit do manifesto feito pelo workflow do GitHub Actions:

apiVersion: v1
kind: Secret
metadata:
name: public-repo-crossplane
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: https://github.com/paulofponciano/EKS-Crossplane-ArgoCD.git
---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: idp
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Internal Developer Portal
sourceRepos:
- 'https://github.com/paulofponciano/EKS-Crossplane-ArgoCD.git'
destinations:
- namespace: '*'
server: 'https://kubernetes.default.svc'
name: 'in-cluster'
clusterResourceWhitelist:
- group: '*'
kind: '*'
namespaceResourceWhitelist:
- group: '*'
kind: '*'
orphanedResources:
warn: true
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: idp-infra-aws
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: idp
source:
repoURL: https://github.com/paulofponciano/EKS-Crossplane-ArgoCD.git
targetRevision: HEAD
path: deployed_infra
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
selfHeal: true
prune: true
allowEmpty: true
kubectl apply -f argocd/application.yaml

Port k8s-exporter

O k8s-exporter permite que o port possa ingerir de volta os dados dos recursos criados pelo crossplane no cluster kubernetes, assim podendo alimentar o catálogo do IDP.

  1. Precisamos das credenciais de API do port para instalação via helm no cluster. Essas credenciais já existem no port, vamos apenas utiliza-las como variáveis na instalação:

2. Antes de executar a instalação via helm, precisamos informar no arquivo config.yaml os atributos para que os recursos criados sejam 'reconhecidos' como entidades, assim serão exportados para o port:

# Removed for brevity
- kind: api.pauloponciano.pro/v1alpha1/customorder
selector:
query: .metadata.namespace | startswith("environment")
port:
entity:
mappings:
- identifier: .metadata.name + "-" + .metadata.namespace + "-" + "paulop"
title: .metadata.name
icon: '"Microservices"'
blueprint: '"order"'
properties:
provider: .spec.resourceConfig.providerConfigName
region: .spec.resourceConfig.region
ownerName: .spec.resourceConfig.tags.ownerName
projectName: .spec.resourceConfig.tags.projectName
labels: .metadata.labels
annotations: .metadata.annotations
relations:
Namespace: .metadata.namespace + "-" + "paulop"

3. Instalação:

helm repo add port-labs https://port-labs.github.io/helm-charts
helm upgrade --install port-k8s-exporter port-labs/port-k8s-exporter \
--create-namespace --namespace port-k8s-exporter \
--set secret.secrets.portClientId=$CLIENT_ID --set secret.secrets.portClientSecret=$CLIENT_SECRET \
--set-file configMap.config=config.yaml

Agora já podemos verificar o que está sendo ingerido e apresentado no catálogo do port:

Não temos nada em Orders porque ainda não criamos uma, calma jovem! Faremos isso da mesma forma que o usuário fará.

Usando o Self-Service

CREATE:

Na área de self-service do port, vamos executar a action de CREATE, fazendo os inputs necessários para o claim da composition do crossplane:

Sucesso da execução da action:

Sucesso na execução do workflow de create no GitHub Actions:

O manifesto de claim foi criado com os inputs feitos através do IDP:

O ArgoCD fez a entrega do claim no cluster kubernetes que já possui a composition e definition do crossplane, assim os recursos da stack são criados na AWS:

Voltando ao catálogo do port, em Orders:

AWS stack serverless:

DELETE:

Vamos agora executar a action de DELETE, também pelo IDP:

Workflow de delete:

ArgoCD prune resources:

--

--

Paulo Ponciano

Solutions Architect | 7x AWS Certified | AWS Black Belt | AWS Community Builder