Go と PostgreSQL で API を構築し、Google Cloud Run、Cloud Build、Secret Manager、Artifact Registry で CI/CD パイプラインを設定し、Cloud Run インスタンスを CockroachDB に接続しました。
API はゲーム「クライシ コア: ファイナルファンタジー VII」に基づいており、「マテリア フュージョン」をシミュレートします。この記事の対象読者は、API を構築してデプロイする方法を知りたい開発者です。このプロジェクトに取り組んでいる間に学んだこと、うまくいかなかったこと、ゲームのマテリア融合ルールの理解と翻訳について話す別の記事があります (リンクは近日公開予定)。
3 つのエンドポイント — ヘルスチェック (GET)、全マテリアのリスト (GET)、マテリア融合のシミュレーション (POST)
マテリア (単数形と複数形の両方) は、魔法の源として機能するクリスタル オーブです。ゲーム内には 144 種類のマテリアが存在し、大きく「魔法」、「コマンド」、「サポート」、「独立」の 4 つのカテゴリに分類されます。ただし、マテリア融合のルールを理解する目的では、融合動作に基づいて 32 の内部カテゴリ と、それらのカテゴリ内に 8 つのグレード を用意する方が簡単でした (参照を参照) .
マテリアは一定時間使用すると「マスター」になります。ここでは期間は重要ではありません。
最も重要なのは、2 つのマテリアを融合して新しいマテリアを生成できることです。フュージョンを管理するルールは次の影響を受けます:
そして、いくつかのルールには 3 レベルの入れ子になった if-else ロジックがあるなど、多くの例外があります。これにより、DB に単純なテーブルを作成してそこに 1000 個のルールを永続化したり、すべてをルール化する 1 つの式を考え出したりする可能性が排除されます。
要するに、次のものが必要です:
理想的には、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;
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 ファイルを作成します。次の値を追加します:
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 }
ミドルウェア機能を支援するために、次のメソッドを
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 エンドポイントの入力フィールドを検証するために使用されます。
ファイル
以下を追加します:
# create the DB createdb materiafusiondb # step into the DB to perform SQL commands psql materiafusiondb
32 個の MateriaType の完全なリストはここにあります。
ファイル
以下を追加します:
-- 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);
次の理由によりサーバー内キャッシュを使用しています:
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
HTTP_PORT=4444 DB_DSN=go_client:<password>@localhost:5432/materiafusiondb?sslmode=disable API_TIMEOUT_SECONDS=5 API_CALLS_ALLOWED_PER_SECOND=1
完全なハンドラー コードはここにあります。
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 } }) }
ローカル DB インスタンスから、次のコマンドを実行します。
brew install postgresql@17
リモート インスタンスへのログイン:
# 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;
これにより、Cloud Build はビルドの開始前にプロジェクト内にファイル certs/root.crt を作成するため、Github リポジトリにプッシュしていなくても、Dockerfile はそのファイルにアクセスできるようになります。
それで終わりです。コミットをプッシュして、ビルドがトリガーされるかどうかを確認してください。 Cloud Run ダッシュボードには、ホストされている Go サーバーの URL が表示されます。
「なぜ Y ではなく X を実行したのですか?」に関する質問については、これをお読みください。
他に知りたいことや議論したいことがある場合は、ここにアクセスするか、以下にコメントしてください。
以上がGo、PostgreSQL、Google Cloud、CockroachDB を使用した API の構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。