Porque a vida é muito curta para redesenhar diagramas
Recentemente, entrei em uma nova empresa como engenheiro de software . Como sempre acontece, tive que começar do zero. Coisas como: Onde está o código de um aplicativo ativo? Como ele é implantado? De onde vêm as configurações? Felizmente, meus colegas fizeram um trabalho fantástico ao transformar tudo em “infraestrutura como código”. Então me peguei pensando: se tudo está no código, por que não existe uma ferramenta para ligar todos os pontos?
Essa ferramenta revisaria a base de código e construiria um diagrama de arquitetura de aplicativo, destacando os principais aspectos. Um novo engenheiro poderia olhar para o diagrama e dizer: “Ah, ok, é assim que funciona”.
Não importa o quanto eu procurei, não consegui encontrar nada parecido. As correspondências mais próximas que encontrei foram serviços que desenham um diagrama de infraestrutura. Coloquei alguns deles nesta análise para que você possa dar uma olhada mais de perto. Eventualmente, desisti de pesquisar no Google e decidi tentar desenvolver algumas coisas novas e legais.
Primeiro, criei um aplicativo Java de amostra com Gradle, Docker e Terraform. O pipeline de ações do GitHub implanta o aplicativo no Amazon Elastic Container Service. Este repositório será uma fonte para a ferramenta que irei construir (o código está aqui ).
Em segundo lugar, desenhei um diagrama de alto nível do que queria ver como resultado:
Decidi que haveria dois tipos de recursos:
Achei o termo artefato muito sobrecarregado, então escolhi Relic . Então, o que é uma relíquia? É 90% de tudo o que você deseja ver. Incluindo mas não limitado a:
Cada Relíquia tem um nome (por exemplo, my-shiny-app), tipo opcional (por exemplo, Jar) e um conjunto de pares chave → valor (por exemplo, caminho → /build/libs/my-shiny-app.jar) que descreve completamente a Relíquia. Eles são chamados de Definições . Quanto mais definições o Relic tiver, melhor.
O segundo tipo é um Source . As fontes definem, constroem ou provisionam Relíquias (por exemplo, caixas amarelas acima). Uma Fonte descreve uma Relíquia em algum lugar e dá uma ideia de onde ela vem. Embora as Fontes sejam os componentes dos quais obtemos mais informações, elas geralmente têm significados secundários no diagrama. Você provavelmente não precisa de muitas flechas indo do Terraform ou Gradle para todas as outras relíquias.
Relíquia e Fonte têm um relacionamento muitos para muitos.
Cobrir cada pedaço de código é impossível. Os aplicativos modernos podem ter muitas estruturas, ferramentas ou componentes de nuvem. Só a AWS tem cerca de 950 recursos e fontes de dados para Terraform! A ferramenta deve ser facilmente extensível e dissociada por design para que outras pessoas ou empresas possam contribuir.
Embora eu seja um grande fã da arquitetura de provedores Terraform incrivelmente conectáveis, decidi construir a mesma, embora simplificada:
O Provedor tem uma responsabilidade clara: construir Relíquias com base nos arquivos fonte solicitados. Por exemplo, GradleProvider lê arquivos *.gradle e retorna Jar , War ou Gz Relics. Cada provedor constrói relíquias dos tipos que conhece. Os provedores não se importam com as interações entre as relíquias. Eles constroem Relíquias de forma declarativa, totalmente isoladas umas das outras.
Com essa abordagem, é fácil ir tão fundo quanto você quiser. Um bom exemplo são as ações do GitHub. Um arquivo YAML de fluxo de trabalho típico consiste em dezenas de etapas usando componentes e serviços fracamente acoplados. Um fluxo de trabalho poderia construir um JAR, depois uma imagem Docker e implantá-la no ambiente. Cada etapa do fluxo de trabalho pode ser coberta pelo seu fornecedor. Portanto, os desenvolvedores de, digamos, Docker Actions criam um Provedor relacionado apenas às etapas de seu interesse.
Essa abordagem permite que qualquer número de pessoas trabalhe em paralelo, agregando mais lógica à ferramenta. Os usuários finais também podem implementar rapidamente seus provedores (no caso de alguma tecnologia proprietária). Veja mais em Personalização abaixo.
Vejamos a próxima armadilha antes de passar para a parte mais interessante. Dois Provedores, cada um criando uma Relíquia. Isso é bom. Mas e se duas destas Relíquias forem apenas representações do mesmo componente definido em dois lugares? Aqui está um exemplo.
AmazonECSProvider analisa JSON de definição de tarefa e produz uma Relíquia com o tipo AmazonECSTask . O fluxo de trabalho de ação do GitHub também tem uma etapa relacionada ao ECS, portanto, outro provedor cria uma relíquia AmazonECSTaskDeployment . Agora, temos duplicatas porque os dois provedores não sabem nada um do outro. Além disso, é incorreto que qualquer um deles presuma que outro já criou uma Relíquia. Então o que?
Não podemos eliminar nenhuma das duplicatas por causa das definições (atributos) que cada uma delas possui. A única maneira é mesclá-los. Por padrão, a próxima lógica define a decisão de mesclagem:
relic1.name() == relic2.name() && relic1.source() != relic2.source()
Mesclamos duas relíquias se seus nomes forem iguais, mas elas são definidas em fontes diferentes (como em nosso exemplo, JSON no repositório e a referência de definição de tarefa está em ações do GithHub).
Quando nos fundimos, nós:
Omiti intencionalmente um aspecto crucial de uma Relíquia. Pode ter um Matcher – e é melhor tê-lo! O Matcher é uma função booleana que pega um argumento e o testa. Matchers são peças cruciais de um processo de vinculação. Se uma Relíquia corresponder a qualquer definição da Relíquia de outra pessoa, elas serão vinculadas.
Lembra quando eu disse que os Provedores não têm ideia sobre as Relíquias criadas por outros Provedores? Isso ainda é verdade. Entretanto, um Provedor define um Matcher para uma Relíquia. Em outras palavras, representa um lado de uma seta entre duas caixas no diagrama resultante.
Exemplo. Dockerfile possui uma instrução ENTRYPOINT.
ENTRYPOINT java -jar /app/arch-diagram-sample.jar
Com alguma certeza, podemos dizer que o Docker conteineriza tudo o que é especificado no ENTRYPOINT . Portanto, o Dockerfile Relic tem uma função Matcher simples: entrypointInstruction.contains(anotherRelicsDefinition)
. Muito provavelmente, algumas Jar Relics com arch-diagram-sample.jar
nas Definições irão corresponder a ele. Se sim, uma seta entre Dockerfile e Jar Relics aparece.
Com o Matcher definido, o processo de vinculação parece bastante simples. O serviço de vinculação itera sobre todas as Relíquias e chama as funções de seu Matcher. A Relíquia A corresponde a alguma das definições da Relíquia B? Sim? Adicione uma borda entre essas relíquias no gráfico resultante. A borda também pode ser nomeada.
A última etapa é visualizar nosso gráfico final da etapa anterior. Além do PNG óbvio, a ferramenta suporta formatos adicionais, como Mermaid , Plant UML e DOT . Esses formatos de texto podem parecer menos atraentes, mas a grande vantagem é que você pode incorporar esses textos em praticamente qualquer página wiki (
Esta é a aparência do diagrama final do repositório de amostra:
A capacidade de conectar componentes personalizados ou ajustar a lógica existente é essencial, especialmente quando uma ferramenta está em fase inicial. Relíquias e fontes são flexíveis o suficiente por padrão; você pode colocar o que quiser neles. Todos os outros componentes são personalizáveis. Os provedores existentes não cobrem os recursos que você precisa? Implemente o seu próprio com facilidade. Não está satisfeito com a lógica de fusão ou ligação descrita acima? Sem problemas; adicione seu próprio LinkStrategy ou MergeStrategy . Empacote tudo em um arquivo JAR e adicione-o na inicialização. Leia mais aqui .
A geração de um diagrama com base no código-fonte provavelmente ganhará força. E a ferramenta NoReDraw em particular (sim, esse é o nome da ferramenta da qual eu estava falando). Contribuintes são bem-vindos !
O benefício mais notável (que vem do nome) é que não há necessidade de redesenhar um diagrama quando os componentes mudam. A falta de atenção da engenharia é o motivo pelo qual a documentação em geral (e os diagramas em particular) fica desatualizada. Com ferramentas como NoReDraw , isso não deve mais ser um problema, pois é facilmente conectável a qualquer pipeline de PR/CI. Lembre-se, a vida é muito curta para redesenhar diagramas 😉