Backstage Production
Até o presente momento, o Backstage não fornece imagens de contêiner nem helm charts oficiais. A única coisa que temos é um código-fonte vindo de um template e um Dockefile.
Se quisermos executar o Backstage no kubernetes precisamos criar um Dockerfile e os manifestos. O chart contido no código fonte do Backstage somente funciona para demonstração rodando uma imagem que não é personalizada para o seu propósito.
Quando rodamos o comando npx @backstage/create-app@latest
foi gerado os códigos default que trabalhamos em cima para aprender, mas fiquei pensando como poderia manter o Backstage de forma produtiva. Fiquei bem preocupado com isso, pois se vamos mudar a cultura do desenvolvimento e o Backstage for o centro de tudo, se ele parar teremos um problema geral. Então fiquei pensando em como mantê-lo atualizado, distribuído, escalável e com segurança.
Estratégias
O Backstage é um projeto que caiu nas graças da comunidade a pouco tempo então esta ganhando muita melhoria. Com certeza será necessário melhorar a arquitetura para que possa sofrer updates de forma mais eficaz. Acabou de acontecer uma evolução de arquitetura na parte do backend, mas o frontend ainda está andamento. Como manter esse projeto atualizado?
Trabalhando por fora
Por enquanto para obter o projeto mais atualizado é necessário recriar a aplicação do zero a partir do comando npx @backstage/create-app@latest
. Gerando esse código em uma pasta source
por exemplo, teremos o código limpo mais perto da documentação oficial. Poderíamos sempre fazer isso para obter a nova versão do projeto. Porém, todo arquivo que sofrer alteração deve ser copiado para fora da pasta source, na pasta raiz, mantendo a mesma estrutura de pastas, e copiado para dentro no momento do build. Mas isso gera problemas e esforços desnecessários.
- Complicado de desenvolver localmente.
- Necessário vários builds de imagem.
- Díficil de debugar e ver alterações em tempo real.
- Necessário pipelines específicas para o build da imagem.
- Necessidade de alterar dockerfile e os fontes do COPY.
- etc.
Não gostei muito dessa abordagem. Eu tentei e perdi tempo. No fundo percebi que acabaria com o mesmo código na pasta raiz e na pasta source e traria complexidade desnecessária.
Trabalhando por dentro
Lendo melhor sobre como funciona o update do Backstage me senti mais confortável em manter um único código. O prório yarn irá nos ajudar a manter pacotes atualizados e as correções precisamos fazer mesmo trabalhando por fora.
Vamos criar o Dockerfile na raiz do projeto baseado na documentação oficial utilizando um multistage.
############################## PREPARANDO O YARN ##################################
FROM node:20-bookworm-slim AS packages
WORKDIR /app
COPY package.json yarn.lock ./
COPY .yarn ./.yarn
COPY .yarnrc.yml ./
COPY packages packages
COPY plugins plugins
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+
############################## INSTALANDO OS PACOTES E DEPENDÊNCIAS ##################################
FROM node:20-bookworm-slim AS build
# Set Python interpreter for `node-gyp` to use
ENV PYTHON=/usr/bin/python3
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends python3 g++ build-essential && \
rm -rf /var/lib/apt/lists/*
# Podemos depois ter uma segundo Dockefile para produção removendo o sqlite para diminuir o imagem pois não será usado em produção
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends libsqlite3-dev && \
rm -rf /var/lib/apt/lists/*
USER node
WORKDIR /app
COPY --from=packages --chown=node:node /app .
COPY --from=packages --chown=node:node /app/.yarn ./.yarn
COPY --from=packages --chown=node:node /app/.yarnrc.yml ./
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
yarn install --immutable
COPY --chown=node:node . .
RUN yarn tsc
RUN yarn --cwd packages/backend build
RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \
&& tar xzf packages/backend/dist/skeleton.tar.gz -C packages/backend/dist/skeleton \
&& tar xzf packages/backend/dist/bundle.tar.gz -C packages/backend/dist/bundle
############################## FINAL ##################################
FROM node:20-bookworm-slim
ENV PYTHON=/usr/bin/python3
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends python3 python3-pip python3-venv g++ build-essential && \
rm -rf /var/lib/apt/lists/*
# Podemos depois ter uma segundo Dockefile para produção removendo o sqlite para diminuir o imagem pois não será usado em produção
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends libsqlite3-dev && \
rm -rf /var/lib/apt/lists/*
# Para rodar o techdocs localmente sem precisar de docker in docker (ADICIONADO)
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN pip3 install mkdocs-techdocs-core
USER node
WORKDIR /app
COPY --from=build --chown=node:node /app/.yarn ./.yarn
COPY --from=build --chown=node:node /app/.yarnrc.yml ./
COPY --from=build --chown=node:node /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton/ ./
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
yarn workspaces focus --all --production && rm -rf "$(yarn cache clean)"
COPY --from=build --chown=node:node /app/packages/backend/dist/bundle/ ./
COPY --chown=node:node app-config*.yaml ./
COPY --chown=node:node examples ./examples
# Esse será o default caso nada seja passado
ENV NODE_ENV=production
ENV NODE_OPTIONS="--no-node-snapshot"
CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"]
Durante o build da imagem o docker verifica o .dockerignore. O que vem inicialmente dará problema, sendo necessário alterar para:
dist-types
node_modules
packages/*/dist
packages/*/node_modules
plugins/*/dist
plugins/*/node_modules
*.local.yaml
Antes de buildar precisamos ajustar como vamos trabalhar com os configs. Nessa idéia é importante entender o app-config.yaml será a base para produção. Em produção alteraremos isso.
app:
baseUrl: ${BACKSTAGE_HOST} # Será o nosso domínio, não localhost
backend:
baseUrl: ${BACKSTAGE_HOST} # Será o nosso domínio, não localhost
listen: ':7007' # Deixei aqui, mas esse precisa ser o mesmo listen de app-config.yaml. Não precisava estar.
database: # Definimos um database
client: pg
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
catalog:
locations: # Removido os exemplos
kubernetes:
frontend: # Vamos dar possibilidade que o pod seja deletado em produção no plugin do kuberentes. Em desenvolvimento não será possível deletar
podDelete:
enabled: true
Agora vamos ao app-config.yaml que é a base para produção. É necessário alguns ajustes para que possamos ter um container em modo developement.
app:
title: Scaffolded Backstage App
baseUrl: http://localhost:7007 # MESMA PORTA DO BACKEND. Originalmente era porta 3000, precismos acertar no app-config.local.yaml
organization:
name: My Company
backend:
baseUrl: http://localhost:7007
listen: ':7007'
csp:
connect-src: ["'self'", 'http:', 'https:']
cors:
origin: http://localhost:3000
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
database: # O database padrão é em memória, em produção fizemos a alteração
client: better-sqlite3
connection: ':memory:'
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}
gitlab:
- host: gitlab.com
token: ${GITLAB_TOKEN}
proxy:
'/argocd/api':
target: http://argo.localhost/api/v1/
changeOrigin: true
# only if your argocd api has self-signed cert
secure: false
headers:
Cookie:
$env: ARGOCD_AUTH_TOKEN_BACKSTAGE
builder: 'local'
generator:
runIn: 'local'
publisher:
type: 'local'
local:
publishDirectory: '/app/backstage-site'
auth:
providers:
github:
development:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
gitlab:
development:
clientId: ${AUTH_GITLAB_CLIENT_ID}
clientSecret: ${AUTH_GITLAB_CLIENT_SECRET}
scaffolder:
defaultAuthor:
name: Backstage Scaffolder
catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow: [Component, System, API, Resource, Location, Group, User, Domain]
providers:
github:
github:
organization: ${GITHUB_ACCOUNT}
schedule:
frequency: { minutes: 3 }
timeout: { minutes: 3 }
filters:
branch: 'main' # string
repository: '.*' # Regex
catalogPath: '/**/catalog-info.{yaml,yml}'
locations: # Isso foi removido de produção
# Arquivos locais de exemplo podem ser carregados em desenvolvimento porém ao invés de usar ../examples rodando localmente com yarn dev é necessário ajustar para ./examples pois dentro do container o caminho é diferente.
- type: file
target: ./examples/entities.yaml
- type: file
target: ./examples/template/template.yaml
rules:
- allow: [Template]
- type: file
target: ./examples/org.yaml
rules:
- allow: [User, Group]
kubernetes:
# Aqui nao temos a permissão de deletar pod
serviceLocatorMethod:
type: 'multiTenant'
clusterLocatorMethods:
- type: 'config'
clusters:
- name: 'cluster-kind'
# url: 'https://localhost:6443' Ajustamos isso para local
url: 'https://kubernetes.default.svc' # Dentro do cluster o pod precisa referenciar o service do kubernetes. Localhost seria dentro do próprio pod.
authProvider: 'serviceAccount'
skipTLSVerify: true
skipMetricsLookup: true
serviceAccountToken: ${KUBERNETES_BACKSTAGE_SA_TOKEN}
permission:
enabled: true
events:
http:
topics:
- gitlab
- github
Agora para rodar isso em um ambiente local, precisamos ter o app-config.local.yaml para acertar as modificações que fizemos no app-config.yaml para rodar em container. Rodar o yarn dev implica em carregar a base app-config.yaml e depois sobreescrever o que temos em app-config.local.yaml.
Lembre de remover esse arquivo do .gitignore!
app:
baseUrl: http://localhost:3000 # muda a porta do frontend em relação ao backend
techdocs:
generator:
runIn: 'docker' # usa o docker ao invés de local
publisher:
local:
publishDirectory: '/tmp/backstage'
auth:
# habilita o guest
guest: {}
catalog:
rules:
- allow: [Component, System, API, Resource, Location, Group, User, Domain]
locations: # Corrimos os path de ./examples para ../../examples
- type: file
target: ../../examples/entities.yaml
- type: file
target: ../../examples/template/template.yaml
rules:
- allow: [Template]
- type: file
target: ../../examples/org.yaml
rules:
- allow: [User, Group]
kubernetes:
clusterLocatorMethods:
- type: 'config'
clusters:
- name: 'cluster-kind'
url: 'https://localhost:6443' # Reajustado para localhost quando estivermos desenvolvendo localmente
authProvider: 'serviceAccount'
skipTLSVerify: true
skipMetricsLookup: true
serviceAccountToken: ${KUBERNETES_BACKSTAGE_SA_TOKEN}
Agora funciona o yarn dev.
Buildando a Imagem
Para buildar e rodar em desenvolvimento inicalmente
docker build -t backstage . --no-cache
# Para rodar lembre de ter todas as variáveis exportadas
# Aqui estamos rodando como development pois alteramos o NODE_ENV e vamos mudar o CMD.
docker container run --rm --interactive --tty --publish 7007:7007 \
--env NODE_ENV=development \
--env GITHUB_ACCOUNT=$GITHUB_ACCOUNT \
--env GITHUB_USER=$GITHUB_USER \
--env GITHUB_USER=$GITHUB_USER \
--env GITHUB_TOKEN=$GITHUB_TOKEN \
--env GITLAB_TOKEN=$GITLAB_TOKEN \
--env ARGOCD_AUTH_TOKEN_BACKSTAGE=$ARGOCD_AUTH_TOKEN_BACKSTAGE \
--env AUTH_GITHUB_CLIENT_ID=$AUTH_GITHUB_CLIENT_ID \
--env AUTH_GITHUB_CLIENT_SECRET=$AUTH_GITHUB_CLIENT_SECRET \
--env AUTH_GITLAB_CLIENT_ID=$AUTH_GITLAB_CLIENT_ID \
--env AUTH_GITLAB_CLIENT_SECRET=$AUTH_GITLAB_CLIENT_SECRET \
--env KUBERNETES_BACKSTAGE_SA_TOKEN=$KUBERNETES_BACKSTAGE_SA_TOKEN \
backstage node packages/backend --config app-config.yaml
Para fazer o push...
# mude para o seu repositório
docker tag backstage:latest davidpuziol/backstage:v1.0.0
docker push davidpuziol/backstage:v1.0.0
Agora já temos uma imagem para rodar em kubernetes. Só para lembrar, o kubernetes precisa conseguir fazer o pull da imagem, se o repositório for público será necessário as credenciais do repositório.
kubernetes
O que precisamos para rodar em kubernetes?
- Precisamos de um deployment e nele precisaremos referenciar várias variáveis de ambiente. Se for production temos um conjunto diferentes se development.
- Definir os healchecks será feito posteriormente pois necessita de usar plugins extras.
- Precisamos uma service account para não usar o default do namespace, que não precisa ser montada no pod pois não terá acesos a api por essa service account.
- Precisamos de um service para controlar acesso aos pods.
- Precisamos de um ingress para expor o nosso Backstage.
- Precisamos de um volume para guardar os docs (farei isso depois) caso não use um serviço externo, mas acho que não é necessário uma vez que a documentação sempre se refaz.
- Precisamos de um hpa para escalomento horizontal
Confira no repositório davidpuziol/backstage na pasta backstage os código para helm chart que criei. Para nao ficar redundante vamos veja o values.yaml no repositório.
Requisitos:
- Necessário criar a secret com todas as variáveis de ambiente definidas.
- Alterar o values.yaml para sua configuração
- Altere o repositório e a tag.
- Altere o nome da secret apontando para a que voce criou se necessário for.
- Altere o ingress host para o que você definiu.
- Altere o postgres host para onde esta os eu banco
- Altere o hpa se quiser.
Esse é só um chart inicial, que vamos alterar ainda defindo um volume para guardar os techdocs, readness e liveness para garantir o healthcheck, security context para implementar mais segurança, e analisar melhor o resource com teste de carga.
## Com o repositório clonado na pasta backstage
helm install backstage -n backstage .
## Um geral do que temos no namespace backstage além do que acabamos de deployar.
## Temos o postgres (statefulset) rodando no mesmo namespace como foi especificado no lab.
## Temos a service account backstage-sa e o token que vamos utilizar no backstage.
## Temos a secret backstage-env-secrets com todas as credenciais necessárias.
kubectl get deployments.apps,ingress,svc,replicasets.apps,statefulsets.apps,pod,serviceaccounts,persistentvolume,persistentvolumeclaims,secrets,configmaps -n backstage
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/backstage 2/2 2 2 6h32m
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/backstage nginx backstage.localhost localhost 80 6h32m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/backstage ClusterIP 10.101.44.219 <none> 7007/TCP 6h32m
service/backstage-postgres-postgresql ClusterIP 10.101.74.104 <none> 5432/TCP 2d19h
service/backstage-postgres-postgresql-hl ClusterIP None <none> 5432/TCP 2d19h
NAME DESIRED CURRENT READY AGE
replicaset.apps/backstage-74857d8858 2 2 2 6h32m
NAME READY AGE
statefulset.apps/backstage-postgres-postgresql 1/1 2d19h
NAME READY STATUS RESTARTS AGE
pod/backstage-74857d8858-64qnd 1/1 Running 2 (6h31m ago) 6h32m
pod/backstage-74857d8858-sfsnf 1/1 Running 0 6h32m
pod/backstage-postgres-postgresql-0 1/1 Running 2 (6h28m ago) 2d19h
NAME SECRETS AGE
serviceaccount/backstage 0 6h32m
serviceaccount/backstage-postgres-postgresql 0 2d19h
serviceaccount/backstage-sa 0 2d19h
serviceaccount/default 0 2d19h
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
persistentvolume/pvc-5f559753-bb50-4383-a217-1d7fcdf27fbe 5Gi RWO Delete Bound backstage/data-backstage-postgres-postgresql-0 standard <unset> 2d19h
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/data-backstage-postgres-postgresql-0 Bound pvc-5f559753-bb50-4383-a217-1d7fcdf27fbe 5Gi RWO standard <unset> 2d19h
NAME TYPE DATA AGE
secret/backstage-env-secrets Opaque 11 2d4h
secret/backstage-postgres-postgresql Opaque 2 2d19h
# backstage-sa-token foi gerado no lab
secret/backstage-sa-token kubernetes.io/service-account-token 3 2d19h
secret/sh.helm.release.v1.backstage-postgres.v1 helm.sh/release.v1 1 2d19h
secret/sh.helm.release.v1.backstage.v1 helm.sh/release.v1 1 6h32m
NAME DATA AGE
configmap/kube-root-ca.crt 1 2d19h
Essa variáveis de ambiente não devem ser passadas dessa forma. Só foram utilizadas desta maneira pois estamos em um ambiente controlado e o que nos importava aqui era deployar no kuberentes.
É necessário que sejam injetadas em tempos de execução utilizando um Vault ou algum outro gerenciador de segredos.
Recomendo fortemente que o postgres seja uma serviço da cloud. Estamos falando de um ambiente produtivo que não pode parar pois vai impactar todos os desenvolvedores da empresa.
Obviamente um SSO deve ser implementado utilizando o domínio da empresa.
E temos o nosso backstage funcionando em backstage.localhost