Создание API с помощью Go, PostgreSQL, Google Cloud и CockroachDB

Создание 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. У меня есть еще одинстатьягде я рассказываю обо всем, чему я научился, работая над этим проектом, и о том, что не сработало.

  1. Репозиторий GitHub и README
  2. Документация и тестирование Swagger (OpenAPI)
  3. Общественный сбор почтальонов
  4. Источник модели домена

Цель API

3 конечные точки — проверка работоспособности (GET), список всех материй (GET) и имитация слияния материй (POST).

Модель домена

Materia (как в единственном, так и во множественном числе) — это кристальная сфера, которая служит источником магии. В игре есть 144 различных материи, и они в целом делятся на 4 категории: «Магия», «Команда», «Поддержка» и «Независимая». Однако для того, чтобы разобраться в правилах слияния материй, было проще иметь32 внутренние категориина основе их поведения при слиянии, и8 классовв пределах этих категорий (см. ссылку).

Материя становится «Освоенной», когда она используется в течение определенного времени. Длительность здесь не важна.

Самое важное, что 2 материи могут быть объединены для получения новой материи. Правила, управляющие слиянием, зависят от:

  • Независимо от того, освоена ли одна или обе материи.
  • Какая материя идет первой (как вX+Yне обязательно равноY+X).
  • Внутренняя категория Materia.
  • Качество материала.

Materia Fusion

И есть МНОГО исключений, некоторые правила имеют 3 уровня вложенностиif-elseлогика. Это исключает возможность создания простой таблицы в БД и сохранения в ней более 1000 правил или создания единой формулы для управления всеми.

Короче говоря, нам нужно:

  1. Столmateriaс колоннамиname(string), materia_type(ENUM)(32 внутренние категории),grade(integer), display_materia_type(ENUM)(4 категории, используемые в игре),description(string)иid(integer)как автоинкрементный первичный ключ.

  2. Структура данных для инкапсуляции формата основных правилMateriaTypeA + MateriaTypeB = MateriaTypeC.

  3. Код для использования базовых и сложных правил для определения выходной Материи с точки зрения ее внутренней категории и сорта.

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"
  ...

полный список из 32MateriaTypes можно найтиздесь.

Создать файл<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.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

  1. Используйте шаги изздесь.
  2. После создания сертификата создайте<rootfolder>/certs/root.crtв проекте и добавляем туда сертификат. Мы сделаем ссылку на этот файл позже в конфигурации Google Run.
  3. ОСТОРОЖНОСТЬ!МыНЕТотправка этой папки в удаленный репозиторий. Добавитьcerts/папка для.gitignore. Мы создаем сертификат локально только для проверки соединения, если вы этого хотите.
  4. Теперь, когда вы перейдете в CockroachDB → Панель управления → Левое меню → Базы данных, вы сможете увидеть созданную вами базу данных.

Миграция

Из локального экземпляра БД выполните:

pg_dump --no-owner --no-privileges -U <admin_username> -d materiafusiondb > full_dump.sql

  1. Перейдите в CockroachDB → Left Menu → Migrations → Add Schema → Перетащите файл SQL, который вы только что получили. Все шаги будут выполнены, кроме вставки данных таблицы. Он также покажет вам список выполненных шагов.
  2. На момент написания этой статьи экземпляр PostgreSQL в CockroachDB не поддерживал такие операторы, какIMPORT INTO. Поэтому мне пришлось создатьINSERTоператор в локальном файле SQL для 270 строк (который мы можем вывести изpg_dumpвывод, который мы только что получили).
  3. Войдите в удаленный экземпляр и запустите файл SQL.

Вход в удаленный экземпляр:

psql -h <REMOTE_DB_CLUSTER_HOSTNAME> -U <REMOTE_USERNAME> -d materiafusiondb -p <REMOTE_DB_PORT>

4. Разверните экземпляр Google Cloud Run

  1. Создайте Dockerfile, напримерэтот.
  2. Перейти кGoogle Cloud Runи создайте новый проект для API.
  3. Создать услугу →Постоянное развертывание из репозиторияНАСТРОЙКА С ПОМОЩЬЮ ОБЛАЧНОГО СБОРАПоставщик репозитория= Github → Выберите свой репозиторий →Тип сборки = Dockerfile→ Сохранить.
  4. Аутентификация = Разрешить несанкционированные вызовы.
  5. Большинство настроек по умолчанию должны подойти и так.
  6. Прокрутите вниз до Контейнеры →Контейнерный порт= 4444.
  7. ВыбиратьПеременные и секретыи добавьте те же переменные среды, что и в нашем локальном.envфайл.

Ценности:

  1. HTTP_PORT = 4444
  2. DB_DSN =<remote_cockroachdb_url>?sslmode=verify-full&sslrootcert=/app/certs/root.crt
  3. API_TIMEOUT_SECONDS = 5
  4. API_CALLS_ALLOWED_PER_SECOND = 1

Использование Google Secret Manager для сертификата

Последний кусочек пазла.

  1. Найдите Secret Manager → Создайте Secret → Имя = «DB_CERT» → Загрузите.crtсертификат CockroachDB.
  2. В Cloud Run → (ваша услуга) → НажмитеИзменить непрерывное развертывание→ Прокрутите вниз доКонфигурация→ Открыть редактор.
  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.


По вопросам, связанным с «Почему вы сделали X, а не Y?», читайтеэтот.

Если вам интересно узнать или обсудить что-то еще, перейдите по ссылкездесь.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE