IDP port e Crossplane composition no Amazon EKS [Lab Session]
Tags: community, idp, crossplane, eks
Published: 2024-01-30 11:14:11

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](https://cdn-images-1.medium.com/max/3540/1*LpFu4sTAuopbaeg9CDCMag.png)

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.](https://github.com/paulofponciano/EKS-Crossplane-ArgoCD.git)

### 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](https://app.getport.io/)](https://cdn-images-1.medium.com/max/3802/1*0QBq_z6OwaA-Jcxc4ARUpw.png)*[app.getport.io](https://app.getport.io/)*

- Os arquivos *json* utilizados estão em **port/blueprints** do repositório. 

Cluster blueprint:

![](https://cdn-images-1.medium.com/max/3800/1*IDLushUGECMDaVTEMSIOtw.png)

```json
{
  "identifier": "cluster",
  "description": "This blueprint represents a Kubernetes Cluster",
  "title": "Cluster",
  "icon": "Cluster",
  "schema": {
    "properties": {},
    "required": []
  },
  "mirrorProperties": {},
  "calculationProperties": {},
  "relations": {}
}
```

Namespace blueprint:

![](https://cdn-images-1.medium.com/max/3796/1*aCncizNyVm2tJHsABl5K1w.png)

```json
{
    "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:

![](https://cdn-images-1.medium.com/max/3800/1*57FTVZ6lTV6Cl3uklz0_6w.png)

```json
{
  "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:

![](https://cdn-images-1.medium.com/max/3810/1*LIRBAZ0ZyiOypqZ1TOrJNg.png)

- 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':

![](https://cdn-images-1.medium.com/max/3804/1*X_7VvfdoyZnmgPEY6WDM0g.png)

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

```json
// 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:*

![](https://cdn-images-1.medium.com/max/2000/1*I40oOdmON3Rc8zs0MG4PNw.png)

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

![](https://cdn-images-1.medium.com/max/2000/1*Q-WdG_u-QMTEh3Kh-baaGQ.png)

### 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*):

- .github/workflows/create-order-app.yaml

```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
```
- scripts/create-app-order.sh

```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
```
- .github/workflows/delete-order-app.yaml

```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:

```yaml
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`

![](https://cdn-images-1.medium.com/max/3812/1*fv78B0_9JpGC-OuqZ0-2Nw.png)

### 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.

[Port Documentation - Kubernetes](https://docs.getport.io/build-your-software-catalog/sync-data-to-catalog/kubernetes/)

- 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:

![](https://cdn-images-1.medium.com/max/4216/1*6aSj4ewqnaaQe7QzibI4Xw.png)

- 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:

```yaml
# 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"
```

- 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`

![](https://cdn-images-1.medium.com/max/4904/1*jKfuGX9DiDK6GYkJ8zQEPg.png)

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

![](https://cdn-images-1.medium.com/max/2458/1*3iC7reRZPm9h5bgqWX5tLg.png)

![](https://cdn-images-1.medium.com/max/2688/1*kQYAqeQ50a13NIcyx9n-Tw.png)

![](https://cdn-images-1.medium.com/max/2906/1*tyqvN84PBU8bCIx1Ssx5dg.png)

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:

![](https://cdn-images-1.medium.com/max/3802/1*DjLG-RM_9ykBqrS2m99pBg.png)

Sucesso da execução da *action*:

![](https://cdn-images-1.medium.com/max/3810/1*RR1qiafnU-QGq3ivuHY7SA.png)

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

![](https://cdn-images-1.medium.com/max/2586/1*hDWn2Ho_1l7UE0qSd6Zppg.png)

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

![](https://cdn-images-1.medium.com/max/2000/1*3AvaMUoten0a0LQqpA3VMA.png)

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:

![](https://cdn-images-1.medium.com/max/3156/1*vcjggu9AWdebGnpoSg3rpA.png)

Voltando ao catálogo do port, em **Orders**:

![](https://cdn-images-1.medium.com/max/3804/1*XdhYCF4q9E-dAjigIZgIvg.png)

![](https://cdn-images-1.medium.com/max/3802/1*DL3_sLMO1C0HYlygfriyKQ.png)

AWS stack serverless:

![](https://cdn-images-1.medium.com/max/2012/1*vlk_BE4WUFfS8RoC1m6dNA.png)

![](https://cdn-images-1.medium.com/max/3042/1*n2VK9S9SFxSuO2_wh8rbqw.png)

![](https://cdn-images-1.medium.com/max/2416/1*TixTQBi_z4PQeg0dXTMAyg.png)

DELETE:

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

![](https://cdn-images-1.medium.com/max/3802/1*-mtMUzVKm-G1_CYapXOwqw.png)

*Workflow* de *delete*:

![](https://cdn-images-1.medium.com/max/2348/1*MJAEGxhxxGzc_105eMQH4Q.png)

ArgoCD *prune resources*:

![](https://cdn-images-1.medium.com/max/3098/1*bgdQKL8-2SOUvHkLiJAGPQ.png)

Happy building!

*References:*

[DevOps Toolkit](https://www.youtube.com/@DevOpsToolkit)