Создание API с помощью Go, PostgreSQL, Google Cloud и CockroachDB
2 ноября 2024 г.Я создал API с помощью Go и PostgreSQL, настроил конвейер CI/CD с помощью Google Cloud Run, Cloud Build, Secret Manager и Artifact Registry, а также подключил экземпляр Cloud Run к CockroachDB.
API основан на игреКризисное ядро: Final Fantasy VII, для имитации «Materia Fusion». Целевая аудитория этой статьи — разработчики, которые просто хотят узнать, как создать и развернуть API. У меня есть еще одинстатьягде я рассказываю обо всем, чему я научился, работая над этим проектом, и о том, что не сработало.
Ссылки для удобства использования
- Репозиторий GitHub и README
- Документация и тестирование Swagger (OpenAPI)
- Общественный сбор почтальонов
- Источник модели домена
Цель API
3 конечные точки — проверка работоспособности (GET), список всех материй (GET) и имитация слияния материй (POST).
Модель домена
Materia (как в единственном, так и во множественном числе) — это кристальная сфера, которая служит источником магии. В игре есть 144 различных материи, и они в целом делятся на 4 категории: «Магия», «Команда», «Поддержка» и «Независимая». Однако для того, чтобы разобраться в правилах слияния материй, было проще иметь32 внутренние категориина основе их поведения при слиянии, и8 классовв пределах этих категорий (см. ссылку).
Материя становится «Освоенной», когда она используется в течение определенного времени. Длительность здесь не важна.
Самое важное, что 2 материи могут быть объединены для получения новой материи. Правила, управляющие слиянием, зависят от:
- Независимо от того, освоена ли одна или обе материи.
- Какая материя идет первой (как в
X+Y
не обязательно равноY+X
). - Внутренняя категория Materia.
- Качество материала.
И есть МНОГО исключений, некоторые правила имеют 3 уровня вложенностиif-else
логика. Это исключает возможность создания простой таблицы в БД и сохранения в ней более 1000 правил или создания единой формулы для управления всеми.
Короче говоря, нам нужно:
Стол
materia
с колоннамиname(string)
,materia_type(ENUM)
(32 внутренние категории),grade(integer)
,display_materia_type(ENUM)
(4 категории, используемые в игре),description(string)
иid(integer)
как автоинкрементный первичный ключ.Структура данных для инкапсуляции формата основных правил
MateriaTypeA + MateriaTypeB = MateriaTypeC
.Код для использования базовых и сложных правил для определения выходной Материи с точки зрения ее внутренней категории и сорта.
1. Настройка локальной базы данных 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_seqAS integerSTART WITH 1INCREMENT BY 1NO MINVALUENO MAXVALUECACHE 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;
2. Создание сервера Go
Создайте шаблонный код, используяautostrada.dev. Добавьте вариантыapi
, postgresql
, httprouter
, env var config
, tinted
ведение журнала,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
Промежуточное ПО и маршруты
В шаблоне уже есть промежуточное ПО для восстановления после паники. Мы добавим еще 3: проверка типа контента, ограничение скорости и защита от тайм-аута 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"
...
полный список из 32MateriaType
s можно найтиздесь.
Создать файл<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.Descriptionbreak
}
}
}
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
}
Полный код обработчика можно найтиздесь.
Документация по определению 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](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
3. Настройка удаленного экземпляра PostgreSQL в CockroachDB
- Используйте шаги изздесь.
- После создания сертификата создайте
<rootfolder>/certs/root.crt
в проекте и добавляем туда сертификат. Мы сделаем ссылку на этот файл позже в конфигурации Google Run. - ОСТОРОЖНОСТЬ!МыНЕТотправка этой папки в удаленный репозиторий. Добавить
certs/
папка для.gitignore
. Мы создаем сертификат локально только для проверки соединения, если вы этого хотите. - Теперь, когда вы перейдете в CockroachDB → Панель управления → Левое меню → Базы данных, вы сможете увидеть созданную вами базу данных.
Миграция
Из локального экземпляра БД выполните:
pg_dump --no-owner --no-privileges -U <admin_username> -d materiafusiondb > full_dump.sql
- Перейдите в CockroachDB → Left Menu → Migrations → Add Schema → Перетащите файл SQL, который вы только что получили. Все шаги будут выполнены, кроме вставки данных таблицы. Он также покажет вам список выполненных шагов.
- На момент написания этой статьи экземпляр PostgreSQL в CockroachDB не поддерживал такие операторы, как
IMPORT INTO
. Поэтому мне пришлось создатьINSERT
оператор в локальном файле SQL для 270 строк (который мы можем вывести изpg_dump
вывод, который мы только что получили). - Войдите в удаленный экземпляр и запустите файл SQL.
Вход в удаленный экземпляр:
psql -h <REMOTE_DB_CLUSTER_HOSTNAME> -U <REMOTE_USERNAME> -d materiafusiondb -p <REMOTE_DB_PORT>
4. Разверните экземпляр Google Cloud Run
- Создайте Dockerfile, напримерэтот.
- Перейти кGoogle Cloud Runи создайте новый проект для API.
- Создать услугу →Постоянное развертывание из репозитория → НАСТРОЙКА С ПОМОЩЬЮ ОБЛАЧНОГО СБОРА → Поставщик репозитория= Github → Выберите свой репозиторий →Тип сборки = Dockerfile→ Сохранить.
- Аутентификация = Разрешить несанкционированные вызовы.
- Большинство настроек по умолчанию должны подойти и так.
- Прокрутите вниз до Контейнеры →Контейнерный порт= 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 → Создайте Secret → Имя = «DB_CERT» → Загрузите
.crt
сертификат CockroachDB. - В Cloud Run → (ваша услуга) → НажмитеИзменить непрерывное развертывание→ Прокрутите вниз доКонфигурация→ Открыть редактор.
- Добавьте это в качестве первого шага:
- 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.
По вопросам, связанным с «Почему вы сделали X, а не Y?», читайтеэтот.
Если вам интересно узнать или обсудить что-то еще, перейдите по ссылкездесь.
Оригинал