ホームページ  >  記事  >  バックエンド開発  >  Go、PostgreSQL、Google Cloud、CockroachDB を使用した API の構築

Go、PostgreSQL、Google Cloud、CockroachDB を使用した API の構築

Linda Hamilton
Linda Hamiltonオリジナル
2024-10-24 07:02:30758ブラウズ

Go と PostgreSQL で API を構築し、Google Cloud Run、Cloud Build、Secret Manager、Artifact Registry で CI/CD パイプラインを設定し、Cloud Run インスタンスを CockroachDB に接続しました。

API はゲーム「クライシ コア: ファイナルファンタジー VII」に基づいており、「マテリア フュージョン」をシミュレートします。この記事の対象読者は、API を構築してデプロイする方法を知りたい開発者です。このプロジェクトに取り組んでいる間に学んだこと、うまくいかなかったこと、ゲームのマテリア融合ルールの理解と翻訳について話す別の記事があります (リンクは近日公開予定)。

簡単に参照できるリンク

  • GitHub リポジトリと README
  • Swagger (OpenAPI) のドキュメントとテスト
  • 公共郵便配達員コレクション
  • ドメインモデルのソース

APIの目的

3 つのエンドポイント — ヘルスチェック (GET)、全マテリアのリスト (GET)、マテリア融合のシミュレーション (POST)

ドメインモデル

マテリア (単数形と複数形の両方) は、魔法の源として機能するクリスタル オーブです。ゲーム内には 144 種類のマテリアが存在し、大きく「魔法」、「コマンド」、「サポート」、「独立」の 4 つのカテゴリに分類されます。ただし、マテリア融合のルールを理解する目的では、融合動作に基づいて 32 の内部カテゴリ と、それらのカテゴリ内に 8 つのグレード を用意する方が簡単でした (参照を参照) .

マテリアは一定時間使用すると「マスター」になります。ここでは期間は重要ではありません。

最も重要なのは、2 つのマテリアを融合して新しいマテリアを生成できることです。フュージョンを管理するルールは次の影響を受けます:

  • どちらかまたは両方のマテリアを習得しているかどうか。
  • どのマテリアが最初に来るか (X のように、Y は必ずしも Y X と等しいわけではありません)。
  • マテリア内部カテゴリ。
  • マテリアグレード

Building an API with Go, PostgreSQL, Google Cloud and CockroachDB

そして、いくつかのルールには 3 レベルの入れ子になった if-else ロジックがあるなど、多くの例外があります。これにより、DB に単純なテーブルを作成してそこに 1000 個のルールを永続化したり、すべてをルール化する 1 つの式を考え出したりする可能性が排除されます。

要するに、次のものが必要です:

  1. 列 name(string)、materia_type(ENUM) (32 の内部カテゴリ)、grade(integer)、display_materia_type(ENUM) (ゲームで使用される 4 つのカテゴリ)、description(string) および id(整数) を自動インクリメント主キーとして使用します。
  2. 基本ルール形式 MateriaTypeA MateriaTypeB = MateriaTypeC をカプセル化するデータ構造。
  3. 基本的かつ複雑なルールを使用して、内部カテゴリとグレードの観点から出力マテリアを決定するコード。

1. ローカル PostgreSQL DB のセットアップ

理想的には、Web サイト自体から DB をインストールできます。ただし、pgAdmin ツールは何らかの理由で DB に接続できなかったので、Homebrew を使用しました。

インストール

brew install postgresql@17

これにより、DB の使用に役立つ一連の 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;

2. Go サーバーの作成

autostrada.dev を使用して定型コードを作成します。 api、postgresql、httprouter、env var config、tintedlogging、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 ファイル

ボイラープレート ジェネレーターは、環境変数を取得してコードに追加するコードを作成しましたが、値の追跡と更新を簡単に行うことができます。

/.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 つを追加します: Content-Type チェック、レート制限、API タイムアウト保護。

料金所ライブラリを追加:

go get github.com/didip/tollbooth

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
}

エラー処理

ミドルウェア機能を支援するために、次のメソッドを /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)
}

リクエストおよびレスポンス構造ファイル

/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"`
}

<ルートフォルダ>/api/requests.go :

brew install postgresql@17

生成されたコードからのバリデーターは、後で Fusion エンドポイントの入力フィールドを検証するために使用されます。

組み合わせルールのデータ構造

ファイル /internal/crisis-core-materia-fusion/constants.go を作成します

以下を追加します:

# create the DB
createdb materiafusiondb
# step into the DB to perform SQL commands
psql materiafusiondb

32 個の MateriaType の完全なリストはここにあります。

ファイル /internal/crisis-core-materia-fusion/models.go を作成します

以下を追加します:

-- 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;

ルールの完全なリストはここにあります。

api/handlers.go のマテリアのハンドラー

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);

サーバー内キャッシュ

次の理由によりサーバー内キャッシュを使用しています:

  1. DB から取得したデータは変更されません。
  2. マテリアとフュージョンの両方のエンドポイントで同じデータが使用されます。

main.go を更新します:

COPY materia(name,materia_type,grade,display_materia_type,description) FROM
 '<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;

api/helpers.go を更新します:

? 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

api/handlers.go の融合用ハンドラー

HTTP_PORT=4444
DB_DSN=go_client:<password>@localhost:5432/materiafusiondb?sslmode=disable
API_TIMEOUT_SECONDS=5
API_CALLS_ALLOWED_PER_SECOND=1

完全なハンドラー コードはここにあります。

Swagger UI と OpenAPI の定義ドキュメント

Swagger ライブラリを追加します:

go get github.com/joho/godotenv

routes.go で Swagger 行のコメントを解除し、インポートを追加します。

// 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

ハンドラー、DTO、およびモデル ファイルに、Swagger ドキュメントのコメントを追加します。すべてのオプションについては、これを参照してください。

ターミナルで次を実行します:

go get github.com/didip/tollbooth

これにより、Go、JSON、YAML で使用できる定義を含む api/docs フォルダーが作成されます。

テストするには、ローカル サーバーを起動し、http://localhost:4444/docs を開きます。


最終的なフォルダー構造:

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
  }
 })
}

3. CockroachDB でのリモート PostgreSQL インスタンスのセットアップ

  1. ここからの手順を使用してください。
  2. 証明書を作成した後、プロジェクトに /certs/root.crt を作成し、そこに証明書を追加します。このファイルは、後の Google Run 設定で参照します。
  3. 注意! このフォルダーをリモート リポジトリにプッシュすることはしません。 certs/ フォルダーを .gitignore に追加します。必要に応じて、接続をテストするためだけにローカルで証明書を作成します。
  4. CockroachDB → ダッシュボード → 左側のメニュー → データベースに移動すると、作成した DB が表示されるはずです。
  5. 移住

    ローカル DB インスタンスから、次のコマンドを実行します。

    brew install postgresql@17
    
    1. CockroachDB → 左側のメニュー → Migrations → Add Schema → 取得した SQL ファイルをドラッグします。テーブル データの挿入を除くすべてのステップが実行されます。実行されたステップのリストも表示されます。
    2. この記事の執筆時点では、CockroachDB の PostgreSQL インスタンスは IMPORT INTO のようなステートメントをサポートしていません。そのため、ローカル SQL ファイルに 270 行の INSERT ステートメントを作成する必要がありました (これは、先ほど取得した pg_dump 出力から導き出すことができます)。
    3. リモート インスタンスにログインし、SQL ファイルを実行します。

    リモート インスタンスへのログイン:

    # create the DB
    createdb materiafusiondb
    # step into the DB to perform SQL commands
    psql materiafusiondb
    

    4. Google Cloud Run インスタンスをデプロイする

    1. 次のような Dockerfile を作成します。
    2. Google Cloud Run に移動し、API の新しいプロジェクトを作成します。
    3. サービスの作成 → リポジトリから継続的にデプロイクラウドビルドでセットアップリポジトリプロバイダ = Github → リポジトリを選択 → ビルドタイプ = Dockerfile → 保存。
    4. 認証 = 認証されていない呼び出しを許可します.
    5. ほとんどのデフォルトはそのままで問題ありません。
    6. コンテナ → コンテナ ポート = 4444.
    7. まで下にスクロールします。
    8. 変数とシークレット タブを選択し、ローカル .env ファイルにあるものと同じ環境変数を追加します。

    値:

    1. HTTP_PORT = 4444
    2. DB_DSN = ?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 を検索 → シークレットを作成 → 名前 = ‘DB_CERT’ → CockroachDB の .crt 証明書をアップロードします。
    2. Cloud Run → (サービス) → 継続的デプロイの編集 をクリック → 構成 まで下にスクロール → エディタを開きます。
    3. これを最初のステップとして追加します:
    -- 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;
    

    これにより、Cloud Build はビルドの開始前にプロジェクト内にファイル certs/root.crt を作成するため、Github リポジトリにプッシュしていなくても、Dockerfile はそのファイルにアクセスできるようになります。


    それで終わりです。コミットをプッシュして、ビルドがトリガーされるかどうかを確認してください。 Cloud Run ダッシュボードには、ホストされている Go サーバーの URL が表示されます。


    「なぜ Y ではなく X を実行したのですか?」に関する質問については、これをお読みください。

    他に知りたいことや議論したいことがある場合は、ここにアクセスするか、以下にコメントしてください。

    以上がGo、PostgreSQL、Google Cloud、CockroachDB を使用した API の構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。