🇺🇸 To read in English: How to create a GitHub resource management repository with Pulumi.

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:

  • Você precisa ter o Pulumi instalado na sua máquina: Download & install Pulumi.
  • A linguagem de programação que irei utilizar será TypeScript.
  • Todo o passo a passo será feito em um diretório inicialmente vazio.

Preparando o projeto

Caso não queira fazer o passo a passo dessa seção, você pode usar o meu template:

GitHub logo alvarofpp / template-infra-pulumi

Template for IaC projects using Pulumi.

Template for IaC using Pulumi

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.

Migrating GitHub resources to Pulumi

You can use the alvarofpp/import-from-github-to-pulumi repository to perform the migration of an organization or your personal repositories to Pulumi.




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

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

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

  3. Instale o provedor do GitHub:

    npm install @pulumi/github
    
  4. Adicione o token do GitHub:

    pulumi config set github:token  --secret
    

    Caso não saiba como criar um token de acesso para a API do GitHub, consulte a documentação oficial: Gerenciar seus tokens de acesso pessoal.

  5. Adicione o owner (pode ser um usuário ou uma organização):

    pulumi config set github:owner 
    

Até aqui o seu diretório deve estar próximo disso:

.
├── Pulumi.main.yaml
├── Pulumi.yaml
├── index.ts
├── package-lock.json
├── package.json
└── tsconfig.json

Tornando o projeto mais dinâmico

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.

  1. Crie um arquivo registry.ts com o seguinte conteúdo:

    import * as fs from "fs";
    import * as path from "path";
    
    import * as pulumi from "@pulumi/pulumi";
    
    export interface RegistryBaseConstructor {
      new (): RegistryBase;
    }
    
    export class RegistryBase {
      constructor(suffix: string, directory: string) {
        this.init(suffix, directory)
          .then((ResourceRegistry) => {
            ResourceRegistry.forEach((ResourceClass) => {
              new ResourceClass();
            });
          })
          .catch((error) => {
            pulumi.log.error(`Critical error in init method: ${error}`);
            throw error;
          });
      }
    
      protected async init(suffix: string, directory: string) {
        const anyResourceRegistry = new Map<string, RegistryBaseConstructor>();
        const classesDir = path.join(directory, "./");
        const files = fs.readdirSync(classesDir);
    
        await Promise.all(
          files.map(async (file) => {
            if (file.endsWith(".ts") && file !== "index.ts") {
              const className = file.replace(".ts", "") + suffix;
              const modulePath = path.join(classesDir, file);
    
              try {
                const module = await import(modulePath);
                const Class = module[className];
                if (Class) {
                  anyResourceRegistry.set(className, Class);
                }
              } catch (error) {
                console.error(`Failed to register ${className} class:`, error);
              }
            }
          }),
        );
    
        return anyResourceRegistry;
      }
    }
    

    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.
  2. Crie o diretório repositories:

    mkdir repositories
    
  3. Crie o arquivo repositories/index.ts com o seguinte conteúdo:

    import { RegistryBase } from "../registry";
    
    export class RepositoriesRegistry extends RegistryBase {
      constructor() {
        super("Repository", __dirname);
      }
    }
    

    A classe RepositoriesRegistry irá conter todas as classes que possuem Repository como sufixo e estejam dentro de repositories/.

  4. No arquivo index.ts, importe o RepositoriesRegistry:

    import { RepositoriesRegistry } from "./repositories";
    
    const repositories = new RepositoriesRegistry();
    

❗ Para o caso de organizações, você deve repetir os passos 2 a 4 para memberships (sufixo Membership) e teams (sufixo Team).

Até aqui o seu diretório deve estar parecido com isso:

.
├── Pulumi.main.yaml
├── Pulumi.yaml
├── index.ts
├── memberships
│   └── index.ts
├── package-lock.json
├── package.json
├── repositories
│   └── index.ts
├── teams
│   └── index.ts
└── tsconfig.json

Coletando dados e gerando os recursos no Pulumi

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//repos.
  • Itere sobre cada repositório e use os seguintes endpoints para coletar dados mais detalhados:
    • GET /repos//: para dados gerais sobre o repositório.
    • GET /repos//pages: para dados sobre as páginas do repositório.

Para membros de uma organização:

  • GET /orgs//members para listar os membros da organização.
  • Itere sobre cada membro e faça uma chamada para o endpoint GET /orgs//memberships/ para coletar dados de associação daquele usuário com a organização.

Para times de uma organização:

  • GET /orgs//teams para listar os times da organização.
  • Itere sobre cada time e use os seguintes endpoints para coletar dados mais detalhados:
    • GET /orgs//teams/ para dados gerais do time.
    • GET /orgs//teams//repos para listar os repositórios do time.
    • GET /orgs//teams//members para listar os membros daquele time. Itere sobre cada membro do time:
      • GET /orgs//teams//memberships/

Agora que você tem todos os dados necessários, basta atribuí-los para os respectivos recursos no Pulumi:

Importando usando repositório

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:

GitHub logo alvarofpp / import-from-github-to-pulumi

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.

The alvarofpp/template-infra-pulumi repository has a directory structure that you can use as a basis.

How to use

This repository can be used for two purposes:

  • Importing your personal repositories.
  • Importing members of an organization's repositories and teams.

For both cases, you first need to run make build to generate the Docker image that will be used to run the scripts.

The commands using make that will be shown below can be found in the Makefile file If you choose not to use the Makefile, you can…

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.

Em um outro diretório, clone o repositório:

git clone [email protected]:alvarofpp/import-from-github-to-pulumi.git

Gerando os arquivos de repositórios pessoais

  1. Exporte o token de acesso ao GitHub:

    export GITHUB_ACCESS_TOKEN=github_pat_...
    
  2. Exporte o seu usuário no GitHub:

    export GITHUB_USER=...
    
  3. 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

  1. Exporte o token de acesso ao GitHub:

    export GITHUB_ACCESS_TOKEN=github_pat_...
    
  2. Exporte o nome da organização no GitHub:

    export GITHUB_ORG=...
    
  3. 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  

# Branch do repositório
pulumi import github:index/branch:Branch  :

# Membro de organização
pulumi import github:index/membership:Membership  :

# Time de organização
pulumi import github:index/team:Team  

# Membros do time
pulumi import github:index/teamMembers:TeamMembers  

# Repositório do time
pulumi import github:index/teamRepository:TeamRepository  :

Caso você tenha optado por usar o repositório que sugeri anteriormente, o passo a passo é:

  1. Copie os arquivos *.ts para os seus respectivos diretórios do projeto do Pulumi.
  2. Execute todos os comandos nos arquivos import_*.txt.
  3. 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.