お題
表題の通り。前回までGraphQLを題材にフロント・バックエンドそれぞれで実装を進めてきた。
まだまだ実装することは山ほどあるけど、今のところローカルマシン内でほそぼそと立ち上げているこのアプリをGKEにでも載せてみようと思っているので、まず手始めにアプリのDocker化を試みる。
今回は、バックエンド(Golang)だけ。接続するDBはローカルのDockerコンテナのまま。
関連記事索引
- 第12回「GraphQLにおけるRelayスタイルによるページング実装再考(Window関数使用版)」
- 第11回「Dataloadersを使ったN+1問題への対応」
- 第10回「GraphQL(gqlgen)エラーハンドリング」
- 第9回「GraphQLにおける認証認可事例(Auth0 RBAC仕立て)」
- 第8回「GraphQL/Nuxt.js(TypeScript/Vuetify/Apollo)/Golang(gqlgen)/Google Cloud Storageの組み合わせで動画ファイルアップロード実装例」
- 第7回「GraphQLにおけるRelayスタイルによるページング実装(後編:フロントエンド)」
- 第6回「GraphQLにおけるRelayスタイルによるページング実装(前編:バックエンド)」
- 第5回「DB接続付きGraphQLサーバ(by Golang)をローカルマシン上でDockerコンテナ起動」
- 第4回「graphql-codegenでフロントエンドをGraphQLスキーマファースト」
- 第3回「go+gqlgenでGraphQLサーバを作る(GORM使ってDB接続)」
- 第2回「NuxtJS(with Apollo)のTypeScript対応」
- 第1回「frontendに「nuxtjs/apollo」、backendに「go+gqlgen」の組み合わせでGraphQLサービスを作る」
開発環境
# OS - Linux(Ubuntu)
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
# バックエンド
言語 - Go
$ go version
go version go1.13.3 linux/amd64
パッケージマネージャ - Go Modules
IDE - Goland
GoLand 2019.3.1
Build #GO-193.5662.65, built on December 23, 2019
# Dockerコンテナ
Docker
$ $ sudo docker -v
Docker version 19.03.5, build 633a0ea838
docker-compose
$ docker-compose -v
docker-compose version 1.23.1, build b02f1306
実践
今回の全ソースは下記。
https://github.com/sky0621/study-graphql/tree/v0.5.0
プロジェクト構成
$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql
$
$ tree -L 1
.
├── backend
├── docker-compose.yml
├── Dockerfile
├── frontend
├── persistence
├── README.md
└── schema
今回の記事で絡むのは、上記のうち「backend
」配下のGolangソースと「Dockerfile
」に「docker-compose.yml
」。
あとは、MySQLコンテナ起動時にアプリが必要とするテーブルを作るため「persistence
」配下のDDL。
Dockerfile
何はともあれDockerfile。
マルチステージビルドで実稼働コンテナは超軽量なscratch
ベースに。
以下、流用しつつ。
https://qiita.com/sky0621/items/4c314bd07da284176a29#dockerfile
# step 1: build go app
FROM golang:1.13.5-alpine3.11 as build-step
# for go mod download
RUN apk add --update --no-cache ca-certificates git
RUN mkdir /go-app
WORKDIR /go-app
COPY backend/go.mod .
COPY backend/go.sum .
RUN go mod download
COPY backend .
RUN cd server && CGO_ENABLED=0 go build -o /go/bin/go-app
# -----------------------------------------------------------------------------
# step 2: exec
FROM scratch
COPY --from=build-step /go/bin/go-app /go/bin/go-app
EXPOSE 5050
ENTRYPOINT ["/go/bin/go-app"]
Goのmain
関数があるファイルは「backend/server/main.go
」にあるため、このファイルがビルド対象。
なので、RUN cd server
に続けてgo build
してる。
あと、GoのアプリはGraphQLサーバとして実装しているのだけど、ポートを5050
で起動しているので、EXPOSE 5050
と書いた。
ただ、↓によると「これだけではホストからコンテナにアクセスできるようにしません。
」なんだとか。
http://docs.docker.jp/engine/reference/builder.html#expose
docker-compose.yml
続いて、ローカルでもろもろのDockerコンテナをひとまとめに制御するときに便利なドッカーコンポーズ。
version: '3'
services:
db:
restart: always
image: mysql:5.7.24
command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: localuser
MYSQL_PASSWORD: localpass
MYSQL_DATABASE: localdb
volumes:
- ./persistence/init:/docker-entrypoint-initdb.d
networks:
- study-graphql-network
app:
build: .
ports:
- "80:5050"
networks:
- study-graphql-network
volumes:
localdb:
external: false
networks:
study-graphql-network:
external: true
dbサービス
1つ目のサービス定義の「db
」は見たまんま、MySQLコンテナを立ち上げるためのもの。これはまあ特に言及することない。定義した情報でDBができる。
ちなみに、以下のようにDDLが用意してあってコンテナ起動時にこのDDLが叩かれてテーブルも作られる。
$ tree persistence/
persistence/
├── init
│ └── 1_create.sql
└── README.md
$
$ cat persistence/init/1_create.sql
CREATE TABLE IF NOT EXISTS `todo` (
`id` varchar(64) NOT NULL,
`text` varchar(256) NOT NULL,
`done` bool NOT NULL,
`user_id` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;
CREATE TABLE IF NOT EXISTS `user` (
`id` varchar(64) NOT NULL,
`name` varchar(256) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;
appサービス
2つ目のサービス定義の「app
」は、同一階層にあるDockerfileを使ってGoアプリをビルドし、以下の指定によってホストマシンの 80 番ポートでコンテナ内のアプリにアクセス可能にする。
ports:
- "80:5050"
networks
今回、GoアプリからMySQLデータベース内のテーブルにアクセスする実装にしているのでコンテナ間で通信できないと意味がない。
Dockerでは明示的にネットワークを作成してコンテナ間で同じネットワーク内ということを実現できるようなので、そのように指定。
まずは、ネットワークを作る。
$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
1abb47a7ebd9 bridge bridge local
0843ed48f717 host host local
09614bd65e4d none null local
$
$ sudo docker network create study-graphql-network
とすると、
$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
1abb47a7ebd9 bridge bridge local
0843ed48f717 host host local
09614bd65e4d none null local
ba8a943cb12b study-graphql-network bridge local
といった感じで新たなネットワークが作られるので、あとはdocker-compose.yml
内で明示的に使う指定をするだけ。
services:
db:
〜〜:
networks:
- study-graphql-network
app:
〜〜:
networks:
- study-graphql-network
networks:
study-graphql-network:
external: true
Goアプリのmain関数
GoアプリはGraphQLサーバとして起動するのだけど、起動時に指定したデータソース接続文字列によってDB接続に行く。
以下の部分なのだけど、ここで指定しているホストIPは、実はちゃんと調べたもの。
「dataSource = "localuser:localpass@tcp(172.19.0.1:3306)/localdb?
」
前段で作っておいたDockerネットワーク、こいつの情報を以下のように表示すると、そこにこのネットワークのゲートウェイIPが載ってるので、それを記載。
$ sudo docker network inspect study-graphql-network
[
{
"Name": "study-graphql-network",
"Id": "ba8a943cb12b07e6dcddbc421a5d5b63c8f48cf526bf1da3ec454bad26233fad",
"Created": "2020-01-07T08:54:36.947913642+09:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.19.0.0/16",
"Gateway": "172.19.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
以下、一応 main 関数を含むソースを記載。(このファイル分だけ載せてもしょうがないのだけど。)
package main
import (
"log"
"net/http"
"os"
"github.com/99designs/gqlgen/handler"
"github.com/jinzhu/gorm"
"github.com/sky0621/study-graphql/backend"
_ "github.com/go-sql-driver/mysql"
)
const dataSource = "localuser:localpass@tcp(172.19.0.1:3306)/localdb?charset=utf8&parseTime=True&loc=Local"
const defaultPort = "5050"
func main() {
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
db, err := gorm.Open("mysql", dataSource)
if err != nil {
panic(err)
}
if db == nil {
panic(err)
}
defer func() {
if db != nil {
if err := db.Close(); err != nil {
panic(err)
}
}
}()
db.LogMode(true)
http.Handle("/", handler.Playground("GraphQL playground", "/query"))
http.Handle("/query", handler.GraphQL(backend.NewExecutableSchema(backend.Config{Resolvers: &backend.Resolver{DB: db}})))
log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
ビルド
せっかくdocker-compose使ってるので、docker-compose build
初回はそれなりに時間がかかる。(2回目以降はキャッシュが効いて、とても早い。)
$ sudo docker-compose build
db uses an image, skipping
Building app
Step 1/13 : FROM golang:1.13.5-alpine3.11 as build-step
1.13.5-alpine3.11: Pulling from library/golang
e6b0cf9c0882: Pull complete
2848faf0eed1: Pull complete
〜〜省略〜〜
Step 13/13 : ENTRYPOINT ["/go/bin/go-app"]
---> Running in ab52efb17aa9
Removing intermediate container ab52efb17aa9
---> 10ab8cd2ef9e
Successfully built 10ab8cd2ef9e
Successfully tagged study-graphql_app:latest
Docker起動
$ sudo docker-compose up
Starting study-graphql_db_1_7a805d40cf64 ... done
Starting study-graphql_app_1_3cd84c32df8a ... done
Attaching to study-graphql_db_1_7a805d40cf64, study-graphql_app_1_3cd84c32df8a
db_1_7a805d40cf64 | 2020-01-09T16:10:28.082404Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
db_1_7a805d40cf64 | 2020-01-09T16:10:28.084061Z 0 [Note] mysqld (mysqld 5.7.24) starting as process 1 ...
app_1_3cd84c32df8a | 2020/01/09 16:10:28 connect to http://localhost:5050/ for GraphQL playground
db_1_7a805d40cf64 | 2020-01-09T16:10:28.088730Z 0 [Note] InnoDB: PUNCH HOLE support available
db_1_7a805d40cf64 | 2020-01-09T16:10:28.088753Z 0 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
〜〜〜 省略 〜〜〜
db_1_7a805d40cf64 | 2020-01-09T16:10:28.342594Z 0 [Note] Event Scheduler: Loaded 0 events
db_1_7a805d40cf64 | 2020-01-09T16:10:28.345471Z 0 [Note] mysqld: ready for connections.
db_1_7a805d40cf64 | Version: '5.7.24' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL)
動作確認
GraphQLの挙動を確認するため、公開した 80
番ポートにアクセス。
こんな感じでGraphQLのプレイグラウンドが表示される。
試しに、 todo テーブルにレコードを追加する mutation を実行してみる。(左が実行した mutation で右が実行結果)
まとめ
次回は、フロントエンド(Vue.js/Nuxt.js)のDocker化かなぁ。
いっそのことGKE載せちゃおうかな。。。