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

1. Введение

Обзор

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

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

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

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

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

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

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

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

cb81e7c8e34bc8d.png

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

d95252b003979716.png

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

7833d5e1c5d18f54.png

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

После подключения к 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. Напишите заявку

Сначала создайте каталог для исходного кода и перейдите в этот каталог.

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 для внешнего интерфейса, который будет использовать 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. Запустите приложение локально

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

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

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

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

Однако если вы работаете на локальном терминале (т. е. не в 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

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

Остановите локальный запуск экспресс-приложения (например, 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 Чтобы создать пустой репозиторий, вы оставите все настройки по умолчанию неотмеченными или не установите их в значение «Нет», чтобы при создании в репозитории по умолчанию не было контента, например

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

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

Пустые инструкции по репозиторию GitHub

Вы будете следить за отправкой существующего репозитория из инструкций командной строки, выполнив следующие команды:

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

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

затем переместите основную ветку в восходящий репозиторий.

git push -u origin main

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

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

  • Нажмите Создать услугу.
  • Нажмите «Непрерывно развертывать из репозитория».
  • Нажмите НАСТРОЙКА CLOUD BUILD .
  • В репозитории исходного кода
    • Выберите GitHub в качестве поставщика репозитория.
    • Нажмите «Управление подключенными репозиториями» , чтобы настроить доступ Cloud Build к репозиторию.
    • Выберите свой репозиторий и нажмите «Далее».
  • В конфигурации сборки
    • Оставьте ветку как ^main$
    • В качестве типа сборки выберите Go, Node.js, Python, Java, .NET Core, Ruby или PHP через пакеты сборки 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 Run, чтобы отслеживать изменения в развертывании.

Проверка исправления в рабочей среде

Как только облачная консоль для вашей службы Cloud Run покажет, что вторая редакция теперь обслуживает 100 % трафика, например https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions, вы можете открыть URL-адрес службы Cloud Run в вашем браузере и убедитесь, что вновь введенные данные о городе сохраняются после обновления страницы.

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

Поздравляем с завершением работы над кодом!

Рекомендуем просмотреть документацию по Cloud Run и непрерывному развертыванию из git .

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

  • Напишите веб-приложение Express с помощью редактора Cloud Shell.
  • Подключите свою учетную запись GitHub к Google Cloud для непрерывного развертывания.
  • Автоматически развертывайте свое приложение в Cloud Run
  • Узнайте, как использовать HTMX и 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 -сервис cloud-run-auto-deploy-codelab .

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