Como implantar automaticamente as alterações do GitHub no Cloud Run usando o Cloud Build

1. Introdução

Visão geral

Neste codelab, você vai configurar o Cloud Run para criar e implantar automaticamente novas versões do aplicativo sempre que enviar as mudanças do código-fonte para um repositório do GitHub.

Este aplicativo de demonstração salva dados do usuário no Firestore, mas apenas uma parte deles é salva corretamente. Você vai configurar implantações contínuas para que, ao enviar uma correção de bug para seu repositório do GitHub, ela fique disponível automaticamente em uma nova revisão.

O que você vai aprender

  • Escrever um aplicativo da Web Express com o editor do Cloud Shell
  • Conecte sua conta do GitHub ao Google Cloud para implantações contínuas
  • Implantar automaticamente seu aplicativo no Cloud Run
  • Saiba como usar HTMX e TailwindCSS

2. Configuração e requisitos

Pré-requisitos

Ativar o Cloud Shell

  1. No Console do Cloud, clique em Ativar o Cloud Shelld1264ca30785e435.png.

cb81e7c8e34bc8d.png

Se esta for a primeira vez que você inicia o Cloud Shell, uma tela intermediária vai aparecer com a descrição dele. Se isso acontecer, clique em Continuar.

d95252b003979716.png

Leva apenas alguns instantes para provisionar e se conectar ao Cloud Shell.

7833d5e1c5d18f54.png

Essa máquina virtual contém todas as ferramentas de desenvolvimento necessárias. Ela oferece um diretório principal persistente de 5 GB, além de ser executada no Google Cloud. Isso aprimora o desempenho e a autenticação da rede. Neste codelab, quase todo o trabalho pode ser feito com um navegador.

Depois de se conectar ao Cloud Shell, você vai ver que sua conta já está autenticada e que o projeto está configurado com o ID do seu projeto.

  1. Execute o seguinte comando no Cloud Shell para confirmar se a conta está autenticada:
gcloud auth list

Resposta ao comando

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Execute o comando a seguir no Cloud Shell para confirmar se o comando gcloud sabe sobre seu projeto:
gcloud config list project

Resposta ao comando

[core]
project = <PROJECT_ID>

Se o projeto não estiver configurado, configure-o usando este comando:

gcloud config set project <PROJECT_ID>

Resposta ao comando

Updated property [core/project].

3. Ativar APIs e definir variáveis de ambiente

Ativar APIs

Este codelab exige o uso das seguintes APIs. Para ativar essas APIs, execute o seguinte comando:

gcloud services enable run.googleapis.com \
    cloudbuild.googleapis.com \
    firestore.googleapis.com \
    iamcredentials.googleapis.com

Configurar as variáveis de ambiente.

Você pode definir variáveis de ambiente que serão usadas ao longo deste codelab.

REGION=<YOUR-REGION>
PROJECT_ID=<YOUR-PROJECT-ID>
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
SERVICE_ACCOUNT="firestore-accessor"
SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com

4. Criar uma conta de serviço

Essa conta de serviço será usada pelo Cloud Run para chamar a API Gemini da Vertex AI. Essa conta de serviço também terá permissões para ler e gravar no Firestore e ler secrets do Secret Manager.

Primeiro, crie a conta de serviço executando este comando:

gcloud iam service-accounts create $SERVICE_ACCOUNT \
  --display-name="Cloud Run access to Firestore"

Agora, conceda à conta de serviço acesso de leitura e gravação ao Firestore.

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
  --role=roles/datastore.user

5. Criar e configurar um projeto do Firebase

  1. No console do Firebase, clique em Adicionar projeto.
  2. Insira <YOUR_PROJECT_ID> para adicionar o Firebase a um dos seus projetos do Google Cloud
  3. Se solicitado, leia e aceite os Termos do Firebase.
  4. Clique em Continuar.
  5. Clique em Confirmar plano para o plano de faturamento do Firebase.
  6. É opcional ativar o Google Analytics para este codelab.
  7. Clique em Adicionar Firebase:
  8. Quando o projeto for criado, clique em Continuar.
  9. No menu Build, clique em Banco de dados do Firestore.
  10. Clique em Criar banco de dados.
  11. Escolha sua região no menu suspenso Local e clique em Próxima.
  12. Use a opção padrão Iniciar no modo de produção e clique em Criar.

6. Escrever o aplicativo

Primeiro, crie um diretório para o código-fonte e use cd para acessar esse diretório.

mkdir cloud-run-github-cd-demo && cd $_

Em seguida, crie um arquivo package.json com o seguinte conteúdo:

{
  "name": "cloud-run-github-cd-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node app.js",
    "nodemon": "nodemon app.js",
    "tailwind-dev": "npx tailwindcss -i ./input.css -o ./public/output.css --watch",
    "tailwind": "npx tailwindcss -i ./input.css -o ./public/output.css",
    "dev": "npm run tailwind && npm run nodemon"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@google-cloud/firestore": "^7.3.1",
    "axios": "^1.6.7",
    "express": "^4.18.2",
    "htmx.org": "^1.9.10"
  },
  "devDependencies": {
    "nodemon": "^3.1.0",
    "tailwindcss": "^3.4.1"
  }
}

Primeiro, crie um arquivo de origem app.js com o conteúdo abaixo. Esse arquivo contém o ponto de entrada do serviço e a lógica principal do app.

const express = require("express");
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
const path = require("path");
const { get } = require("axios");

const { Firestore } = require("@google-cloud/firestore");
const firestoreDb = new Firestore();

const fs = require("fs");
const util = require("util");
const { spinnerSvg } = require("./spinnerSvg.js");

const service = process.env.K_SERVICE;
const revision = process.env.K_REVISION;

app.use(express.static("public"));

app.get("/edit", async (req, res) => {
    res.send(`<form hx-post="/update" hx-target="this" hx-swap="outerHTML">
                <div>
  <p>
    <label>Name</label>    
    <input class="border-2" type="text" name="name" value="Cloud">
    </p><p>
    <label>Town</label>    
    <input class="border-2" type="text" name="town" value="Nibelheim">
    </p>
  </div>
  <div class="flex items-center mr-[10px] mt-[10px]">
  <button class="btn bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium mr-[10px]">Submit</button>
  <button class="btn bg-gray-200 text-gray-800 px-4 py-2 rounded-lg text-center text-sm font-medium mr-[10px]" hx-get="cancel">Cancel</button>  
                ${spinnerSvg} 
                </div>
  </form>`);
});

app.post("/update", async function (req, res) {
    let name = req.body.name;
    let town = req.body.town;
    const doc = firestoreDb.doc(`demo/${name}`);

    //TODO: fix this bug
    await doc.set({
        name: name
        /* town: town */
    });

    res.send(`<div hx-target="this" hx-swap="outerHTML" hx-indicator="spinner">
                <p>
                <div><label>Name</label>: ${name}</div>
                </p><p>
                <div><label>Town</label>: ${town}</div>
                </p>
                <button
                    hx-get="/edit"
                    class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]"
                >
                    Click to update
                </button>               
            </div>`);
});

app.get("/cancel", (req, res) => {
    res.send(`<div hx-target="this" hx-swap="outerHTML">
                <p>
                <div><label>Name</label>: Cloud</div>
                </p><p>
                <div><label>Town</label>: Nibelheim</div>
                </p>
                <div>
                <button
                    hx-get="/edit"
                    class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]"
                >
                    Click to update
                </button>                
                </div>
            </div>`);
});

const port = parseInt(process.env.PORT) || 8080;
app.listen(port, async () => {
    console.log(`booth demo: listening on port ${port}`);

    //serviceMetadata = helper();
});

app.get("/helper", async (req, res) => {
    let region = "";
    let projectId = "";
    let div = "";

    try {
        // Fetch the token to make a GCF to GCF call
        const response1 = await get(
            "http://metadata.google.internal/computeMetadata/v1/project/project-id",
            {
                headers: {
                    "Metadata-Flavor": "Google"
                }
            }
        );

        // Fetch the token to make a GCF to GCF call
        const response2 = await get(
            "http://metadata.google.internal/computeMetadata/v1/instance/region",
            {
                headers: {
                    "Metadata-Flavor": "Google"
                }
            }
        );

        projectId = response1.data;
        let regionFull = response2.data;
        const index = regionFull.lastIndexOf("/");
        region = regionFull.substring(index + 1);

        div = `
        <div>
        This created the revision <code>${revision}</code> of the 
        Cloud Run service <code>${service}</code> in <code>${region}</code>
        for project <code>${projectId}</code>.
        </div>`;
    } catch (ex) {
        // running locally
        div = `<div> This is running locally.</div>`;
    }

    res.send(div);
});

Crie um arquivo chamado spinnerSvg.js.

module.exports.spinnerSvg = `<svg id="spinner" alt="Loading..."
                    class="htmx-indicator animate-spin -ml-1 mr-3 h-5 w-5 text-blue-500"
                    xmlns="http://www.w3.org/2000/svg"
                    fill="none"
                    viewBox="0 0 24 24"
                >
                    <circle
                        class="opacity-25"
                        cx="12"
                        cy="12"
                        r="10"
                        stroke="currentColor"
                        stroke-width="4"
                    ></circle>
                    <path
                        class="opacity-75"
                        fill="currentColor"
                        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
                    ></path>
                </svg>`;

Criar um arquivo input.css para o tailwindCSS

@tailwind base;
@tailwind components;
@tailwind utilities;

E crie o arquivo tailwind.config.js para o tailwindCSS

/** @type {import('tailwindcss').Config} */
module.exports = {
    content: ["./**/*.{html,js}"],
    theme: {
        extend: {}
    },
    plugins: []
};

e crie um arquivo .gitignore.

node_modules/

npm-debug.log
coverage/

package-lock.json

.DS_Store

Agora, crie um diretório public.

mkdir public
cd public

Dentro desse diretório público, crie o arquivo index.html para o front-end, que vai usar o htmx.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0"
        />
        <script
            src="https://unpkg.com/htmx.org@1.9.10"
            integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
            crossorigin="anonymous"
        ></script>

        <link href="./output.css" rel="stylesheet" />
        <title>Demo 1</title>
    </head>
    <body
        class="font-sans bg-body-image bg-cover bg-center leading-relaxed"
    >
        <div class="container max-w-[700px] mt-[50px] ml-auto mr-auto">
            <div class="hero flex items-center">                    
                <div class="message text-base text-center mb-[24px]">
                    <h1 class="text-2xl font-bold mb-[10px]">
                        It's running!
                    </h1>
                    <div class="congrats text-base font-normal">
                        Congratulations, you successfully deployed your
                        service to Cloud Run. 
                    </div>
                </div>
            </div>

            <div class="details mb-[20px]">
                <p>
                    <div hx-trigger="load" hx-get="/helper" hx-swap="innerHTML" hx-target="this">Hello</div>                   
                </p>
            </div>

            <p
                class="callout text-sm text-blue-700 font-bold pt-4 pr-6 pb-4 pl-10 leading-tight"
            >
                You can deploy any container to Cloud Run that listens for
                HTTP requests on the port defined by the
                <code>PORT</code> environment variable. Cloud Run will
                scale automatically based on requests and you never have to
                worry about infrastructure.
            </p>

            <h1 class="text-2xl font-bold mt-[40px] mb-[20px]">
                Persistent Storage Example using Firestore
            </h1>
            <div hx-target="this" hx-swap="outerHTML">
                <p>
                <div><label>Name</label>: Cloud</div>
                </p><p>
                <div><label>Town</label>: Nibelheim</div>
                </p>
                <div>
                <button
                    hx-get="/edit"
                    class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]"
                >
                    Click to update
                </button>                
                </div>
            </div>

            <h1 class="text-2xl font-bold mt-[40px] mb-[20px]">
                What's next
            </h1>
            <p class="next text-base mt-4 mb-[20px]">
                You can build this demo yourself!
            </p>
            <p class="cta">
                <button
                    class="bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium"
                >
                    VIEW CODELAB
                </button>
            </p> 
        </div>
   </body>
</html>

7. Executar o aplicativo no local

Nesta seção, você vai executar o aplicativo localmente para confirmar que há um bug quando o usuário tenta salvar dados.

Primeiro, você precisa ter a função de usuário do Datastore para acessar o Firestore (se estiver usando sua identidade para autenticação, por exemplo, se estiver executando no Cloud Shell) ou pode representar a conta de usuário criada anteriormente.

Como usar o ADC ao executar localmente

Se você estiver executando no Cloud Shell, já estará em uma máquina virtual do Google Compute Engine. Suas credenciais associadas a essa máquina virtual (conforme mostrado ao executar gcloud auth list) serão usadas automaticamente pelas Application Default Credentials (ADC). Portanto, não é necessário usar o comando gcloud auth application-default login. No entanto, sua identidade ainda precisará da função de usuário do Datastore. Você pode pular para a seção Executar o app localmente.

No entanto, se você estiver executando no terminal local (ou seja, não no Cloud Shell), precisará usar as Application Default Credentials para se autenticar nas APIs do Google. Você pode 1) fazer login usando suas credenciais (desde que tenha o papel de usuário do Datastore) ou 2) fazer login personificando a conta de serviço usada neste codelab.

Opção 1: usar suas credenciais para o ADC

Se quiser usar suas credenciais, primeiro execute gcloud auth list para verificar como você está autenticado na gcloud. Em seguida, talvez seja necessário conceder à sua identidade a função de usuário da Vertex AI. Se sua identidade tiver a função de proprietário, você já terá a função de usuário do Datastore. Caso contrário, execute este comando para conceder à sua identidade o papel de usuário da Vertex AI e o papel de usuário do Datastore.

USER=<YOUR_PRINCIPAL_EMAIL>

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member user:$USER \
  --role=roles/datastore.user

Em seguida, execute o comando a seguir

gcloud auth application-default login

Opção 2: representar uma conta de serviço para ADC

Se você quiser usar a conta de serviço criada neste codelab, sua conta de usuário precisará ter o papel de criador de token da conta de serviço. Para receber essa função, execute o seguinte comando:

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member user:$USER \
  --role=roles/iam.serviceAccountTokenCreator

Em seguida, execute o comando a seguir para usar o ADC com a conta de serviço

gcloud auth application-default login --impersonate-service-account=$SERVICE_ACCOUNT_ADDRESS

Executar o aplicativo localmente

Em seguida, verifique se você está no diretório raiz cloud-run-github-cd-demo do codelab.

cd .. && pwd

Agora, instale as dependências.

npm install

Por fim, inicie o app executando o script a seguir. Esse script também vai gerar o arquivo output.css do tailwindCSS.

npm run dev

Agora, abra o navegador da Web em http://localhost:8080. Se você estiver no Cloud Shell, abra o site clicando no botão "Visualização da Web" e selecionando "Visualizar porta 8080".

Botão &quot;Visualizar na porta 8080&quot; da visualização na Web

Digite o texto nos campos de entrada de nome e cidade e clique em "Salvar". Depois, atualize a página. Você vai perceber que o campo "cidade" não foi mantido. Você vai corrigir esse bug na próxima seção.

Interrompa a execução do app Express localmente (por exemplo, Ctrl^c no macOS).

8. Criar um repositório do GitHub

No diretório local, crie um novo repositório com "main" como o nome da ramificação padrão.

git init
git branch -M main

Faça commit da base de código atual que contém o bug. Você vai corrigir o bug depois que a implantação contínua for configurada.

git add .
git commit -m "first commit for express application"

Acesse o GitHub e crie um repositório vazio que seja privado ou público. Este codelab recomenda nomear seu repositório como cloud-run-auto-deploy-codelab. Para criar um repositório vazio, deixe todas as configurações padrão desmarcadas ou definidas como "Nenhum" para que nenhum conteúdo esteja no repositório por padrão quando ele for criado, por exemplo:

Configurações padrão do GitHub

Se você concluiu esta etapa corretamente, as seguintes instruções vão aparecer na página do repositório vazio:

Instruções para repositório vazio do GitHub

Siga as instruções para enviar um repositório atual da linha de comando executando os seguintes comandos:

Primeiro, adicione o repositório remoto executando

git remote add origin <YOUR-REPO-URL-PER-GITHUB-INSTRUCTIONS>

e envie a ramificação principal para o repositório upstream.

git push -u origin main

9. Configurar a implantação contínua

Agora que você tem código no GitHub, é possível configurar a implantação contínua. Acesse o Console do Cloud para o Cloud Run.

  • Clique em "Criar um serviço".
  • Clique em Implantar continuamente a partir de um repositório.
  • Clique em CONFIGURAR O CLOUD BUILD.
  • Em Repositório de origem
    • Selecione o GitHub como o provedor de repositório
    • Clique em Gerenciar repositórios conectados para configurar o acesso do Cloud Build ao repositório.
    • Selecione o repositório e clique em Próxima.
  • Em "Configuração do build"
    • Deixe a ramificação como ^main$
    • Em "Tipo de build", selecione Go, Node.js, Python, Java, .NET Core, Ruby ou PHP via buildpacks do Google Cloud.
  • Deixe o diretório de contexto de build como /
  • Clique em Salvar.
  • Em Autenticação
    • Clique em Permitir invocações não autenticadas.
  • Em Contêineres, volumes, rede, segurança
    • Na guia "Segurança", selecione a conta de serviço que você criou em uma etapa anterior, por exemplo, Cloud Run access to Firestore.
  • Clique em CRIAR.

Isso vai implantar o serviço do Cloud Run que contém o bug que você vai corrigir na próxima seção.

10. Corrigir o erro

Corrigir o bug no código

No editor do Cloud Shell, abra o arquivo app.js e acesse o comentário que diz //TODO: fix this bug.

mude a seguinte linha de

 //TODO: fix this bug
    await doc.set({
        name: name
    });

a

//fixed town bug
    await doc.set({
        name: name,
        town: town
    });

Verifique a correção executando

npm run start

e abra o navegador da Web. Salve os dados novamente para a cidade e atualize. Você vai notar que os dados da cidade inseridos recentemente foram mantidos corretamente na atualização.

Agora que você verificou a correção, está tudo pronto para implantá-la. Primeiro, faça o commit da correção.

git add .
git commit -m "fixed town bug"

e depois envie para o repositório upstream no GitHub.

git push origin main

O Cloud Build vai implantar as mudanças automaticamente. Acesse o console do Cloud para seu serviço do Cloud Run e monitore as mudanças de implantação.

Verificar a correção na produção

Quando o Console do Cloud para seu serviço do Cloud Run mostrar que uma segunda revisão está atendendo 100% do tráfego, por exemplo, https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions, abra o URL do serviço do Cloud Run no navegador e verifique se os dados da cidade recém-inseridos são mantidos após a atualização da página.

11. Parabéns!

Parabéns por concluir o codelab!

Recomendamos consultar a documentação do Cloud Run e da implantação contínua do Git.

O que vimos

  • Escrever um aplicativo da Web Express com o editor do Cloud Shell
  • Conecte sua conta do GitHub ao Google Cloud para implantações contínuas
  • Implantar automaticamente seu aplicativo no Cloud Run
  • Saiba como usar HTMX e TailwindCSS

12. Limpar

Para evitar cobranças acidentais, por exemplo, se os serviços do Cloud Run forem invocados mais vezes do que sua alocação mensal de invocações do Cloud Run no nível sem custo financeiro, exclua o Cloud Run ou o projeto criado na etapa 2.

Para excluir o serviço do Cloud Run, acesse o console do Cloud Run em https://console.cloud.google.com/run e exclua o serviço do Cloud Run que você criou neste codelab. Por exemplo, exclua o serviço cloud-run-auto-deploy-codelab.

Se você quiser excluir todo o projeto, acesse https://console.cloud.google.com/cloud-resource-manager, selecione o projeto criado na Etapa 2 e escolha "Excluir". Se você excluir o projeto, vai precisar mudar de projeto no SDK Cloud. Para conferir a lista de todos os projetos disponíveis, execute gcloud projects list.