Help us understand the problem. What is going on with this article?

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

お題

表題の通り。前回までGraphQLを題材にフロント・バックエンドそれぞれで実装を進めてきた。
まだまだ実装することは山ほどあるけど、今のところローカルマシン内でほそぼそと立ち上げているこのアプリをGKEにでも載せてみようと思っているので、まず手始めにアプリのDocker化を試みる。
今回は、バックエンド(Golang)だけ。接続するDBはローカルのDockerコンテナのまま。

関連記事索引

開発環境

# 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

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

[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サービス

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

[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
初回はそれなりに時間がかかる。(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 番ポートにアクセス。
Screenshot from 2020-01-10 01-14-13.png
こんな感じでGraphQLのプレイグラウンドが表示される。

試しに、 todo テーブルにレコードを追加する mutation を実行してみる。(左が実行した mutation で右が実行結果)
Screenshot from 2020-01-10 01-16-32.png

登録した結果も query で表示してみる。
Screenshot from 2020-01-10 01-18-44.png

まとめ

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

sky0621
Go使い。最近はRustラブ。Webアプリケーション作ることが多い。フロントエンドもクラウド(GCP好き)もそれなりに触る。2019/10からGraphQLも嗜む。
https://github.com/sky0621/Curriculum-Vitae/blob/master/README.md
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away