Gerenciar infraestrutura como código (IaC, Infrastructure as Code) se tornou uma prática essencial para equipes de desenvolvimento modernas, e o Pulumi surge como uma ferramenta poderosa nesse cenário, permitindo definir recursos em linguagens de programação que você já conhece - seja Python, JavaScript, TypeScript, Go ou C#.
Se você já utiliza o GitHub para controle de versão e colaboração, talvez esteja se perguntando como integrar seus recursos existentes (repositórios, organizações, membros, etc) ao fluxo de trabalho do Pulumi. A boa notícia é que o Pulumi oferece mecanismos para importar e gerenciar esses recursos, permitindo que você aproveite a infraestrutura existente sem precisar recriá-la do zero.
Nesta postagem, vamos explorar o processo de criar um repositório que nos permita gerenciar recursos do GitHub, desde repositórios individuais até estruturas organizacionais (quase) completas.
Motivação
No meu caso, a minha principal motivação é a minha bagunça. Atualmente tenho 126 repositórios pessoais na minha conta do GitHub, além de fazer parte de 14 organizações, dos quais alguns eu sou o responsável pela parte de Ops. Fazer a gestão de acessos, licenças, workflows, entre outras coisas vem se tornando uma tarefa bastante onerosa.
Além disso, no último semestre tenho trabalhado diariamente com o AWS CDK (Cloud Development Kit), uma ferramenta da AWS que permite definir infraestrutura na nuvem usando linguagens de programação como TypeScript. Comparado com os anos que trabalhei com Terraform, que utiliza HCL (HashiCorp Configuration Language), uma linguagem declarativa específica para definir infraestrutura como código, posso afirmar que tenho gostado muito mais de usar uma linguagem de programação convencional para gerir a infraestrutura do que o HCL.
Sendo assim, o Pulumi se apresentou como uma ótima opção de uso. Ele me permite reutilizar minha estrutura para projetos em TypeScript (como linter, CI/CD, etc), resolve meu problema de gestão de infraestrutura e possibilita descrever cenários mais dinâmicamente do que antes (creio que uma das principais vantagens de se usar uma linguagem de programação para isso).
Considerações
Antes de prosseguirmos, quero descrever algumas considerações para esse artigo:
This template comes with a structure for you to manage your personal
repositories or organizations on GitHub, as well as making it easy to expand to
other contexts.
Perceba que para cada conta será um diretório com um projeto em TypeScript, com seus próprios arquivos package.json e afins. No meu caso, será um projeto para meus repositórios pessoais e um projeto para cada organização. O passo a passo para preparar o projeto é o mesmo em todos esses casos.
Criando um projeto no Pulumi
Use o login local do Pulumi:
pulumi login file:$(pwd)
O comando pulumi login é usado para autenticar sua CLI do Pulumi com um backend de armazenamento de estado. Por padrão, você se conecta ao backend gerenciado na nuvem do Pulumi. Quando usamos file:$(pwd), você configura o Pulumi para usar um backend de arquivo local em vez do backend na nuvem.
Inicie um projeto no Pulumi:
pulumi new typescript --force
Precisamos usar o --force porque o comando login irá criar um diretório home e o comando new exige ser executado em um diretório vazio.
Como terei inúmeros recursos, não é interessante ter que usar um esquema de importação convencional, pois implicaria em sempre ter que importar um novo repositório quando criado, além dos mais de 120 repositórios que já tenho. Ao invés disso, optei por importar dinâmicamente os recursos.
O objetivo é ter diretórios para cada tipo de recurso e uma forma dinâmica de importarmos esses recursos.
Crie um arquivo registry.ts com o seguinte conteúdo:
import*asfsfrom"fs";import*aspathfrom"path";import*aspulumifrom"@pulumi/pulumi";exportinterfaceRegistryBaseConstructor{new ():RegistryBase;}exportclassRegistryBase{constructor(suffix:string,directory:string){this.init(suffix,directory).then((ResourceRegistry)=>{ResourceRegistry.forEach((ResourceClass)=>{newResourceClass();});}).catch((error)=>{pulumi.log.error(`Critical error in init method: ${error}`);throwerror;});}protectedasyncinit(suffix:string,directory:string){constanyResourceRegistry=newMap<string,RegistryBaseConstructor>();constclassesDir=path.join(directory,"./");constfiles=fs.readdirSync(classesDir);awaitPromise.all(files.map(async (file)=>{if (file.endsWith(".ts")&&file!=="index.ts"){constclassName=file.replace(".ts","")+suffix;constmodulePath=path.join(classesDir,file);try{constmodule=awaitimport(modulePath);constClass=module[className];if (Class){anyResourceRegistry.set(className,Class);}}catch (error){console.error(`Failed to register ${className} class:`,error);}}}),);returnanyResourceRegistry;}}
A classe RegistryBase será nossa classe base para as classes que conterão cada tipo de recurso. A função dela é ser um registry de recursos. A busca por esses recursos será dinâmica, atendendo dois critérios:
Os recursos precisam ter um sufixo previamente definido.
Os recursos precisam estar no mesmo diretório que o registry.
Crie o diretório repositories:
mkdir repositories
Crie o arquivo repositories/index.ts com o seguinte conteúdo:
Nós queremos que os recursos sejam os mais fidedignos do seu estado atual, pois caso não sejam isso pode acarretar em uma atualização não desejada de estado durante a importação ou o uso. Para isso podemos usar a API do GitHub para coletar os dados de cada recurso e declara-los no Pulumi.
Processo manual
Aqui você deverá criar um script para coletar os dados da API do GitHub e atribuí-los aos recursos do Pulumi na linguagem de programação escolhida.
Para repositórios:
Temos duas formas de listar os repositórios:
Para seus repositórios pessoais: GET /user/repos?affiliation=owner.
Para os repositórios de uma organização: GET /orgs/<org>/repos.
Itere sobre cada repositório e use os seguintes endpoints para coletar dados mais detalhados:
GET /repos/<user_or_org>/<repository_name>: para dados gerais sobre o repositório.
GET /repos/<repository_name>/pages: para dados sobre as páginas do repositório.
Para membros de uma organização:
GET /orgs/<org>/members para listar os membros da organização.
Itere sobre cada membro e faça uma chamada para o endpoint GET /orgs/<org>/memberships/<member> para coletar dados de associação daquele usuário com a organização.
Para times de uma organização:
GET /orgs/<org>/teams para listar os times da organização.
Itere sobre cada time e use os seguintes endpoints para coletar dados mais detalhados:
GET /orgs/<org>/teams/<team> para dados gerais do time.
GET /orgs/<org>/teams/<team>/repos para listar os repositórios do time.
GET /orgs/<org>/teams/<team>/members para listar os membros daquele time. Itere sobre cada membro do time:
GET /orgs/<org>/teams/<team>/memberships/<member>
Agora que você tem todos os dados necessários, basta atribuí-los para os respectivos recursos no Pulumi:
Para abstrair todo esse passo a passo de coleta de dados do GitHub e montagem dos recursos do Pulumi que vimos anteriormente, criei o seguinte repositório:
Scripts to import resources from organizations and personal projects on GitHub into Pulumi.
Import from GitHub to Pulumi
This repository contains a configured Docker image with scripts to import your personal
repositories or repositories, members and teams of an organization into Pulumi.
After running the import script, two sets of artifacts are generated
and saved in the resources/:
import_*.txt: files with commands for importing resources.
members/*.ts, repositories/*.ts and teams/*.ts: Pulumi resource files.
Nesse repositório, você terá uma imagem Docker configurada e com scripts para gerar os arquivos dos recursos para o Pulumi com base nos dados retornados pela a API do GitHub.
Para esse repositório funcionar, você deve ter o Docker e o make instalado na sua máquina. Se você usa Linux, provavelmente já tem o make instalado.
⚠️ Caso você esteja importando contas e organizações em sequência, após cada importação você pode usar o comando make clear para limpar os logs e arquivos gerados durante a importação anterior.
Execute o comando de gerar os arquivos para o Pulumi:
make import-my-repos
No diretório resources terá os seguintes arquivos:
- import_*.txt: arquivo com os comandos de importação.
- repositories/*.ts: arquivos com os recursos para cada repositório.
Gerando os arquivos de repositórios, membros e times de uma organização
Exporte o token de acesso ao GitHub:
export GITHUB_ACCESS_TOKEN=github_pat_...
Exporte o nome da organização no GitHub:
export GITHUB_ORG=...
Execute o comando de gerar os arquivos para o Pulumi:
make import-org
No diretório resources terá os seguintes arquivos:
- import_*.txt: arquivo com os comandos de importação.
- memberships/*.ts: arquivos para cada membro da organização.
- repositories/*.ts: arquivos para cada repositório da organização.
- teams/*.ts: arquivos para cada time da organização.
Importando os recursos
Caso você tenha coletado por conta própria os dados, então também deve montar os comandos de importação, sendo eles:
# Repositório
pulumi import github:index/repository:Repository <resource_name> <repository>
# Branch do repositório
pulumi import github:index/branch:Branch <resource_name> <repositorio>:<branch>
# Membro de organização
pulumi import github:index/membership:Membership <resource_name> <org>:<member>
# Time de organização
pulumi import github:index/team:Team <resource_name> <team_id>
# Membros do time
pulumi import github:index/teamMembers:TeamMembers <resource_name> <team_id>
# Repositório do time
pulumi import github:index/teamRepository:TeamRepository <resource_name> <team_id>:<repository>
Caso você tenha optado por usar o repositório que sugeri anteriormente, o passo a passo é:
Copie os arquivos *.ts para os seus respectivos diretórios do projeto do Pulumi.
Execute todos os comandos nos arquivos import_*.txt.
Agora execute pulumi up e aplique as atualizações, caso haja.
Pronto, a partir de agora você terá um projeto que possibilita gerir seus recursos do GitHub usando o Pulumi. Lembre-se que essa estrutura pode ser expandida para outros tipos de recursos e que você está usando uma linguagem de programação para isso, então automações com scripts funcionam muito bem nesse contexto.
Dicas e sugestões
Perceba que usamos o login local do Pulumi, isso é apenas para facilitar a didática. Em um ambiente real, é preferível que você salve os estados no S3 ou serviço semelhante.
A indentação dos códigos pode não está adequadamente alinhados, mas você pode usar um formatter/linter para consertar isso. No meu caso, uso o do Deno (deno fmt).
Casos de uso
Existem vários casos de uso e isso depende do seu contexto ou do contexto da organização. Aqui irei listar apenas alguns casos de uso que são principalmente uteis para contas pessoais.
Atualização de arquivos especificos
Com o recurso RepositoryFile você consegue gerenciar arquivos especificos nos seus repositórios. Exemplos de arquivos interessantes para usar esse recurso são LICENSE e também workflows. Este último tipo de arquivo (workflows) é bastante útil para contas pessoais, pois o GitHub permite compartilhar actions e workflows em organizações, porém não em contas pessoais.
Atualização de repositórios para ocasiões especiais
Eu possuo alguns repositórios que participam do Hacktoberfest. Portanto pretendo colocar uma verificação de data para que todo início de outubro adicione o tópico hacktoberfest no repositório e em todo início de novembro remova esse tópico.
Gestão de compartilhamentos e acessos
Agora ficou mais fácil definir quais contas fazem parte da sua organização, seus privilégios, quais times tem acessos a quais repositórios, etc.
Gestão de variáveis e segredos
Se você usa o GitHub Actions, deve estar familiarizado com o uso de variáveis e segredos nos seus workflows. Uma boa prática é colocar um tempo de expiração curto em segredos e renova-los sempre que possível. O Pulumi permite que você armazene segredos na stack, o comando de inserir o token do GitHub no projeto é um exemplo disso, portando você pode integrar essa funcionalidade com um script de renovação de token. Toda vez que o script é executado, ele automaticamente atualiza o token na stack e atualiza o segredo no repositório.