在本地机器上通过Docker容器启动带有DB连接的Golang GraphQL服务器

请用中文将以下内容转述:
题目

就像标题所说的一样。在上次之前,我们在前端和后端分别实施了GraphQL。
虽然还有很多要实现的地方,但是我打算将这个应用程序部署到GKE上,虽然目前只在本地机器上运行着。所以首先尝试将应用程序进行Docker化。
这次只涉及后端(使用Golang)。连接的数据库仍然是本地的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サービスを作る」

开发环境

操作系统 – Linux(Ubuntu)

$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"

# 后台

言语 – 前进

$ go version
go version go1.13.3 linux/amd64

包管理器 – Go Modules

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

本次文章涉及的是Golang源代码中的”backend”文件夹下的源代码和”Dockerfile”文件,以及在MySQL容器启动时,为应用程序创建必需的表格的”persistence”文件夹下的DDL文件。

Dockerfile的中文释义是什么?

首先是Dockerfile。通过多阶段构建,在实际运行中使用超轻量的scratch基础镜像。以下是部分重用的内容。

# 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"]

由于在文件”backend/server/main.go”中存在main函数,所以这个文件是构建目标。因此,在”RUN cd server”后面继续执行”go build”命令。此外,Go应用程序作为GraphQL服务器实现,并在端口5050上启动,因此写入”EXPOSE 5050″。然而,根据下文所述,仅凭此行不足以使主机能够访问容器。
参考链接:http://docs.docker.jp/engine/reference/builder.html#expose

docker-compose.yml 在中国大陆的母语中的释义:Docker组合文件。

接下来,当在本地控制各种Docker容器的时候,Docker Compose便成为一个很方便的工具。

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

数据库服务 kuò fú wù)

第一个服务定义中的“db”指的是就像它看起来的那样,用于启动MySQL容器的内容。这个没啥特别要提及的。通过定义的信息,数据库就可以创建了。
顺便一提,有准备好的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”将使用位于同一层级的Dockerfile来构建Go应用程序,并通过以下指定使得在主机的80端口访问容器内的应用程序成为可能。

    ports:
    - "80:5050"

网络

由于这次是在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服务器启动,并根据在启动时指定的数据源连接字符串连接到数据库。
在下面的部分,指定的主机IP实际上是经过仔细查询的。
数据源 = “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": {}
    }
]

下面是包含主函数的源代码。虽然只有这个文件的话没有意义,但还是附上了。

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)

确认动作

Screenshot from 2020-01-10 01-14-13.png
Screenshot from 2020-01-10 01-16-32.png
Screenshot from 2020-01-10 01-18-44.png

总结

下一次的话,可能是将前端(Vue.js/Nuxt.js)进行Docker化。
索性考虑将其部署在GKE上吧。。。

bannerAds