Как создать API с Go, PostgreSQL, Google Cloud и CockroachDB
Возможно создать API на основе языка Go с использованием PostgreSQL. Для развертывания необходимо настроить конвейер CI/CD с использованием Google Cloud Run, Cloud Build, Secret Manager и Artifact Registry. Затем подключить экземпляр Cloud Run к базе данных CockroachDB.
API моделирует процесс слияния материалов, известный из игры Crisis Core: Final Fantasy VII.
Целевая аудитория: Эта статья предназначена для разработчиков, которые хотят узнать, как создавать и развертывать API. Более глубокий анализ этого проекта, с описанием проблем и решениями, а также объяснением правил слияния материалов.
Цель API:
API включает 3 конечные точки:
- Проверка работоспособности (GET)
- Список всех материалов (GET)
- Имитация слияния материалов (POST)
Модель Domain
Materia: это магический кристальный шар. В игре существует 144 разных материалов, которые, в общем смысле, делятся на 4 категории: «Магические», «Командные», «Вспомогательные» и «Независимые». Внутренняя категория: для удобства работы с правилами слияния, я разделил материалы на 32 внутренних категории, основываясь на их поведении при слиянии. Класс: в каждой внутренней категории существует 8 классов.
Освоение Materia: материал становится «освоенным», когда его используют в течение определенного времени (продолжительность не важна).
Слияние Materia: Два Materia могут быть объединены для получения нового материала. Правила слияния зависят от:
- Освоение Materia (оба или только один)
- Порядка Materia (X + Y ≠ Y + X).
- Внутренней категории Materia
- Класса Materia.
Существует множество исключений, а некоторые правила имеют 3 уровня вложенной логики if-else
. Это исключает возможность использования простой таблицы в базе данных для хранения более 1000 правил или использования одной формулы для управления ими.
Требования к базе данных:
- Таблица
materia
со столбцами:name(string)
,materia_type(ENUM)
,grade(integer)
(целое число),display_materia_type(ENUM)
(перечисление),description(string)
и`id(integer)
как автоматически увеличивающийся первичный ключ. - Структура данных для инкапсуляции базовых правил в формате
MateriaTypeA + MateriaTypeB = MateriaTypeC
. - Код, который использует базовые и сложные правила для определения результата слияния материалов с точки зрения их внутренней категории и класса.
Настройка локальной базы данных PostgreSQL
В идеале, вы можете установить базу данных с веб-сайта. Но по какой-то причине инструмент pgAdmin не смог подключиться к базе данных, поэтому я использовал Homebrew.
Команда для установки:
brew install postgresql@17
Это установит целый набор двоичных файлов CLI для помощи в использовании БД.
Необязательно: добавьте /opt/homebrew/opt/postgresql@17/bin
в переменную $PATH
.
# create the DB
createdb materiafusiondb
# step into the DB to perform SQL commands
psql materiafusiondb
Создайте пользователя и разрешения
-- create an SQL user to be used by the Go server
CREATE USER go_client WITH PASSWORD 'xxxxxxxx';
-- The Go server doesn't ever need to add data to the DB.
-- So let's give it just read permission.
CREATE ROLE readonly_role;
GRANT USAGE ON SCHEMA public TO readonly_role;
-- This command gives SELECT access to all future created tables.
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_role;
-- If you want to be more strict and give access only to tables that already exist, use this:
-- GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;
GRANT readonly_role TO go_client;
Создайте таблицу
CREATE TYPE display_materia_type AS ENUM ('Magic', 'Command', 'Support', 'Independent');
CREATE TYPE materia_type AS ENUM ('Fire', 'Ice', 'Lightning', 'Restore', 'Full Cure', 'Status Defense', 'Defense', 'Absorb Magic', 'Status Magic', 'Fire & Status', 'Ice & Status', 'Lightning & Status', 'Gravity', 'Ultimate', 'Quick Attack', 'Quick Attack & Status', 'Blade Arts', 'Blade Arts & Status', 'Fire Blade', 'Ice Blade', 'Lightning Blade', 'Absorb Blade', 'Item', 'Punch', 'SP Turbo', 'HP Up', 'AP Up', 'ATK Up', 'VIT Up', 'MAG Up', 'SPR Up', 'Dash', 'Dualcast', 'DMW', 'Libra', 'MP Up', 'Anything');
CREATE TABLE materia (
id integer NOT NULL,
name character varying(50) NOT NULL,
materia_type materia_type NOT NULL,
grade integer NOT NULL,
display_materia_type display_materia_type,
description text
CONSTRAINT materia_pkey PRIMARY KEY (id)
);
-- The primary key 'id' should auto-increment by 1 for every row entry.
CREATE SEQUENCE materia_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE materia_id_seq OWNED BY materia.id;
ALTER TABLE ONLY materia ALTER COLUMN id SET DEFAULT nextval('materia_id_seq'::REGCLASS);
Добавьте данные
Создайте лист Excel с заголовком таблицы и данными и экспортируйте его в CSV-файл. Затем запустите команду:
COPY materia(name,materia_type,grade,display_materia_type,description) FROM
'<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;
Создание сервера Go
Создайте шаблонный код с помощью autostrada.dev
. Добавьте опции api
, postgresql
, httprouter
, env var config
, tinted logging
, git
, live reload
, makefile
. В итоге мы получим такую файловую структуру:
📦 codebase
├─ cmd
│ └─ api
│ ├─ errors.go
│ ├─ handlers.go
│ ├─ helpers.go
│ ├─ main.go
│ ├─ middleware.go
│ └─ server.go
├─ internal
│ ├─ database --- db.go
│ ├─ env --- env.go
│ ├─ request --- json.go
│ ├─ response --- json.go
│ └─ validator
│ ├─ helpers.go
│ └─ validators.go
├─ go.mod
├─ LICENSE
├─ Makefile
├─ README.md
└─ README.html
Файл .env
Генератор шаблона создал код для извлечения переменных среды и добавления их в код, но мы можем упростить отслеживание и обновление значений.
Создайте файл <rootfolder>/.env
. Добавьте следующие значения:
HTTP_PORT=4444
DB_DSN=go_client:<password>@localhost:5432/materiafusiondb?sslmode=disable
API_TIMEOUT_SECONDS=5
API_CALLS_ALLOWED_PER_SECOND=1
Добавьте библиотеку godotenv:
go get github.com/joho/godotenv
Добавьте следующее в main.go
:
// At the beginning of main():
err := godotenv.Load(".env") // Loads environment variables from .env file
if err != nil { // This will be true in prod, but that's fine.
fmt.Println("Error loading .env file")
}
// Modify config struct:
type config struct {
baseURL string
db struct {
dsn string
}
httpPort int
apiTimeout int
apiCallsAllowedPerSecond float64
}
// Modify run() to use the new values from .env:
cfg.httpPort = env.GetInt("HTTP_PORT")
cfg.db.dsn = env.GetString("DB_DSN")
cfg.apiTimeout = env.GetInt("API_TIMEOUT_SECONDS")
cfg.apiCallsAllowedPerSecond = float64(env.GetInt("API_CALLS_ALLOWED_PER_SECOND"))
// cfg.baseURL = env.GetString("BASE_URL") - not required
Middleware и маршруты
В шаблоне уже есть middleware для восстановления после паники. Мы добавим еще 3: проверка Content-Type, ограничение скорости и защита от тайм-аута API.
Добавить библиотеку tollbooth
:
go get github.com/didip/tollbooth
Обновите <rootfolder/api/middleware.go
:
func (app *application) contentTypeCheck(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-Type") != "application/json" {
app.unsupportedMediaType(w, r)
return
}
next.ServeHTTP(w, r)
})
}
func (app *application) rateLimiter(next http.Handler) http.Handler {
limiter := tollbooth.NewLimiter(app.config.apiCallsAllowedPerSecond, nil)
limiter.SetIPLookups([]string{"X-Real-IP", "X-Forwarded-For", "RemoteAddr"})
return tollbooth.LimitHandler(limiter, next)
}
func (app *application) apiTimeout(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
timeoutDuration := time.Duration(app.config.apiTimeout) * time.Second
ctx, cancel := context.WithTimeout(r.Context(), timeoutDuration)
defer cancel()
r = r.WithContext(ctx)
done := make(chan struct{})
go func() {
next.ServeHTTP(w, r)
close(done)
}()
select {
case <-done:
return
case <-ctx.Done():
app.gatewayTimeout(w, r)
return
}
})
}
Нам нужно добавить промежуточное ПО для обработки запросов. Промежуточное ПО можно применить ко всем маршрутам или только к определенным. В нашем случае проверка Content-Type, требующая наличия заголовка Content-Type: application/json
в запросах, нужна только для POST-запросов. Поэтому изменим файл routes.go
следующим образом:
func (app *application) routes() http.Handler {
mux := httprouter.New()
mux.NotFound = http.HandlerFunc(app.notFound)
mux.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowed)
// Serve the Swagger UI. Uncomment this line later
// mux.Handler("GET", "/docs/*any", httpSwagger.WrapHandler)
mux.HandlerFunc("GET", "/status", app.status)
mux.HandlerFunc("GET", "/materia", app.getAllMateria)
// Adding content-type check middleware to only the POST method
mux.Handler("POST", "/fusion", app.contentTypeCheck(http.HandlerFunc(app.fuseMateria)))
return app.chainMiddlewares(mux)
}
func (app *application) chainMiddlewares(next http.Handler) http.Handler {
middlewares := []func(http.Handler) http.Handler{
app.recoverPanic,
app.apiTimeout,
app.rateLimiter,
}
for _, middleware := range middlewares {
next = middleware(next)
}
return next
}
Обработка ошибок
Добавьте следующие методы в <rootfolder>/api/errors.go
, чтобы помочь функциям промежуточного ПО:
func (app *application) unsupportedMediaType(w http.ResponseWriter, r *http.Request) {
message := fmt.Sprintf("The %s Content-Type is not supported", r.Header.Get("Content-Type"))
app.errorMessage(w, r, http.StatusUnsupportedMediaType, message, nil)
}
func (app *application) gatewayTimeout(w http.ResponseWriter, r *http.Request) {
message := "Request timed out"
app.errorMessage(w, r, http.StatusGatewayTimeout, message, nil)
}
Файлы структуры запроса и ответа
<rootfolder>/api/dtos.go
package main
// MateriaDTO provides Materia details - Name, Description and Type (Magic / Command / Support / Independent)
type MateriaDTO struct {
Name string `json:"name" example:"Thunder"`
Type string `json:"type" example:"Magic"`
Description string `json:"description" example:"Shoots lightning forward dealing thunder damage."`
}
// StatusDTO provides status of the server
type StatusDTO struct {
Status string `json:"Status" example:"OK"`
}
// ErrorResponseDTO provides Error message
type ErrorResponseDTO struct {
Error string `json:"Error" example:"The server encountered a problem and could not process your request"`
}
<rootfolder>/api/requests.go
package main
import (
"github.com/RayMathew/crisis-core-materia-fusion-api/internal/validator"
)
// MateriaFusionRequest provides input Materia names and their Mastered states
type MateriaFusionRequest struct {
Materia1Mastered *bool `json:"materia1mastered" example:"true"`
Materia2Mastered *bool `json:"materia2mastered" example:"false"`
Materia1Name string `json:"materia1name" example:"Fire"`
Materia2Name string `json:"materia2name" example:"Blizzard"`
Validator validator.Validator `json:"-"`
}
Validator
из сгенерированного кода будет использоваться позже для проверки полей ввода для конечной точки \fusion
.
Структура данных для правил комбинирования
Создайте файл <rootfolder>/internal/crisis-core-materia-fusion/constants.go
Добавьте следующее:
package crisiscoremateriafusion
type MateriaType string
const (
Fire MateriaType = "Fire"
Ice MateriaType = "Ice"
Lightning MateriaType = "Lightning"
Restore MateriaType = "Restore"
...
Создайте файл <rootfolder>/internal/crisis-core-materia-fusion/models.go
Добавьте следующее:
package crisiscoremateriafusion
type Materia struct {
Name string `json:"name"`
Type string `json:"materia_type"`
DisplayType string `json:"display_type"`
Description string `json:"description"`
Grade int `json:"grade"`
}
// Data structure to hold all basic combination rules
type BasicCombinationRule struct {
FirstMateriaType MateriaType
SecondMateriaType MateriaType
ResultantMateriaType MateriaType
}
var FILBasicRules = []BasicCombinationRule{
{Fire, Fire, Fire},
{Ice, Ice, Ice},
{Lightning, Lightning, Lightning},
{Fire, Ice, Lightning},
{Ice, Fire, Lightning},
{Fire, Lightning, Ice},
{Lightning, Fire, Ice},
{Lightning, Ice, Fire},
{Ice, Lightning, Fire},
}
var ...
Обработчик для \materia в api/handlers.go
func (app *application) getAllMateria(w http.ResponseWriter, r *http.Request) {
var allDisplayMateria []MateriaDTO
var allMateria []ccmf.Materia
var err error
allMateria, err = app.getAllMateriaFromApprSource()
if err != nil {
app.serverError(w, r, err)
}
// Some materia have the same name but different grades.
// We need to allow only unique names are sent in the response.
seenMateriaNames := make(map[string]bool)
for _, materia := range allMateria {
if _, isDuplicate := seenMateriaNames[materia.Name]; !isDuplicate {
seenMateriaNames[materia.Name] = true
allDisplayMateria = append(allDisplayMateria, MateriaDTO{
Name: materia.Name,
Type: materia.DisplayType,
Description: materia.Description,
})
}
}
err = response.JSON(w, http.StatusOK, allDisplayMateria)
if err != nil {
app.serverError(w, r, err)
}
}
func (app *application) getAllMateriaFromApprSource() (allMateria []ccmf.Materia, err error) {
// Check if allMateria data is in cache
if data, found := app.getCachedData(string(ccmf.AllMateriaCacheKey)); found {
// Type assertion: assert that data is of type []Materia
if allMateriaCache, ok := data.([]ccmf.Materia); ok {
allMateria = allMateriaCache
app.logger.Debug("cache hit")
} else {
app.logger.Error("Failed to assert cached data as []Materia")
return nil, errors.New("failed to assert cached data as []Materia")
}
} else {
// allMateria data is not in cache. Get from DB
app.logger.Debug("cache miss")
allMateria, err = app.db.GetAllMateria()
app.setCache(string(ccmf.AllMateriaCacheKey), allMateria)
}
return
}
Внутрисерверный кэш
Мы используем внутрисерверный кэш, потому что:
- Данные, полученные из БД, никогда не меняются.
- Одни и те же данные используются конечными точками
\materia
и\fusion
.
Обновите main.go
:
// declare a cache and a mutex.
// the mutex is to ensure there is only one operation using the cache at a time.
type application struct {
db *database.DB
logger *slog.Logger
cache map[string]interface{}
wg sync.WaitGroup
mu sync.Mutex
config config
}
// in run() initialize the cache:
app := &application{
config: cfg,
db: db,
logger: logger,
cache: make(map[string]interface{}),
}
Обновите api/helpers.go
:
// remove backgroundTask()
// add getter and setter for the cache:
func (app *application) getCachedData(key string) (interface{}, bool) {
app.mu.Lock()
defer app.mu.Unlock()
data, found := app.cache[key]
return data, found
}
func (app *application) setCache(key string, value interface{}) {
app.mu.Lock()
defer app.mu.Unlock()
app.cache[key] = value
}
Обработчик для \fusion в api/handlers.go
// showing only relevant parts of the code
func (app *application) fuseMateria(w http.ResponseWriter, r *http.Request) {
var fusionReq MateriaFusionRequest
err := request.DecodeJSON(w, r, &fusionReq)
if err != nil {
app.badRequest(w, r, err)
return
}
// Using the Validator we had defined in dtos.go
fusionReq.Validator.CheckField(fusionReq.Materia1Name != "", "materia1name", "materia1name is required")
fusionReq.Validator.CheckField(fusionReq.Materia2Name != "", "materia2name", "materia2name is required")
fusionReq.Validator.CheckField(fusionReq.Materia1Mastered != nil, "materia1mastered", "materia1mastered is required")
fusionReq.Validator.CheckField(fusionReq.Materia2Mastered != nil, "materia2mastered", "materia2mastered is required")
if fusionReq.Validator.HasErrors() {
app.failedValidation(w, r, fusionReq.Validator)
return
}
var allMateria []ccmf.Materia
allMateria, err = app.getAllMateriaFromApprSource()
if err != nil {
app.serverError(w, r, err)
}
var materia1Type string
var materia1Grade int
var materia2Type string
var materia2Grade int
// matching the request input with the categories in DB
for _, materia := range allMateria {
if materia1Type != "" && materia2Type != "" {
break
}
if materia.Name == fusionReq.Materia1Name && materia1Type == "" {
materia1Type = materia.Type
materia1Grade = materia.Grade
}
if materia.Name == fusionReq.Materia2Name && materia2Type == "" {
materia2Type = materia.Type
materia2Grade = materia.Grade
}
}
if materia1Type == "" || materia2Type == "" {
app.badRequest(w, r, errors.New("one or both of the Materia names are not recognised"))
return
}
// game rule - higher grade Materia moves to first position
exchangePositionsIfNeeded(&fusionReq, &materia1Grade, &materia2Grade, &materia1Type, &materia2Type)
relevantBasicRuleMap := ccmf.BasicRuleMap[ccmf.MateriaType(materia1Type)]
var relevantBasicRule ccmf.BasicCombinationRule
// finding the relevant combination rule
for _, rule := range relevantBasicRuleMap {
if (rule.FirstMateriaType == ccmf.MateriaType(materia1Type)) &&
(rule.SecondMateriaType == ccmf.MateriaType(materia2Type)) {
relevantBasicRule = rule
break
}
}
var resultantMateria MateriaDTO
// game rule - grade of resultant Materia depends on the input Materia as well as their Mastered state
resultantMateriaGrade := determineGrade(fusionReq, materia1Grade)
if relevantBasicRule.FirstMateriaType == "" {
app.logger.Info("none of the basic rules satisfy the requirement.")
// get final output using complex rules
resultantMateria = useComplexRules(materia1Grade, materia2Grade, resultantMateriaGrade, materia1Type, materia2Type, *fusionReq.Materia1Mastered, *fusionReq.Materia2Mastered, &allMateria)
} else {
// get final output using basic rules
resultantMateriaType := relevantBasicRule.ResultantMateriaType
for _, materia := range allMateria {
if materia.Grade == resultantMateriaGrade && materia.Type == string(resultantMateriaType) {
resultantMateria.Name = materia.Name
resultantMateria.Type = materia.DisplayType
resultantMateria.Description = materia.Description
break
}
}
}
err = response.JSON(w, http.StatusOK, resultantMateria)
if err != nil {
app.serverError(w, r, err)
}
}
// Combination rules which do not follow any pattern, and had to be coded separately
func useComplexRules(materia1Grade, materia2Grade, resultantMateriaGrade int, materia1Type, materia2Type string, materia1Mastered, materia2Mastered bool, allMateria *[]ccmf.Materia) (resultantMateria MateriaDTO) {
var resultantMateriaType string
switch {
// Complex Rule 1: FIL, Defense
case (materia1Type == string(ccmf.Fire) ||
materia1Type == string(ccmf.Ice) ||
materia1Type == string(ccmf.Lightning)) && materia2Type == string(ccmf.Defense):
if materia1Grade == 1 && materia2Grade == 1 {
// output is Defense when grades are equal to 1
resultantMateriaType = string(ccmf.Defense)
if materia1Mastered || materia2Mastered {
// final Grade is increased when output is Defense
increaseGrade(&resultantMateriaGrade)
}
} else {
// output is FIL when grades are NOT equal to 1
resultantMateriaType = materia1Type
}
...
// prepare response DTO
updateResultantMateriaData(allMateria, resultantMateriaGrade, resultantMateriaType, &resultantMateria)
return resultantMateria
}
Полный код обработчика можно найти здесь: https://github.com/RayMathew/crisis-core-materia-fusion-api/blob/main/api/handlers.go.
Документация по определению Swagger UI и OpenAPI
Добавьте библиотеку Swagger:
go get -u github.com/swaggo/swag/cmd/swag
go get github.com/swaggo/http-swagger
go get github.com/swaggo/swag
В routes.go
раскомментируйте строку Swagger и добавьте импорт:
httpSwagger "github.com/swaggo/http-swagger"
В файлах обработчика, DTO и модели добавьте комментарии для документации Swagger. Обратитесь к этому для всех вариантов.
В терминале выполните:
cd api
swag init -d .
Это создает папку api/docs
с определением, доступным для Go, JSON и YAML.
Чтобы протестировать его, запустите локальный сервер и откройте http://localhost:4444/docs.
Окончательная структура папок
📦 crisis-core-materia-fusion-api
.gitignore
├─ Dockerfile
├─ LICENSE
├─ Makefile
├─ README.md
├─ api
│ ├─ docs
│ │ ├─ docs.go
│ │ ├─ swagger.json
│ │ └─ swagger.yaml
│ ├─ dtos.go
│ ├─ errors.go
│ ├─ handlers.go
│ ├─ helpers.go
│ ├─ main.go
│ ├─ middleware.go
│ ├─ requests.go
│ ├─ routes.go
│ └─ server.go
├─ go.mod
├─ go.sum
└─ internal
├─ crisis-core-materia-fusion
│ ├─ constants.go
│ └─ models.go
├─ database -- db.go
├─ env -- env.go
├─ request -- json.go
├─ response -- json.go
└─ validator
├─ helpers.go
└─ validator.go
Настройка удаленного экземпляра PostgreSQL в CockroachDB
После создания сертификата сохраните его в папку certs
вашего проекта под именем root.crt
. Позже мы будем ссылаться на этот файл в конфигурации Google Run. Важно помнить, что папка certs
не должна быть добавлена в удаленный репозиторий. Добавьте ее в .gitignore
.
Сертификат создается локально только для проверки соединения, поэтому его удаление из репозитория не вызовет проблем.
После завершения настройки, в веб-интерфейсе CockroachDB перейдите в "Панель управления" → "Базы данных" (левое меню). Вы должны увидеть созданную вами базу данных в списке.
Миграция
Из локального экземпляра БД выполните:
pg_dump --no-owner --no-privileges -U <admin_username> -d materiafusiondb > full_dump.sql
В веб-интерфейсе CockroachDB перейдите в раздел Migrations (левое меню) и выберите Добавить Schema. Затем перетащите файл SQL, который вы только что получили. Система автоматически выполнит все шаги, кроме вставки данных в таблицу. Она также отобразит список выполненных действий.
К сожалению, на момент написания этой статьи экземпляр PostgreSQL в CockroachDB не поддерживает оператор IMPORT INTO
. Поэтому мне пришлось создать оператор INSERT
для 270 строк в локальном файле SQL (его можно получить из вывода pg_dump
).
Теперь вам нужно войти в удаленный экземпляр CockroachDB и запустить этот SQL-файл.
Инструкции по входу в удаленный экземпляр:
psql -h <REMOTE_DB_CLUSTER_HOSTNAME> -U <REMOTE_USERNAME> -d materiafusiondb -p <REMOTE_DB_PORT>
Разверните экземпляр Google Cloud Run
- Создайте Dockerfile, как этот.
- Перейдите в Google Cloud Run и создайте новый проект для API.
- Создать службу (Service) → Непрерывное развертывание из репозитория → НАСТРОЙКА С ОБЛАЧНОЙ СБОРКОЙ (SETUP WITH CLOUD BUILD) → Поставщик репозитория = Github → Выберите репозиторий → Тип сборки = Dockerfile → Сохранить (Save).
- Authentication = Разрешить неаутентифицированные вызовы.
- Большинство значений по умолчанию должны быть в порядке как есть.
- Прокрутите вниз до Контейнеры (Containers) → Порт контейнера (Container Port) = 4444.
- Выберите вкладку Переменные и секреты и добавьте те же переменные среды, что и в нашем локальном файле
.env
.
Значения:
HTTP_PORT = 4444
DB_DSN = <remote_cockroachdb_url>?sslmode=verify-full&sslrootcert=/app/certs/root.crt
API_TIMEOUT_SECONDS = 5
API_CALLS_ALLOWED_PER_SECOND = 1
Использование Google Secret Manager для сертификата
Последний кусочек головоломки.
- Найдите Secret Manager → Create Secret → Name = ‘DB_CERT’ → Загрузите сертификат .crt CockroachDB.
- В Cloud Run → (ваша служба) → Нажмите Edit Continuous Deployment → Прокрутите вниз до Configuration → Open Editor.
- Добавьте это в качестве первого шага:
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
args:
- '-c'
- >
mkdir -p certs
gcloud secrets versions access latest --secret="DB_CERT" >
certs/root.crt
id: Fetch Secret
Благодаря этому Cloud Build создаст файл certs/root.crt
в вашем проекте перед началом сборки. Таким образом, Dockerfile получит доступ к этому файлу, даже если он не был добавлен в репозиторий Github.
Это всё, что нужно сделать. Сохраните изменения, зафиксируйте их и проверьте, запускается ли сборка. После успешного завершения на панели мониторинга Cloud Run появится URL-адрес вашего развернутого сервера Go.