Backstage, App template e Crossplane no Amazon EKS [Lab Session]
Tags: community, idp, aws, backstage
Published: 2024-01-30 14:08:06

Seguindo em mais um lab praticando _#platformengineering_ junto com meu amigo [Gabriel Portela](https://www.linkedin.com/in/gabportela/), dessa vez explorando mais o Backstage combinado com GitHub Actions, Crossplane, ArgoCD. E claro, rodando no kubernetes.
> **An open platform for building developer portals**
> Powered by a centralized software catalog, Backstage restores order to your infrastructure and enables your product teams to ship high-quality code quickly — without compromising autonomy.
> [https://backstage.io/](https://backstage.io/)

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

* [Repositório de infraestrutura (terraform) — EKS, ArgoCD, Crossplane](https://github.com/paulofponciano/EKS-Crossplane-ArgoCD.git)

* [Repositório do backstage](https://github.com/paulofponciano/backstage-app.git)

* [Github Organization — Reusable workflows, GitOps, Repositórios publicados, Templates](https://github.com/orgs/paulofponciano-idp/repositories)

> **Considerações:**
> Todos os recursos que rodam no kubernetes, estão no mesmo cluster para esse lab. O post [Serverless com Crossplane composition no EKS + GitOps [Lab Session]](https://medium.com/@paulofponciano/serverless-com-crossplane-composition-no-eks-gitops-lab-session-16e42f9f3d3e) pode ajudar no setup do cluster EKS já com ArgoCD e Crossplane. E claro, é possível rodar em qualquer cluster kubernetes.

---

### Novo app backstage

Inicialmente, criamos o *app* backstage localmente para inicializar o repositório git com a estrutura necessária e criar a primeira imagem docker que posteriomente, entregaremos no kubernetes.

[Documentation on Creating an App Backstage](https://backstage.io/docs/getting-started/create-an-app)

[Building a Docker image | Backstage Software Catalog and Developer Platform](https://backstage.io/docs/deployment/docker/)

- Criando o *app*:

` npx @backstage/create-app@latest`

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

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

` cd backstage`

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

`yarn install --frozen-lockfile`
`yarn tsc`

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

`yarn build:backend`

![](https://cdn-images-1.medium.com/max/2328/1*D9rAWs-NWNYwZi3bcqvUgw.png)

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

Até aqui, tudo *default*. Neste lab, utilizamos também alguns outros *plugins* do backstage, então adicionamos:

`yarn add --cwd packages/backend @backstage/plugin-kubernetes-backend`
`yarn add --cwd packages/app @backstage/plugin-kubernetes`
`yarn add --cwd packages/app @backstage/integration-aws-node`
`yarn add --cwd packages/app @backstage/plugin-home`

Caso siga com o padrão, não é necessário adicionar os *plugins* acima, porém o código/config que está no [repositório](https://github.com/paulofponciano/backstage-app.git) do lab precisa deles. Assim também é necessário modificar no typescript. Seguimos as documentações:

[Installation | Backstage Software Catalog and Developer Platform](https://backstage.io/docs/features/kubernetes/installation)

[Backstage homepage - Setup and Customization | Backstage Software Catalog and Developer Platform](https://backstage.io/docs/getting-started/homepage/)

[@backstage/integration-aws-node | Backstage Software Catalog and Developer Platform](https://backstage.io/docs/reference/integration-aws-node/)

- Criando imagem docker e subindo para o DockerHub. Já existe um Dockerfile para *build* do *backend*, foi criado junto com a estrutura inicial do *app* quando rodamos o npx:

`docker image build . -f packages/backend/Dockerfile --tag backstage-app`

![](https://cdn-images-1.medium.com/max/2326/1*2bZGMdvjB5yqq038PMM5Jw.png)

`docker images`

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

`docker tag db926b361c29 paulofponciano/backstage-app:latest`
`docker push paulofponciano/backstage-app:latest`

![](https://cdn-images-1.medium.com/max/2246/1*2XUhZqaKSLVOyGYwKr1r_w.png)

Agora basta subir o código para o GitHub. Para facilitar nossa vida, criamos também um *workflow* para que esse *build* seja feito de forma automática sempre que subirmos alguma alteração para o repositório:

- backstage-app/.github/workflows/master.yaml

```yaml
name: Main Master Build

on:
  push:
    branches: [main]
    paths-ignore:
      - 'k8s/**'
      - 'catalog-entities/**'

jobs:
  build:
    runs-on: ubuntu-latest

    env:
      CI: true
      NODE_OPTIONS: --max-old-space-size=4096

    steps:
      - uses: actions/checkout@v3

      # Beginning of yarn setup
      - name: use node.js 18.x
        uses: actions/setup-node@v3
        with:
          node-version: 18.x
          registry-url: https://registry.npmjs.org/ # Needed for auth

      - name: cache all node_modules
        id: cache-modules
        uses: actions/cache@v3
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}

      - name: find location of global yarn cache
        id: yarn-cache
        if: steps.cache-modules.outputs.cache-hit != 'true'
        run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT

      - name: cache global yarn cache
        uses: actions/cache@v3
        if: steps.cache-modules.outputs.cache-hit != 'true'
        with:
          path: ${{ steps.yarn-cache.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-

      - name: yarn install
        run: yarn install --immutable
      # End of yarn setup

      - name: type checking and declarations
        run: yarn tsc:full

      - name: build
        run: yarn --cwd packages/backend build

      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USER }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: build image and push to docker hub
        uses: docker/build-push-action@v4
        with: 
          context: .
          file: packages/backend/Dockerfile
          push: true
          tags: paulofponciano/backstage:${{ github.sha }}, paulofponciano/backstage:latest
```

Estamos usando os *secrets* armazenados em *repository secrets* do GitHub para conectar ao DockerHub:

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

### GitHub OAuth App

Para autenticação, utilizamos também o GitHub na parte de OAuth Apps.

- Basta criar uma nova *application* (Settings > Developer settings > OAuth Apps). Com isso, teremos um *Client ID* e *Client secrets*, que utilizaremos como um *secret* do kubernetes para o backstage. É necessário informar a *Homepage URL* e *Authorization callback URL:*

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

### Manifestos k8s

Criamos um diretório ['k8s'](https://github.com/paulofponciano/backstage-app/tree/main/k8s) no repositório do backstage para concentrar os manifestos que aplicamos no cluster.

- Criar namespace:

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: backstage
  labels:
    istio-injection: enabled
```

- Backstage secrets:

```yaml
# kubernetes/backstage-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: backstage-secrets
  namespace: backstage
type: Opaque
data:
  GITHUB_TOKEN: '' # sample base 64
```

Este *GITHUB_TOKEN* é necessário para que o backstage consiga acessar os repositórios onde estão os catálogos, templates, etc. Basta gerar um PAT (*Personal access token*) e colocar em base 64. No GitHub: **Settings > Developer Settings > Personal access tokens (Classic)**.

- Postgres secrets:

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secrets
  namespace: backstage
type: Opaque
data:
  POSTGRES_USER: YmFja3N0YWdl # sample base 64
  POSTGRES_PASSWORD: aHVudGVyMg== # sample base 64
```

- Postgres — Deployment, PVC, Service:

```yaml
---
# kubernetes/postgres-storage.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: postgres-storage
  namespace: backstage
  labels:
    type: local
spec:
  storageClassName: gp3
  capacity:
    storage: 2G
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: '/mnt/data'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-storage-claim
  namespace: backstage
spec:
  storageClassName: gp3
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2G
---
# kubernetes/postgres.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: backstage
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:13.2-alpine
          imagePullPolicy: 'IfNotPresent'
          ports:
            - containerPort: 5432
          envFrom:
            - secretRef:
                name: postgres-secrets
          volumeMounts:
            - mountPath: /var/lib/postgresql/data
              name: postgresdb
      volumes:
        - name: postgresdb
          persistentVolumeClaim:
            claimName: postgres-storage-claim
---
# kubernetes/postgres-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: backstage
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
```

- GitHub Auth Secrets:

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: github-auth-secrets
  namespace: backstage
type: Opaque
data:
  AUTH_GITHUB_CLIENT_ID: '' # base 64
  AUTH_GITHUB_CLIENT_SECRET: '' # base 64
```

Neste informamos o *Client ID* e *Client secret* gerados anteriormente, quando criamos o **GitHub OAuth App**.

- Backstage ingestion secret:

`kubectl -n kube-system create serviceaccount backstage-ingestion`
`kubectl create clusterrolebinding backstage-ingestion --clusterrole=cluster-admin --serviceaccount=kube-system:backstage-ingestion`

```
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: backstage-ingestion
  namespace: kube-system
  annotations:
    kubernetes.io/service-account.name: backstage-ingestion
type: kubernetes.io/service-account-token
EOF
```

`kubectl -n kube-system get secret backstage-ingestion -o go-template='{{.data.token | base64decode}}'`

A saída desse comando é o *Service account token*, que utilizamos como **SA_TOKEN** no *plugin* de kubernetes do backstage, possibilitando a ingestão dos dados do *cluster* para o backstage.

- EKS secrets:

```yaml
# kubernetes/postgres-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: eks-secrets
  namespace: backstage
type: Opaque
data:
  SA_TOKEN: '' # base 64
  CA_DATA: '' # base 64

```

Informamos o **SA_TOKEN** gerado anteriormente e o *certificate authority* gerado para o EKS. No *secrets* como **CA_DATA**. É possível localizar pela console do EKS:

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

- Backstage — Deployment, SA, Service:

```yaml
# kubernetes/backstage.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backstage
  namespace: backstage
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backstage
  template:
    metadata:
      labels:
        app: backstage
    spec:
      serviceAccountName: backstage-service-account
      containers:
        - name: backstage
          image: paulofponciano/backstage:latest
          imagePullPolicy: 'Always'
          ports:
            - name: http
              containerPort: 7007
          envFrom:
            - secretRef:
                name: postgres-secrets
            - secretRef:
                name: backstage-secrets
            - secretRef:
                name: github-auth-secrets
            - secretRef:
                name: eks-secrets
          env:
          - name: POSTGRES_PORT
            value: "5432"
          - name: POSTGRES_HOST
            value: "postgres.backstage.svc.cluster.local"
# Uncomment if health checks are enabled in your app:
# https://backstage.io/docs/plugins/observability#health-checks
#          readinessProbe:
#            httpGet:
#              port: 7007
#              path: /healthcheck
#          livenessProbe:
#            httpGet:
#              port: 7007
#              path: /healthcheck
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: backstage-service-account
  namespace: backstage
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: backstage-cluster-ro
subjects:
- namespace: backstage
  kind: ServiceAccount
  name: backstage-service-account
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:aggregate-to-view
---
# kubernetes/backstage-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: backstage
  namespace: backstage
spec:
  selector:
    app: backstage
  ports:
    - name: http
      port: 80
      targetPort: http

```

- Backstage ingress — Istio Gateway, Virtual Service:

```yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: backstage-gateway
  namespace: backstage
spec:
  selector:
    istio: ingressgateway
  servers:
    - hosts:
        - backstage.pauloponciano.digital
      port:
        name: http
        number: 80
        protocol: HTTP
      tls:
        httpsRedirect: true
    - hosts:
        - backstage.pauloponciano.digital
      port:
        name: https-workloads
        number: 443
        protocol: HTTP
      tls:
        mode: PASSTHROUGH
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: backstage
  namespace: backstage
spec:
  gateways:
    - backstage-gateway
  hosts:
    - backstage.pauloponciano.digital
  http:
    - route:
        - destination:
            host: backstage.backstage.svc.cluster.local
            port:
              number: 80
          weight: 100
```

### Backstage app-config.yaml

No repositório do backstage, temos o arquivo *app-config.yaml* com as configurações e integrações. Até o momento, esse arquivo está *default*. Para esse lab, deixamos conforme abaixo e fizemos *commit* e *push* para o repo, acionando assim um novo *build* com GitHub Actions (*workflow master.yaml*) que já criamos.

```yaml
app:
  title: Community Backstage App
  baseUrl: https://backstage.pauloponciano.digital

organization:
  name: Community

backend:
  baseUrl: https://backstage.pauloponciano.digital
  listen:
    port: 7007
  csp:
    connect-src: ["'self'", 'http:', 'https:']
  cors:
    origin: https://backstage.pauloponciano.digital
    methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
    credentials: true
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}

integrations:
  github:
    - host: github.com
      token: ${GITHUB_TOKEN}
  aws:
    mainAccount:
    accounts:
      - accountId: ${AWS_ACCOUNT_ID}
        accessKeyId: ${AWS_ACCESS_KEY_ID}
        secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
        region: ${AWS_REGION}

proxy:
  '/test':
    target: 'https://example.com'
    changeOrigin: true
techdocs:
  builder: 'local' # Alternatives - 'external'
  generator:
    runIn: 'docker' # Alternatives - 'local'
  publisher:
    type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives.

auth:
  environment: production
  providers:
    github:
      production:
        clientId: ${AUTH_GITHUB_CLIENT_ID}
        clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}

scaffolder:
  defaultAuthor:
    name: ":robot: [backstage-bot]"
    email: paulofponciano@gmail.com

catalog:
  import:
    entityFilename: catalog-info.yaml
    pullRequestBranchName: backstage-integration
  rules:
    - allow: [Component, System, Group, Resource, Location, Template, API]
  locations:
    - type: url
      target: https://github.com/paulofponciano/backstage-app/blob/main/catalog-entities/locations.yaml

kubernetes:
  serviceLocatorMethod:
    type: 'multiTenant'
  clusterLocatorMethods:
    - type: 'config'
      clusters:
        - url: https://api-server-endpoint.gr7.us-east-2.eks.amazonaws.com
          name: pegasus # cluster name
          authProvider: 'serviceAccount'
          skipTLSVerify: false
          skipMetricsLookup: true
          serviceAccountToken: ${SA_TOKEN}
          caData: ${CA_DATA}
          customResources:
            - group: 'api.pauloponciano.digital' # Crossplane
              apiVersion: 'v1alpha1'
              plural: 'xcustomdatabases'
            - group: 'api.pauloponciano.digital' # Crossplane
              apiVersion: 'v1alpha1'
              plural: 'xcustomorders'
```

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

### Backstage

Após finalizar a etapa anterior com sucesso no *build*, aplicamos todos os manifestos k8s no *cluster*. Pode ser feito em sequência:

`kubectl apply -f backstage_ns.yaml`
`kubectl apply -f backstage_secrets.yaml`
`kubectl apply -f postgres_secrets.yaml`
`kubectl apply -f postgres.yaml`
`kubectl apply -f github_auth_secrets.yaml`
`kubectl apply -f eks_secrets.yaml`
`kubectl apply -f backstage.yaml`
`kubectl apply -f backstage_istio_ingress.yaml`

Como criamos um CNAME em um DNS Público apontando para o NLB de entrada do istio, já podemos acessar o backstage:

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

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

Já temos alguns componentes que foram registrados na subida do *app*:

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

Esse *import* vem por esse bloco do *app-config.yaml. *Algumas dessas entidades no catálogo já não são *default*, mas servem de exemplo. Podem encontra-las [aqui](https://github.com/paulofponciano/backstage-app/tree/main/catalog-entities).

```yaml
catalog:
  import:
    entityFilename: catalog-info.yaml
    pullRequestBranchName: backstage-integration
  rules:
    - allow: [Component, System, Group, Resource, Location, Template, API]
  locations:
    - type: url
      target: https://github.com/paulofponciano/backstage-app/blob/main/catalog-entities/locations.yaml
```

### GitHub Organization

- Criamos uma [Organization](https://github.com/orgs/paulofponciano-idp/repositories) no GitHub para centralizar os repositórios de *templates* de *apps*, gitops (ArgoCD) e *workflows* reutilizáveis (GitHub Actions). Os repositórios criados pelo backstage com base nos *templates* para novos *apps*, também ficam na organização:

![](https://cdn-images-1.medium.com/max/3330/1*0d_UaMh8S1308FXaLg8QdA.png)

- Definimos também os *secrets* a nível de organização para utilizar nos *workflows*. Neste momento, o **GH_PAT** é o mesmo *Personal access token* que criamos para passar no *secrets* do kubernetes:

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

### ArgoCD

O ArgoCD é o recurso central para as entregas no *cluster* EKS, nos permitindo utilizar a estratégia de GitOps. Assim conectamos o Argo ao repositório ['gitops'](https://github.com/paulofponciano-idp/gitops.git) da organização com a estrutura abaixo:

* deployed_apps/applications

* deployed_infra/system:default

![](https://cdn-images-1.medium.com/max/2512/1*Qs_P8pvdkRdVT-L1faOjVw.png)

Utilizamos esse manifesto para criar as *applications*, *project* e conectar ao repositório:

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: public-repo-gitops
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  type: git
  url: https://github.com/paulofponciano-idp/gitops.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-idp/gitops.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-idp/gitops.git
    targetRevision: HEAD
    path: deployed_infra/system:default
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated:
      selfHeal: true
      prune: true
      allowEmpty: true
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: idp-apps
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: idp
  source:
    repoURL: https://github.com/paulofponciano-idp/gitops.git
    targetRevision: HEAD
    path: deployed_apps/applications
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated:
      selfHeal: true
      prune: true
      allowEmpty: true
```

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

### Template GO (Backstage fetch:template)

No repositório [go-template-backstage](https://github.com/paulofponciano-idp/go-template-backstage.git) da organização, temos uma estrutura padrão como exemplo para uma aplicação em golang. Esse repositório serve como *tamplate* para o backstage quando algum consumidor acionar a criação de um novo *app*.

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

Uma parte importante desse *template*, é o *workflow* que também será utilizado no momento em que o backstage realizar o *fetch* e criar um novo repositório (*push*). Esse *workflow* é responsável por acionar outros *workflows* (*workflow_call*) que estão no repositório [.github](https://github.com/paulofponciano-idp/.github.git) da organização.

Do lado do backstage utilizamos o *template* *app-go.yaml* abaixo, que guardamos no próprio [repositório do backstage](https://github.com/paulofponciano/backstage-app/tree/main/catalog-entities/templates) nesse caso:

```yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: golang-template
  title: Golang Template
  description: Create a new Golang App

  tags:
    - golang
    - website
    - component
spec:
  owner: group:sre
  type: component

  parameters:
    - title: Provide some simple information
      required:
        - component_id
        - system
        - lifecycle
        - owner
        - type
      properties:
        component_id:
          title: Name
          type: string
          pattern: "^([a-z0-9\\-]+)$"
          description: Unique name of the component
          ui:field: EntityNamePicker
          ui:autofocus: true
        description:
          title: Description
          type: string
          description: Help others understand what this website is for.
        system:
          title: System
          type: string
          description: System of the component
          ui:field: EntityPicker
          ui:options:
            allowedKinds:
              - System
            defaultKind: System
        lifecycle:
          title: Lifecycle
          description: 'Application lifecycle'
          type: string
          default: experimental
          enum:
            - deprecated
            - experimental
            - production
        type:
          title: Type
          description: 'Application type'
          type: string
          default: service
          enum:
            - service
            - website
            - library
        owner:
          title: Owner
          type: string
          description: Owner of the component
          ui:field: OwnerPicker
          ui:options:
            allowedKinds:
              - Group

    - title: Choose a location
      required:
        - repoUrl
      properties:
        repoUrl:
          title: Repository Location
          type: string
          ui:field: RepoUrlPicker
          ui:options:
            allowedHosts:
              - github.com
            allowedOwners:
              - paulofponciano-idp

    - title: Infrastructure
      properties:
        kube:
          title: Create Kubernetes App
          description: Checking this will also create Kubernetes App
          type: boolean
          default: false
        env:
          title: Environment
          description: 'Environment to create resources'
          type: string
          default: dev
          enum:
            - dev
            - stg
            - prd
        dryRun:
          title: Only perform a dry run, don't publish anything
          type: boolean
          default: false

  steps:
    - id: template
      name: Fetch Application Template on GitHub Repo
      action: fetch:template
      cookiecutterCompat: true
      input:
        url: https://github.com/paulofponciano-idp/go-template-backstage
        copyWithoutTemplating:
          - .github/workflows/*
        values:
          component_id: ${{ parameters.component_id }}
          system: ${{ parameters.system }}
          description: ${{ parameters.description }}
          destination: ${{ parameters.repoUrl | parseRepoUrl }}
          owner: ${{ parameters.owner }}
          lifecycle: ${{ parameters.lifecycle }}
          type: ${{ parameters.type }}
          env: ${{ parameters.env}}

    - id: publish
      name: Publish Application
      action: publish:github
      if: ${{ parameters.dryRun !== true }}
      input:
        allowedHosts:
          - github.com
        description: This is ${{ parameters.component_id }}
        repoUrl: ${{ parameters.repoUrl }}
        defaultBranch: main
        repoVisibility: public
        collaborators:
          - team: sre
            access: maintain
          - team: ${{ parameters.owner }}
            access: push

    - id: fetch-kube
      name: Fetch Kubernetes Template
      action: fetch:template
      if: ${{ parameters.kube == true }}
      input:
        targetPath: ./kube
        url: https://github.com/paulofponciano/backstage-app/tree/main/catalog-entities/skeleton/kubernetes/apps/kustomize
        values:
          component_id: ${{ parameters.component_id }}
          description: ${{ parameters.description }}
          destination: ${{ parameters.repoUrl | parseRepoUrl }}
          owner: ${{ parameters.owner }}

    - id: kube-pr
      name: "Open PR in GitOps Repository"
      action: publish:github:pull-request
      if: ${{ parameters.kube == true }}
      input:
        repoUrl: github.com?repo=gitops&owner=paulofponciano-idp
        branchName: create-${{ parameters.component_id }}
        title: ':robot: [backstage-bot] Create new App ${{ parameters.component_id }}'
        description: |
          # New project: ${{ parameters.component_id }}
          ${{ parameters.description if parameters.description }}
        sourcePath: kube
        targetPath: deployed_apps

    - id: register
      name: Register Application in Catalog
      action: catalog:register
      if: ${{ parameters.dryRun !== true }}
      input:
        repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}
        catalogInfoPath: "/catalog-info.yaml"

  output:
    links:
      - title: Go to Repository
        url: ${{ steps['publish'].output.repoContentsUrl }}
      - title: Go to GitOps Pull Request
        url: ${{ steps.kube-pr.output.remoteUrl }}
      - title: Open in catalog
        icon: catalog
        entityRef: ${{ steps['register'].output.entityRef }}
```

### Skeleton k8s (Backstage fetch:template)

No passo anterior, cuidamos do *template* de aplicação e das ações do backstage no momento de criar essa nova aplicação. Como esse *app* é entregue (argocd) e opera no kubernetes, precisamos também do 'esqueleto' para os manifestos. Guardamos o skeleton [aqui](https://github.com/paulofponciano/backstage-app/tree/main/catalog-entities/skeleton/kubernetes) com essa estrutura:

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

### Registrar template GO no Backstage

Registrar o template é um processo simples, apenas informar a url e importar:

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

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

![](https://cdn-images-1.medium.com/max/5732/1*j1eTK6TykWn-akpspAvOgA.png)

### Criando novo app (go) através do Backstage

Validamos agora tudo que foi criado até o momento, buscando o fluxo que desenhamos:

![](https://cdn-images-1.medium.com/max/2862/1*MT-i3ilNESsmhCAUoWSJDQ.png)

- *Input* das informações:

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

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

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

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

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

- Aprovar *pull-request* no repositório de GitOps:

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

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

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

Podemos ver o *replace* das informações que passamos dentro dos manifestos que o backstage pegou no diretório de 'skeleton', esses manifestos agora estão no diretório 'deployed_apps' de GitOps.

- Checando o novo repositório do *app* e *workflow* de *first-release*:

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

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

O *workflow* de *first-release* é acionado no momento do *push* que o backstage faz para criação do novo repositório. Este por sua vez, faz a chamada (*workflow_call*) dos *workflows* reutilizáveis de *env*, *build* e *deploy*:

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

- Checando o *sync* no ArgoCD, vemos que o novo *app* já está sendo entregue no *cluster*:

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

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

- Agora em nosso Backstage Catalog, temos o novo componente registrado. Podemos visualizar os detalhes:

![](https://cdn-images-1.medium.com/max/5656/1*s5kqDy8Ty7igYz7K-gR1IA.png)

![](https://cdn-images-1.medium.com/max/5656/1*4ENQLPn5s5W1wLv5xgwzPQ.png)

Detalhes dos recursos no kubernetes relacionados a aplicação:

![](https://cdn-images-1.medium.com/max/5656/1*8vJAqnsE711VIBFlTUltRw.png)

![](https://cdn-images-1.medium.com/max/5660/1*MMrv19eW0k-5PpI3W5ITxw.png)

Detalhes de CI/CD do GitHub Actions:

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

![](https://cdn-images-1.medium.com/max/5656/1*0O56Py2afv7Jlm4uELfyGA.png)

Acessando o *app* de exemplo:

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

### Backstage + Crossplane para provisionar recursos

Levando em conta que já temos o crossplane rodando no *cluster* EKS como parte do *setup* da infraestrutura, podemos utiliza-lo no cenário de provisionamento de recursos junto ao backstage, neste lab usando AWS como *provider*. A estratégia é a mesma, utilizar o backstage para fazer o *input* das informações que serão passadas em um manifesto (*claim*) e entregar com ArgoCD.

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

- Registrar o [template de rds](https://github.com/paulofponciano/backstage-app/blob/main/catalog-entities/templates/database.yaml) no backstage:

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

![](https://cdn-images-1.medium.com/max/5736/1*IpxVFhpegMek-bn2kBrR5Q.png)

- Skeleton do *claim* para composition de rds:

```yaml
# CLAIM
---
apiVersion: api.pauloponciano.digital/v1alpha1
kind: CustomDatabase
metadata:
  name: ${{values.name}}-${{values.env}}-${{values.engine}}-db
  namespace: environment-crossplane
  labels:
    backstage.io/kubernetes-id: rds
spec:
  compositionSelector:
    matchLabels:
      db-engine: ${{values.engine}}
  resourceConfig:
    providerConfigName: aws
    region: ${{values.region}}
    size: ${{values.size}}
    engine: ${{values.engine}}
    tags:
      automation-by: crossplane
      ownerName: ${{values.owner}}
```

*Composition* e *definition* já devem estar aplicadas no *cluster* para o crossplane. Podem encontra-las [aqui](https://github.com/paulofponciano/EKS-Crossplane-ArgoCD/tree/main/crossplane_compositions/rds).

![](https://cdn-images-1.medium.com/max/2214/1*4ezUHaf8Dd_Af3gtwX0j3w.png)

- *Input* das informações:

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

![](https://cdn-images-1.medium.com/max/3760/1*50Ofpi6UYOAEisCClzB_Tg.png)

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

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

- Aprovar *pull-request* no repositório de GitOps:

![](https://cdn-images-1.medium.com/max/5372/1*n8ZmE2C5uNQiB-d-f9dnAA.png)

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

- Checando o *sync* no ArgoCD, na *application* 'idp-infra-aws':

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

- No catálogo do backstage, podemos checar que a composite do crossplane foi criada no *cluster*:

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

- Olhando na console AWS, podemos confirmar a criação do recurso:

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

---

Com isso, podemos ver grandes possibilidades e facilidades que o backstage combinado com outras soluções, pode trazer para os times de uma organização.

Agradecimento a todos que publicam conteúdos abertos para fortalecer a comunidade, e que ajudaram muito nesse lab.

[https://github.com/devxp-tech](https://github.com/devxp-tech)

[https://github.com/diegoluisi](https://github.com/diegoluisi)

{% embed https://youtu.be/Ii-lpLuzPxw?si=FppNuw5KmWJAqt9q %}

---

Happy building!