概要
Go,MySQLでバックエンドを構築する際にDockerで完結するような開発環境を作りたいなと思い調べつつ実装してみました。
要件としては下記の通り。
- Go,MySQLの開発環境がdocker-composeで構成されていること
- ORMのGORMを用いてDBにread,writeできること
- データが永続化されること(次回のコンテナ起動時に前回データが残っている状態)
環境
$ go version
go version go1.14.2 darwin/amd64
$ docker version
Client: Docker Engine - Community
Version: 19.03.8
API version: 1.40
Go version: go1.12.17
Git commit: afacb8b
Built: Wed Mar 11 01:21:11 2020
OS/Arch: darwin/amd64
Experimental: false
Server: Docker Engine - Community
Engine:
Version: 19.03.8
API version: 1.40 (minimum version 1.12)
Go version: go1.12.17
Git commit: afacb8b
Built: Wed Mar 11 01:29:16 2020
OS/Arch: linux/amd64
Experimental: true
containerd:
Version: v1.2.13
GitCommit: 7ad184331fa3e55e52b890ea95e65ba581ae3429
runc:
Version: 1.0.0-rc10
GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd
docker-init:
Version: 0.18.0
GitCommit: fec3683
実装
APIのベースとルーティング実装
まずはGo Modulesでライブラリ管理してhttpリクエストをルーティングするような処理をかいていきます。
$ mkdir sample-go-mysql-docker
$ cd sample-go-mysql-docker
$ touch main.go
$ go mod init
$ go get -u "github.com/gorilla/mux"
endpointを定義して疎通確認します。
main.go
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter()
router.HandleFunc("/products", createProduct).Methods("POST")
router.HandleFunc("/products/latest", getProductLatest).Methods("GET")
if err := http.ListenAndServe(":8080", router); err != nil {
log.Fatal(err)
}
}
func createProduct(w http.ResponseWriter, r *http.Request) {
TODO
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("success create product"))
}
func getProductLatest(w http.ResponseWriter, r *http.Request) {
TODO
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("success get product latest"))
}
curlして下記のように返って来ればok
$ go run main.go
$ curl -X POST http://localhost:8080/products
success create product%
$ curl http://localhost:8080/products/latest
success get product latest%
DB構築
docker-composeでDBを構築していく。
$ touch docker-compose.yml
mysqlのcontainerを定義します。
docker-compose.yml
version: '3'
services:
db:
image: mysql:8.0
ports:
- '3306:3306'
volumes:
- ./docker/db/init:/docker-entrypoint-initdb.d
- ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf
- ./tmp/db/mysql_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_USER: user
MYSQL_PASSWORD: password
dbを初期化するためのsqlは ./docker/db/init
配下に作成します。
ここで注意するのが docker-entrypoint-initdb.d
が毎回実行されるのではなく /var/lib/mysql
が存在しないようなケース(初回起動時)などに実行されるので頻繁にinit用のsqlを書き換えたりしていると反映されなくてハマる。その場合は一度コンテナを削除するなどする。
docker/db/init/init_db.sql
SET CHARSET utf8mb4;
DROP DATABASE IF EXISTS sample;
CREATE DATABASE IF NOT EXISTS sample DEFAULT CHARACTER SET utf8mb4;
日本語を扱う場合は4バイト文字列を扱ったりする可能性があるので文字コードの設定を変更しておかないといけない。そのためにcharsetをutf8mb4にしてcollationはutf8mb4_general_ciにしたmy.cnf
をマウントして反映しておく。
docker/db/my.cnf
[mysqld]
# character
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
[client]
default-character-set=utf8mb4
docker-composeからコンテナ起動してpsでプロセス確認するとmysqlが起動できていることがわかります。
$ docker-compose up -d
$ docker ps
APIサーバーのdocker定義追加
先ほどは go run
コマンドを使ってアプリケーションを起動しましたがdocker-composeコマンドを使って起動できるようにします。
apiのDockerfileを定義します。
docker/api/Dockerfile
FROM golang:1.14.2
ENV GO111MODULE=on
ENV APP_DIR=/go/src/github.com/konchanxxx/sample-go-mysql-docker
WORKDIR $APP_DIR
# APIサーバーからDBを参照する際のデバッグ等に使えるのでclientを入れておく
RUN apt-get update && apt-get install -y default-mysql-client
COPY . .
RUN go mod download
docker-composeファイルのservicesにapiを追加します。この際よくlinksを定義する記事を見かけたりするのですがdocker-composeで記述した各サービスはデフォルトで一つのnetworkに設定されるため書かなくて良いのとlinksは廃止される見込みのため記述していません。
version: '3'
services:
api:
build:
context: .
dockerfile: docker/api/Dockerfile
tty: true
depends_on:
- db
ports:
- '8080:8080'
command: sh ./bin/run.sh
db:
image: mysql:8.0
ports:
- '3306:3306'
volumes:
- ./docker/db/init:/docker-entrypoint-initdb.d
- ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf
- ./tmp/db/mysql_data:/var/lib/mysql
environment:
MYSQL_DATABASE: sample
MYSQL_ROOT_PASSWORD: password
MYSQL_USER: user
MYSQL_PASSWORD: password
run.shはmysqlが起動するまでwaitします。
bin/run.sh
until mysqladmin ping -h db --silent; do
echo 'waiting for connection mysql...'
sleep 2
done
echo 'Database is launched!!'
exec go run main.go
APIサーバーとDBの繋ぎこみ
APIサーバーとDBを繋いでいきます。ORMはGORMを利用します。
エラーハンドリングは簡易的にpanicで処理しているので実際にアプリケーションを実装する際はエラーレスポンス等を返す必要があります。
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
)
const (
dbDialect = "mysql"
)
func init() {
conn, err := getConn()
if err != nil {
panic(err)
}
defer conn.Close()
conn.AutoMigrate(&Product{})
}
func main() {
router := mux.NewRouter()
router.HandleFunc("/products", createProduct).Methods("POST")
router.HandleFunc("/products/latest", getProductLatest).Methods("GET")
if err := http.ListenAndServe(":8080", router); err != nil {
log.Fatal(err)
}
}
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
}
func createProduct(w http.ResponseWriter, r *http.Request) {
conn, err := getConn()
if err != nil {
panic(err)
}
defer conn.Close()
err = conn.Create(&Product{Name: "switch", Price: 29980}).Error
if err != nil {
panic(err)
}
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
}
func getProductLatest(w http.ResponseWriter, r *http.Request) {
conn, err := getConn()
if err != nil {
panic(err)
}
defer conn.Close()
p := &Product{}
err = conn.Last(p).Error
if err != nil {
panic(err)
}
res, err := json.Marshal(p)
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(res)
}
func getConn() (*gorm.DB, error) {
connectionString := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?parseTime=true&loc=Local&charset=utf8mb4",
"user",
"password",
"db",
"3306",
"sample",
)
return gorm.Open(dbDialect, connectionString)
}
起動確認
アプリケーションのファイルを修正したのでdocker-compose buildでイメージをビルドし直してからコンテナ起動する。curlでデータがDBに生成されていることがわかる。
$ docker-compose build
$ docker-compose up -d
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
843d3bdf2feb sample-go-mysql-docker_api "sh ./bin/run.sh" 4 minutes ago Up 4 minutes 0.0.0.0:8080->8080/tcp sample-go-mysql-docker_api_1
66b6ac91f5e2 mysql:8.0 "docker-entrypoint.s…" 26 minutes ago Up 4 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp sample-go-mysql-docker_db_1
$ curl -X POST http://localhost:8080/products
$ curl http://localhost:8080/products/latest
{"id":1,"name":"switch","price":29980}%
成果物
github.com
ハマったところ
- init dbするところでコンテナが初回起動済みだと変更が反映されなかったりするのでコンテナ削除する必要などがあったりする
- MySQL: 5.7 系で最初やろうとしたがdocker storage engineのoverlay2のエラーでディレクトリマウントできなかった。こちらまだ解決できてないので時間があれば調べる。
参考