Golang×Prisma×MySQL 開発環境を Docker Compose で構築!

はじめに

API の個人開発を進めている中で、Golang の勉強をサボりつつなんかいい感じの DB マイグレーションツールが欲しいなと調べていたところ、Prismaというものを見つけました。
GraphQL を利用して DB マイグレーションを行ったり、一部の言語で型安全な ORM として利用できるそうです。

細かい説明は他の記事に譲るとして、ここでは開発環境の構築を行います。
ただし、ローカルはあんまり汚したくないので、できるだけ仮想環境でやりましょう。

コンテナ構成

Prisma の公式ページに アーキテクチャが載っています。

クライアントは API サーバーとやりとりを行い、API サーバーと DB の間に一枚 Prisma を挟む形ですね。
これをローカル環境で再現しようとすると、以下の図の形になるかと思います。

Docker Compose で API サーバー、Prisma サーバー、DB の3マシンを起動し、ブラウザからは API サーバーにのみアクセスする形になります。
ブラウザからは Prisma サーバーおよび DB には直接アクセスできないわけですね。

今回構築する開発環境では、より作業を行いやすいようにもっとゆるく作ります。
具体的には下図の通りです。

最初の図から、Prisma Cli が増えています。また、Prisma サーバーにアクセスできるようになっていますね。
詳細は後述しますが、Prisma サーバーにはブラウザから DB 操作を行うことができるツールが付属しています。
本番環境でこのツールを使えるようにするのは当然 NG ですが、開発環境では何かと便利なので、ポートを解放して使えるようにします。

ディレクトリ構成

最終的なディレクトリ構成はこんな感じです。

親ディレクトリ  
├── api-server  
│   └── dockerfile  
├── app  
│   ├── Gopkg.lock  
│   ├── Gopkg.toml  
│   ├── datamodel.prisma  
│   ├── generated  
│   │   └── prisma-client  
│   │       └── prisma.go  
│   ├── main.go  
│   ├── prisma.yml  
│   └── vendor  
├── docker-compose.yml  
└── prisma-cli  
    └── dockerfile  

一般的な Golang のディレクトリ構成とは大幅に異なりますが、これは Docker Compose による操作や指定をわかりやすくするためです。
通常のプロジェクトではちゃんと Golang の仕様や思想に従ったディレクトリ構成をお勧めします。

API サーバーの設定

特筆することはないです。
今回は Golang を使って API サーバーを建てる予定なので、依存性管理ができるようにdepを入れておきましょう。

FROM golang:1.13-alpine3.10  

WORKDIR /go/src/app  

RUN apk update \  
  && apk add git \  
  && go get -u github.com/golang/dep/cmd/dep  

Prisma Cli の設定

Prisma には Cli が公式で用意されています。
Homebrew で公開されているものもありますが、今回は npm ライブラリを利用しましょう。

FROM node:13.1-alpine3.10  

WORKDIR /usr/src/app  

RUN npm install -g yarn \  
  && yarn global add prisma  

Docker Compose 設定

Prisma サーバーと MySQL については、公式のページにサンプルがあります。これを参考にしつつ書いていきましょう。

version: "3"  

services:  
  api-server:  
    build: ./api-server  
    container_name: api-server  
    tty: true  
    ports:  
      - "8080:8080"  
    volumes:  
      - ./app:/go/src/app  

  prisma-cli:  
    build: ./prisma-cli  
    container_name: prisma-cli  
    tty: true  
    volumes:  
      - ./app:/usr/src/app  

  prisma-server:  
    image: prismagraphql/prisma:1.34  
    container_name: prisma-server  
    restart: always  
    ports:  
      - "4466:4466"  
    environment:  
      PRISMA_CONFIG: |  
        port: 4466  
        databases:  
          default:  
            connector: mysql  
            host: database  
            port: 3306  
            user: root  
            password: prisma  

  database:  
    image: mysql:5.7  
    container_name: database  
    environment:  
      MYSQL_ROOT_PASSWORD: prisma  
    volumes:  
      - mysql:/var/lib/mysql  

volumes:  
  mysql: ~  

いくつか注意点があります。

  • API サーバーと Prisma Cli にtty: trueをつける
    • API サーバーと Prisma Cli は実行し続けるコマンドを持たないため、起動完了後に停止してしまう
    • 停止したコンテナにはアクセスできないので、停止せずにアクセスできる状態を保つための設定
  • Prisma サーバーにrestart: alwaysをつける
    • Prisma サーバーは起動すると DB への接続を試みる
    • MySQL は起動に時間がかかるため、初回の接続は失敗する
    • 接続失敗するとコンテナが落ちてしまうので、接続できるまで再起動し続けるための設定

設定ができたらルートディレクトリに移動し、起動しましょう。

$ docker-compose up -d  

起動処理のログは$ docker-compose logsで確認できます。ログから Prisma サーバーの起動を確認できたら、ブラウザでlocalhost:4466にアクセスしてみましょう。アクセスできれば OK です。

DB の操作

では、構築した環境で DB を操作してみましょう。
まずは Prisma Cli に入ります。ディストリビューションは Alpine なので、bash の代わりに ash を使います。

$ docker-compose exec prisma-cli ash  

次にプロジェクトおよび DB の初期化を行います。

$ prisma init --endpoint http://prisma-server:4466  
$ prisma deploy  

ここで、エンドポイントとしてhttp://prisma-server:4466を指定しています。
prisma-serverは docker-compose.yml で指定した Prisma サーバーのサービス名ですね。Docker Compose では、サービス名を利用してサービス間で通信を行うことができます。つまり、ここでは「Prisma サーバーの 4466 ポート」をエンドポイントとして指定しているのです。
prisma deploydatamodel.prismaに従って DB のマイグレーションを行います。この辺りの書き方はGraphQLに従っているようなので、そちらを参考にしてください。

初期化が終わったら、データ構造の確認をしてみましょう。ブラウザからlocalhost:4466/_adminにアクセスしてみてください。datamodel.prismaの通りにデータ構造が作成できていれば OK です。

このページからは DB の操作を行うことができます。Prisma Adminとか言うらしいです。
試しにいくつかデータを登録してみましょう。画面右中央近くの+をクリックし、画面右側に出てくるタブに値を入力するだけ。最後に画面右下のボタンで保存すれば完了です。

最後に Golang 用の Prisma Client を生成して終わりましょう。Prisma Cli から Prisma Client を生成ってギャグかな?
prisma.ymlに Golang 用の Prisma Client 生成設定を追加して、生成コマンドを実行します。

endpoint: http://prisma-server:4466  
datamodel: datamodel.prisma  

generate:  
  - generator: go-client  
    output: ./generated/prisma-client/  
$ prisma generate  

うまくいけば、上記の output で指定したディレクトリにファイルが生成されているはずです。

API サーバー

それでは、生成した Prisma Client を使って API サーバーを作成しましょう。
先ほどと同じようにシェルを使って API サーバーに入ってもいいのですが、私は VSCode プラグインのRemote - Containersをお勧めします。
なんでもいいので、シェルが使える状態になったら OK です。
まずは先ほど生成した Prisma Client を使ってデータを取得してみましょう。公式ページのこの辺りこの辺りを参考に進めていきます。

$ cd /go/src/app  
$ dep init  
$ touch main.go  
package main  

import (  
    "app/generated/prisma-client"  
    "context"  
    "fmt"  
)  

func main() {  
    client := prisma.New(nil)  
    ctx := context.TODO()  

    users, err := client.Users(nil).Exec(ctx)  
    if err != nil {  
        panic(err)  
    }  

    fmt.Println(users)  
}  

はいできました。Golang は学習中なのでクオリティは察してください。
とりあえず実行してみましょう。

$ go run main.go  
[{ck3ez5b7e00110766n4zr6wro Alice} {ck3ez5b8u001507666qcbs2qy Bob} {ck3ez5b9b001907668hf3pgyw Charles}]  

DB に登録した「Alice」「Bob」「Charles」の情報が取得できていますね。ck3e~の文字列は自動生成された ID です。
あとはこれを HTTP リクエストに対して Json 形式でレスポンスできれば OK です。この記事この記事を参考に進めていきましょう。

package main  

import (  
    "app/generated/prisma-client"  
    "context"  
    "log"  
    "net/http"  
    "reflect"  

    "github.com/ant0ine/go-json-rest/rest"  
)  

var ctx = context.TODO()  
var client = prisma.New(nil)  

func main() {  
    api := rest.NewApi()  
    api.Use(rest.DefaultDevStack...)  

    router, err := rest.MakeRouter(  
        rest.Get("/users", GetUsers),  
    )  
    if err != nil {  
        log.Fatal(err)  
        panic("Failed to setup router.")  
    }  

    api.SetApp(router)  
    log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))  
}  

func GetUsers(w rest.ResponseWriter, r *rest.Request) {  
    users, err := client.Users(nil).Exec(ctx)  
    if err != nil {  
        handleError(w, err)  
        return  
    }  

    usersMap := usersToMap(users)  
    w.WriteJson(usersMap)  
}  

func handleError(w rest.ResponseWriter, err error) {  
    log.Fatal(err)  

    result := map[string]string{"error": "server error"}  

    w.WriteJson(result)  
    w.WriteHeader(http.StatusInternalServerError)  
}  

func usersToMap(users []prisma.User) map[string]map[string]string {  
    result := make(map[string]map[string]string)  

    for _, user := range users {  
        tag := reflect.TypeOf(user).Field(1).Tag.Get("json")  
        result[user.ID] = map[string]string{tag: user.Name}  
    }  

    return result  
}  

出来上がったら、実行してアクセスしてみましょう。アクセス先はlocalhost:8080/usersです。

$ go run ./main.go  
{  
  "ck3ez5b7e00110766n4zr6wro": {  
    "name": "Alice"  
  },  
  "ck3ez5b8u001507666qcbs2qy": {  
    "name": "Bob"  
  },  
  "ck3ez5b9b001907668hf3pgyw": {  
    "name": "Charles"  
  }  
}  

こんな感じの Json レスポンスが取得できれば OK です!

おわりに

というわけで、仮想環境に Prisma サーバーを軸にした開発環境を構築できました。これを発展させていけば、簡単な API サーバー程度ならすぐ実装できるんじゃないでしょうか。
とはいえ GraphQL の詳細や Prisma Client の利用方法についてはほとんど触れていませんし、ちゃんと運用するためにはもっと勉強が必要でしょうね。覚えることはどんどん増えていきます。

今回は無印の Prisma を利用しましたが、今後リリース予定の Prisma2 が現在開発されています。
まだ Golang 非対応(2019/11/30)だったので今回は利用しませんでしたが、早いところ Golang で使えるようになってほしいですね。

参考資料

Prisma 関連

prisma - 最速 GraphQL Server 実装
Prisma
Prisma Docs
Prisma 公式サーバーイメージ
Prisma2

Docker 関連

いい加減 docker-compose で links を使うのをやめて network でコンテナ間名前解決をする
Dockerfile のベストプラクティス
Compose ファイル・リファレンス

Golang 関連

Go にはディレクトリ構成のスタンダードがあるらしい。
VSCode で Go の Modules 設定
golang で REST API をやってみた ①
Go-Json-Rest
Standard Go Project Layout
Go の構造体にメタ情報を付与するタグの基本
Golang
dep(Golang の依存性管理ツール)

その他

Remote Containers
GraphQL
draw.io(製図に利用)