DevGang
Авторизоваться

Как создать 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 могут быть объединены для получения нового материала. Правила слияния зависят от:

  1. Освоение Materia (оба или только один)
  2. Порядка Materia (X + Y ≠ Y + X).
  3. Внутренней категории Materia
  4. Класса 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
}

Внутрисерверный кэш

Мы используем внутрисерверный кэш, потому что:

  1. Данные, полученные из БД, никогда не меняются.
  2. Одни и те же данные используются конечными точками \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 для сертификата

Последний кусочек головоломки.

  1. Найдите Secret Manager → Create Secret → Name = ‘DB_CERT’ → Загрузите сертификат .crt CockroachDB.
  2. В Cloud Run → (ваша служба) → Нажмите Edit Continuous Deployment → Прокрутите вниз до Configuration → Open Editor.
  3. Добавьте это в качестве первого шага:
  - 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.

Источник:

#PostgreSQL #Golang #Google API
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу