DB接続付きGraphQLサーバ(by Golang)をローカルマシン上でDockerコンテナ起動

# お題
表題の通り。[前回](https://qiita.com/sky0621/items/f03631f0309f5c375b1d)までGraphQLを題材にフロント・バックエンドそれぞれで実装を進めてきた。
まだまだ実装することは山ほどあるけど、今のところローカルマシン内でほそぼそと立ち上げているこのアプリを[GKE](https://cloud.google.com/kubernetes-engine/?hl=ja)にでも載せてみようと思っているので、まず手始めにアプリのDocker化を試みる。
今回は、バックエンド（Golang）だけ。接続するDBはローカルのDockerコンテナのまま。

# 関連記事索引
- 第12回「[GraphQLにおけるRelayスタイルによるページング実装再考(Window関数使用版)](https://qiita.com/sky0621/items/957c010ba01c7a756ef7)」
- 第11回「[Dataloadersを使ったN+1問題への対応](https://qiita.com/sky0621/items/621f075e4257270a9e02)」
- 第10回「[GraphQL(gqlgen)エラーハンドリング](https://qiita.com/sky0621/items/affc833e770fee8b576e)」
- 第９回「[GraphQLにおける認証認可事例（Auth0 RBAC仕立て）](https://qiita.com/sky0621/items/efbb8fa085094230d0fd)」
- 第８回「[GraphQL/Nuxt.js(TypeScript/Vuetify/Apollo)/Golang(gqlgen)/Google Cloud Storageの組み合わせで動画ファイルアップロード実装例](https://qiita.com/sky0621/items/5c2c565678c102d9d4ed)」
- 第７回「[GraphQLにおけるRelayスタイルによるページング実装（後編：フロントエンド）](https://qiita.com/sky0621/items/fdcdaefeef7cf4b750ac)」
- 第６回「[GraphQLにおけるRelayスタイルによるページング実装（前編：バックエンド）](https://qiita.com/sky0621/items/1e8823200633f2c46013)」
- 第５回「[DB接続付きGraphQLサーバ(by Golang)をローカルマシン上でDockerコンテナ起動](https://qiita.com/sky0621/items/58fa2bcfc0935f6186cb)」
- 第４回「[graphql-codegenでフロントエンドをGraphQLスキーマファースト](https://qiita.com/sky0621/items/f03631f0309f5c375b1d)」
- 第３回「[go+gqlgenでGraphQLサーバを作る（GORM使ってDB接続）](https://qiita.com/sky0621/items/32e3e26189589080e492)」
- 第２回「[NuxtJS(with Apollo)のTypeScript対応](https://qiita.com/sky0621/items/0297f5a026be380a5929)」
- 第１回「[frontendに「nuxtjs/apollo」、backendに「go+gqlgen」の組み合わせでGraphQLサービスを作る](https://qiita.com/sky0621/items/8abd445edba347e8f6f1)」

# 開発環境
## # 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

```docker:[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コンテナをひとまとめに制御するときに便利なドッカーコンポーズ。

```yaml:[docker-compose.yml]
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サービス
１つ目のサービス定義の「`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サービス
２つ目のサービス定義の「`app`」は、同一階層にあるDockerfileを使ってGoアプリをビルドし、以下の指定によってホストマシンの 80 番ポートでコンテナ内のアプリにアクセス可能にする。

```yaml
    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`内で明示的に使う指定をするだけ。

```yaml
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 関数を含むソースを記載。（このファイル分だけ載せてもしょうがないのだけど。）

```go:[backend/server/server.go]
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`
初回はそれなりに時間がかかる。（２回目以降はキャッシュが効いて、とても早い。）

```
$ 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` 番ポートにアクセス。
![Screenshot from 2020-01-10 01-14-13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/dc1516ba-eb19-8af8-975c-11f6afda5db4.png)
こんな感じでGraphQLのプレイグラウンドが表示される。

試しに、 todo テーブルにレコードを追加する mutation を実行してみる。（左が実行した mutation で右が実行結果）
![Screenshot from 2020-01-10 01-16-32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/717a4750-6c2d-0555-6c79-0050b61ee71a.png)

登録した結果も query で表示してみる。
![Screenshot from 2020-01-10 01-18-44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/8dbfb811-331b-0b82-b999-8a0729eff0c9.png)

# まとめ
次回は、フロントエンド（Vue.js/Nuxt.js）のDocker化かなぁ。
いっそのことGKE載せちゃおうかな。。。