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 seu aplicativo sempre que enviar as mudanças do código-fonte para um repositório do GitHub.

Esse app de demonstração salva dados do usuário no Firestore. No entanto, apenas uma parte deles é salva corretamente. Você vai configurar implantações contínuas de modo que, quando enviar uma correção de bug ao seu repositório do GitHub, veja automaticamente a correção ser disponibilizada em uma nova revisão.

O que você vai aprender

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

2. Configuração e requisitos

Pré-requisitos

  • Você tem uma conta do GitHub e sabe como criar e enviar códigos para repositórios.
  • Você fez login no console do Cloud.
  • Você já implantou um serviço do Cloud Run. Por exemplo, siga o guia de início rápido sobre a implantação de um serviço da Web pelo código-fonte.

Ativar o Cloud Shell

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

cb81e7c8e34bc8d.png

Se você estiver iniciando o Cloud Shell pela primeira vez, verá uma tela intermediária com a descrição dele. Se aparecer uma tela intermediária, clique em Continuar.

d95252b003979716.png

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

7833d5e1c5d18f54.png

Essa máquina virtual tem 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. Grande parte do trabalho neste codelab, se não todo, pode ser feito em um navegador.

Depois de se conectar ao Cloud Shell, você verá sua autenticação e o projeto estará 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 seguinte comando no Cloud Shell para confirmar que 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. Ative APIs e defina variáveis de ambiente

Ativar APIs

Este codelab requer o uso das APIs a seguir. É possível ativar essas APIs executando 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 neste 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, execute este comando para criar a conta de serviço:

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 no 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 projeto atual do Google Cloud
  3. Se solicitado, leia e aceite os Termos do Firebase.
  4. Clique em Continuar.
  5. Clique em Confirmar plano para confirmar o plano de faturamento do Firebase.
  6. A ativação do Google Analytics neste codelab é opcional.
  7. Clique em Adicionar Firebase:
  8. Quando o projeto estiver pronto, 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 depois clique em Criar.

6. Programar o aplicativo

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

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 para o 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 com o nome 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>`;

Crie um arquivo input.css para o tailwindCSS.

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

E crie o arquivo tailwind.config.js para 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 novo diretório public.

mkdir public
cd public

Dentro desse diretório público, crie o arquivo index.html para o front-end, que usará 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 nele quando o usuário tenta salvar dados.

Primeiro, você precisa ter o papel de Usuário do Datastore para acessar o Firestore (se estiver usando sua identidade para autenticação, por exemplo, ao executar 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á está em execução em uma máquina virtual do Google Compute Engine. As credenciais associadas a essa máquina virtual, conforme mostrado ao executar gcloud auth list, serão usadas automaticamente pelo Application Default Credentials (ADC). Por isso, não é necessário usar o comando gcloud auth application-default login. No entanto, sua identidade ainda precisará do papel Usuário do Datastore. Pule para a seção Executar o app localmente.

No entanto, se você estiver executando no seu terminal local (ou seja, não no Cloud Shell), será necessário usar o 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 representando a conta de serviço usada neste codelab.

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

Se você quiser usar suas credenciais, primeiro execute gcloud auth list para verificar como você está autenticado na gcloud. Em seguida, pode ser necessário conceder à sua identidade o papel de Usuário da Vertex AI. Se sua identidade tem o papel de Proprietário, você já tem esse papel 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 seguinte comando:

gcloud auth application-default login

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

Para 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. Você pode conseguir esse papel executando 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 você vai instalar as dependências.

npm install

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

npm run dev

Agora abra o navegador da Web em http://localhost:8080. Se você estiver no Cloud Shell, abra o site abrindo o botão "Visualização na Web" e selecionando "Porta de visualização 8080".

visualização da web - botão de visualização na porta 8080

Insira texto nos campos de nome e cidade e clique em "Salvar". Em seguida, atualize a página. Você notará que o campo de cidade não persistiu. Você vai corrigir esse bug na próxima seção.

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

8. Criar um repositório do GitHub

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

git init
git branch -M main

Confirme a base de código atual que contém o bug. O bug vai ser corrigido 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 particular para você ou público. Este codelab recomenda o nome cloud-run-auto-deploy-codelab para seu repositório.Para criar um repositório vazio, deixe todas as configurações padrão desmarcadas ou defina-as como nenhuma. Assim, nenhum conteúdo estará no repositório por padrão quando for criado, por exemplo,

Configurações padrão do GitHub

Se você concluiu essa etapa corretamente, verá as seguintes instruções na página do repositório vazio:

Instruções do repositório do GitHub vazia

Siga as instruções para enviar um repositório existente pela linha de comando com os seguintes comandos:

Primeiro, adicione o repositório remoto executando

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

e, em seguida, enviar por push a ramificação principal para o repositório upstream.

git push -u origin main

9. Configurar a implantação contínua

Agora que o código está em um GitHub, você pode configurar a implantação contínua. Acesse o Console do Cloud para Cloud Run.

  • Clique em Criar um serviço
  • Clique em Implantar continuamente a partir de um repositório.
  • Clique em CONFIGURAR O CLOUD BUILD.
  • No Repositório de origem
    • Selecione GitHub como o provedor do repositório
    • Clique em Gerenciar repositórios conectados para configurar o acesso do Cloud Build ao repositório
    • Selecione seu repositório e clique em Próxima
  • Em Configuração da compilação
    • Deixar ramificação como ^main$
    • Em "Tipo de build", selecione Go, Node.js, Python, Java, .NET Core, Ruby ou PHP usando os 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êiner(es), 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 implantará o serviço do Cloud Run que contém o bug que você 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 //TODO: fix this bug.

altere a linha a seguir 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 da cidade novamente e atualize. Você verá que os dados recém-inseridos sobre cidades persistiram corretamente na atualização.

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

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

e, em seguida, enviá-lo por push para o repositório upstream no GitHub.

git push origin main

O Cloud Build vai implantar as alterações automaticamente. Acesse o console do Cloud do serviço do Cloud Run para monitorar as alterações na implantação.

Verificar a correção na produção

Quando o console do Cloud do serviço do Cloud Run mostrar que uma segunda revisão está exibindo 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 continuam após a atualização da página.

11. Parabéns!

Parabéns por concluir o codelab.

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

O que vimos

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

12. Limpar

Para evitar cobranças acidentais (por exemplo, se os serviços do Cloud Run forem invocados por engano mais vezes do que sua alocação mensal de invocação 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 do Cloud 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ê optar por excluir o projeto inteiro, acesse https://console.cloud.google.com/cloud-resource-manager, selecione o projeto criado na etapa 2 e escolha "Excluir". Se você excluir o projeto, precisará alterar os projetos no SDK Cloud. Para conferir a lista de todos os projetos disponíveis, execute gcloud projects list.