Как автоматически развернуть изменения из GitHub в Cloud Run с помощью Cloud Build

1. Введение

Обзор

В этом практическом занятии вы настроите Cloud Run для автоматической сборки и развертывания новых версий вашего приложения всякий раз, когда вы отправляете изменения исходного кода в репозиторий GitHub.

Это демонстрационное приложение сохраняет пользовательские данные в Firestore, однако корректно сохраняется лишь часть данных. Вам потребуется настроить непрерывную интеграцию, чтобы при отправке исправления ошибки в ваш репозиторий GitHub это исправление автоматически становилось доступным в новой ревизии.

Что вы узнаете

  • Напишите веб-приложение на Express с помощью редактора Cloud Shell.
  • Подключите свою учетную запись GitHub к Google Cloud для непрерывного развертывания.
  • Автоматически разверните ваше приложение в Cloud Run.
  • Узнайте, как использовать HTMLX и TailwindCSS.

2. Настройка и требования

Предварительные требования

Активировать Cloud Shell

  1. В консоли Cloud нажмите «Активировать Cloud Shell» . d1264ca30785e435.png .

cb81e7c8e34bc8d.png

Если вы запускаете Cloud Shell впервые, вам будет показан промежуточный экран с описанием его возможностей. Если вы увидели промежуточный экран, нажмите «Продолжить» .

d95252b003979716.png

Подготовка и подключение к Cloud Shell займут всего несколько минут.

7833d5e1c5d18f54.png

Эта виртуальная машина оснащена всеми необходимыми инструментами разработки. Она предоставляет постоянный домашний каталог объемом 5 ГБ и работает в облаке Google, что значительно повышает производительность сети и аутентификацию. Большая часть, если не вся, ваша работа в этом практическом задании может быть выполнена с помощью браузера.

После подключения к Cloud Shell вы увидите, что прошли аутентификацию и что проект настроен на ваш идентификатор проекта.

  1. Выполните следующую команду в Cloud Shell, чтобы подтвердить свою аутентификацию:
gcloud auth list

вывод команды

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Выполните следующую команду в Cloud Shell, чтобы убедиться, что команда gcloud знает о вашем проекте:
gcloud config list project

вывод команды

[core]
project = <PROJECT_ID>

Если это не так, вы можете установить это с помощью следующей команды:

gcloud config set project <PROJECT_ID>

вывод команды

Updated property [core/project].

3. Включите API и установите переменные среды.

Включить API

Для выполнения этого практического задания требуется использование следующих API. Вы можете включить эти API, выполнив следующую команду:

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

Настройка переменных среды

Вы можете установить переменные окружения, которые будут использоваться на протяжении всего этого практического занятия.

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. Создайте учетную запись службы.

Эта учетная запись службы будет использоваться Cloud Run для вызова API Vertex AI Gemini. У этой учетной записи службы также будут разрешения на чтение и запись в Firestore, а также на чтение секретов из Secret Manager.

Сначала создайте учетную запись службы, выполнив следующую команду:

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

Теперь предоставьте учетной записи службы права на чтение и запись в Firestore.

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

5. Создайте и настройте проект Firebase.

  1. В консоли Firebase нажмите «Добавить проект» .
  2. Введите <YOUR_PROJECT_ID>, чтобы добавить Firebase в один из ваших существующих проектов Google Cloud.
  3. При появлении запроса ознакомьтесь с условиями использования Firebase и примите их.
  4. Нажмите «Продолжить» .
  5. Нажмите «Подтвердить план» , чтобы подтвердить тарифный план Firebase.
  6. Включение Google Analytics для этого практического занятия является необязательным.
  7. Нажмите «Добавить Firebase» .
  8. После создания проекта нажмите «Продолжить» .
  9. В меню «Сборка» выберите «База данных Firestore» .
  10. Нажмите «Создать базу данных» .
  11. Выберите свой регион из выпадающего списка «Местоположение» , затем нажмите «Далее» .
  12. Используйте значение по умолчанию «Запустить в производственном режиме» , затем нажмите «Создать» .

6. Напишите заявку.

Сначала создайте директорию для исходного кода и перейдите в неё с помощью команды `cd`.

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

Затем создайте файл package.json со следующим содержимым:

{
  "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"
  }
}

Сначала создайте исходный файл app.js со следующим содержимым. Этот файл содержит точку входа для сервиса и основную логику приложения.

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);
});

Создайте файл с именем 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>`;

Создайте файл input.css для tailwindCSS.

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

И создайте файл tailwind.config.js для tailwindCSS.

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

И создайте файл .gitignore .

node_modules/

npm-debug.log
coverage/

package-lock.json

.DS_Store

Теперь создайте новую public директорию.

mkdir public
cd public

Внутри этой общедоступной директории создайте файл index.html для внешнего интерфейса, который будет использовать HTML.

<!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. Запустите приложение локально.

В этом разделе вы запустите приложение локально, чтобы подтвердить наличие ошибки в приложении, возникающей при попытке пользователя сохранить данные.

Во-первых, для доступа к Firestore вам потребуется либо роль пользователя Datastore (если вы используете свою учетную запись для аутентификации, например, работаете в Cloud Shell), либо вы можете выдать себя за ранее созданную учетную запись пользователя.

Использование АЦП при локальном запуске

Если вы работаете в Cloud Shell, вы уже используете виртуальную машину Google Compute Engine. Ваши учетные данные, связанные с этой виртуальной машиной (как показано при выполнении gcloud auth list ), будут автоматически использоваться Application Default Credentials (ADC), поэтому нет необходимости использовать команду gcloud auth application-default login . Однако вашей учетной записи по-прежнему потребуется роль Datastore User. Вы можете перейти к разделу «Запуск приложения локально» .

Однако, если вы работаете на локальном терминале (то есть не в Cloud Shell), вам потребуется использовать учетные данные приложения по умолчанию для аутентификации в API Google. Вы можете либо 1) войти в систему, используя свои учетные данные (при условии, что у вас есть роль пользователя хранилища данных), либо 2) войти в систему, выдав себя за учетную запись службы, используемую в этом практическом задании.

Вариант 1) Использование ваших учетных данных для ADC

Если вы хотите использовать свои учетные данные, сначала выполните gcloud auth list , чтобы проверить, как вы прошли аутентификацию в gcloud. Затем вам может потребоваться предоставить вашей учетной записи роль пользователя Vertex AI. Если у вашей учетной записи есть роль владельца, у вас уже есть роль пользователя хранилища данных. В противном случае вы можете выполнить эту команду, чтобы предоставить вашей учетной записи роль пользователя Vertex AI и роль пользователя хранилища данных.

USER=<YOUR_PRINCIPAL_EMAIL>

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

Затем выполните следующую команду.

gcloud auth application-default login

Вариант 2) Использование служебной учетной записи для ADC

Если вы хотите использовать учетную запись службы, созданную в этом практическом задании, вашей учетной записи пользователя потребуется роль «Создатель токена учетной записи службы» . Получить эту роль можно, выполнив следующую команду:

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

Далее вам потребуется выполнить следующую команду, чтобы использовать ADC с учетной записью службы.

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

Запустите приложение локально

Далее убедитесь, что вы находитесь в корневом каталоге cloud-run-github-cd-demo для вашей практической работы.

cd .. && pwd

Теперь перейдём к установке зависимостей.

npm install

Наконец, вы можете запустить приложение, выполнив следующий скрипт. Этот скрипт также сгенерирует файл output.css из tailwindCSS.

npm run dev

Теперь откройте веб-браузер по адресу http://localhost:8080. Если вы используете Cloud Shell, вы можете открыть веб-сайт, нажав кнопку «Предварительный просмотр веб-страницы» и выбрав «Предварительный просмотр порта 8080».

Предварительный просмотр веб-страницы - предварительный просмотр на порту 8080 (кнопка)

Введите текст в поля «Имя» и «Город» и нажмите «Сохранить». Затем обновите страницу. Вы заметите, что поле «Город» не сохранилось. Вы исправите эту ошибку в следующем разделе.

Остановите локальный запуск приложения Express (например, Ctrl^c на macOS).

8. Создайте репозиторий GitHub.

В локальной директории создайте новый репозиторий, указав в качестве имени ветки по умолчанию ветку main.

git init
git branch -M main

Зафиксируйте текущий код, содержащий ошибку. Вы исправите ошибку после настройки непрерывного развертывания.

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

Перейдите на GitHub и создайте пустой репозиторий, который может быть либо приватным, либо публичным. В этом руководстве рекомендуется назвать репозиторий cloud-run-auto-deploy-codelab Чтобы создать пустой репозиторий, оставьте все настройки по умолчанию отключенными или установите значение «none», чтобы при создании репозитория в нем по умолчанию не было никакого контента, например:

Настройки GitHub по умолчанию

Если вы правильно выполнили этот шаг, на странице пустого репозитория вы увидите следующие инструкции:

Инструкции по созданию пустого репозитория GitHub

Для отправки изменений в существующий репозиторий из командной строки выполните следующие команды:

Сначала добавьте удалённый репозиторий, выполнив команду...

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

Затем отправьте основную ветку в репозиторий upstream.

git push -u origin main

9. Настройка непрерывного развертывания

Теперь, когда у вас есть код в GitHub, вы можете настроить непрерывное развертывание. Перейдите в консоль Cloud Run .

  • Нажмите «Создать службу».
  • Нажмите « Непрерывное развертывание из репозитория».
  • Нажмите «НАСТРОИТЬ СОЗДАНИЕ ОБЛАКА» .
  • В репозитории исходного кода
    • Выберите GitHub в качестве поставщика репозитория.
    • Нажмите «Управление подключенными репозиториями» , чтобы настроить доступ Cloud Build к репозиторию.
    • Выберите свой репозиторий и нажмите «Далее».
  • В разделе «Конфигурация сборки»
    • Оставить ветку как ^main$
    • В поле «Тип сборки» выберите Go, Node.js, Python, Java, .NET Core, Ruby или PHP с помощью buildpack-пакетов Google Cloud.
  • Оставьте каталог контекста сборки как /
  • Нажмите «Сохранить».
  • В процессе аутентификации
    • Нажмите «Разрешить неаутентифицированные вызовы».
  • В разделах «Контейнеры», «Тома», «Сеть», «Безопасность»
    • На вкладке «Безопасность» выберите учетную запись службы, созданную на предыдущем шаге, например, Cloud Run access to Firestore
  • Нажмите СОЗДАТЬ

Это позволит развернуть службу Cloud Run, содержащую ошибку, которую вы исправите в следующем разделе.

10. Исправить ошибку

Исправить ошибку в коде

В редакторе Cloud Shell откройте файл app.js и перейдите к комментарию с текстом //TODO: fix this bug

изменить следующую строку из

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

к

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

Проверьте правильность исправления, запустив команду.

npm run start

Откройте веб-браузер. Снова сохраните данные для города и обновите страницу. Вы увидите, что введенные данные о городе корректно сохранились после обновления.

Теперь, когда вы проверили исправление, вы готовы его развернуть. Сначала зафиксируйте исправление.

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

а затем загрузить его в основной репозиторий на GitHub.

git push origin main

Cloud Build автоматически развернет ваши изменения. Для отслеживания изменений в развертывании вы можете перейти в консоль Cloud Console для вашей службы Cloud Run.

Проверьте исправление в рабочей среде.

Как только в консоли Cloud Console для вашей службы Cloud Run отобразится информация о том, что вторая версия теперь обрабатывает 100% трафика, например, https://console.cloud.google.com/run/detail/<ВАШ_РЕГИОН>/<НАЗВАНИЕ_ВАШЕЙ_СЛУЖБЫ>/revisions, вы можете открыть URL-адрес службы Cloud Run в браузере и убедиться, что введенные данные о городе сохраняются после обновления страницы.

11. Поздравляем!

Поздравляем с завершением практического занятия!

Мы рекомендуем ознакомиться с документацией по Cloud Run и непрерывной развертыванию из Git .

Что мы рассмотрели

  • Напишите веб-приложение на Express с помощью редактора Cloud Shell.
  • Подключите свою учетную запись GitHub к Google Cloud для непрерывного развертывания.
  • Автоматически разверните ваше приложение в Cloud Run.
  • Узнайте, как использовать HTMLX и TailwindCSS.

12. Уборка

Чтобы избежать непреднамеренных списаний средств (например, если сервисы Cloud Run будут случайно запущены больше раз, чем предусмотрено вашим ежемесячным лимитом на запуск Cloud Run в бесплатном тарифе ), вы можете либо удалить Cloud Run, либо удалить проект, созданный на шаге 2.

Чтобы удалить службу Cloud Run, перейдите в консоль Cloud Run по адресу https://console.cloud.google.com/run и удалите службу Cloud Run, созданную вами в этом практическом задании, например, удалите службу cloud-run-auto-deploy-codelab .

Если вы решите удалить весь проект, перейдите по ссылке https://console.cloud.google.com/cloud-resource-manager , выберите проект, созданный на шаге 2, и нажмите «Удалить». После удаления проекта вам потребуется изменить проекты в вашем Cloud SDK. Список всех доступных проектов можно просмотреть, выполнив gcloud projects list .