dockerを使ってgoの環境構築(DBと接続)

10 min読了の目安(約9000字TECH技術記事

これからデータベースやストレージなどGo以外も使って開発していくので、そのまま自分のPC上にこれらの環境を構築していくのはいいものではありません。そこでDockerを使ってコンテナ上で開発をしていこうと思います。前回の続きになります。

今回のデータはgithubの07go_dockerにあります。

今回はデータベース(DB)のコンテナを立ててgoから接続することを目標にします。

データベースコンテナ

まずはデータベースコンテナを作成します。使用するRDBMSはmysqlです。選定に深い理由はありませんが強いて言うならどこかのサイトの記事でウェブでの使用に向いていると書いてあったからです。

ファイルの準備

前回のディレクトリに新しくmysqlディレクトリを作成し、以下のように新しいファイルを作成します。

$ tree
.
├── Dockerfile
├── app
├── docker-compose.yml
└── mysql
    ├── Dockerfile
    ├── data
    ├── init
    │   └── 1_create.sql
    └── my.cnf

mysqlディレクトリ内のDockerfileに以下を記述します。ここの記述に合わせてイメージを作成します。

mysql/Dockerfile
#使うDockerイメージ
FROM mysql:8.0
#ポートを開ける
EXPOSE 3306
#MySQL設定ファイルをイメージ内にコピー
ADD ./my.cnf /etc/mysql/conf.d/my.cnf
#docker runに実行される
CMD ["mysqld"]

mysqlの設定ファイル?になります。

my.cnf
[mysqld]
character-set-server=utf8
[mysql]
default-character-set=utf8
[client]
default-character-set=utf8

docker-composeファイルも以下のように書き換えます。mysqlと言うサービスを新たに追加します。ここでもデータベースの永続化のためにボリュームのマウントをしています。

docker-compose.yml
version: "3" # composeファイルのバージョン
services: 
    app: # サービス名
        build: . # ビルドに使うDockerfileの場所
        tty: true # コンテナの永続化
        ports: # ホストOSのポートとコンテナのポートをつなげる 
            - "8080:8080"
        volumes:
            - ./app:/go/src/app # マウントディレクトリ
        depends_on: 
            - mysql

    mysql:
        build: ./mysql/
        volumes:
            # 初期データを投入するSQLが格納されているdir
            - ./mysql/init:/docker-entrypoint-initdb.d
            # 永続化するときにマウントするdir
            - ./mysql/data:/var/lib/mysql
        environment: 
            - MYSQL_ROOT_PASSWORD=golang
        ports:
            - "3306:3306"

これはデータベースとテーブルを作成するsqlファイルになります。docker-compose upの時に実行されます。どのようなことを実行するかはコード内のコメントを見てください。

1_create.sql
-- golang_dbという名前のデータベースを作成
CREATE DATABASE golang_db;
-- golang_dbをアクティブ
use golang_db;
-- usersテーブルを作成。名前とパスワード
CREATE TABLE users (
    id INT(11) AUTO_INCREMENT NOT NULL,
    name VARCHAR(64) NOT NULL,
    password CHAR(30) NOT NULL,
    PRIMARY KEY (id)
);
-- usersテーブルに2つレコードを追加
INSERT INTO users (name, password) VALUES ("gophar", "5555");
INSERT INTO users (name, password) VALUES ("octcat", "0000");

コンテナの作成&立ち上げ

ファイルが準備出来たらビルドします。

$ docker-compose build
Building mysql
Step 1/4 : FROM mysql:8.0
 ---> 8e85dd5c3255
Step 2/4 : EXPOSE 3306
 ---> Using cache
 ---> 57e6e235b797
Step 3/4 : ADD ./my.cnf /etc/mysql/conf.d/my.cnf
 ---> Using cache
 ---> 3603a7f231cf
Step 4/4 : CMD ["mysqld"]
 ---> Using cache
 ---> a03850f907a0

Successfully built a03850f907a0
Successfully tagged 07go_docker_mysql:latest
Building app
Step 1/7 : FROM golang:1.15.2-alpine
 ---> b3bc898ad092
Step 2/7 : RUN apk update && apk add git
 ---> Using cache
 ---> 82065b063fa0
Step 3/7 : RUN mkdir /go/src/app
 ---> Using cache
 ---> 255ef438ae85
Step 4/7 : WORKDIR /go/src/app
 ---> Using cache
 ---> 9c73427c8b19
Step 5/7 : ADD . /go/src/app
 ---> e3826e3c7d9b
Step 6/7 : RUN go get -u github.com/oxequa/realize     && go get github.com/go-sql-driver/mysql
 ---> Running in 6d1562465cfe
Removing intermediate container 6d1562465cfe
 ---> cfb7fd63e955
Step 7/7 : CMD ["realize", "start"]
 ---> Running in 67385e578f57
Removing intermediate container 67385e578f57
 ---> 6d76e5f4f220

Successfully built 6d76e5f4f220
Successfully tagged 07go_docker_app:latest

ビルドが上手くいったらコンテナを立ち上げましょう。ここで1_create.sqlが実行され各種ファイルがmysql/dataに作成されます。

$ docker-compose up -d
Creating network "07go_docker_default" with the default driver
Creating 07go_docker_mysql_1 ... done
Creating 07go_docker_app_1   ... done

ここで一度ちゃんと作成出来たか確認するためにコンテナ内に入ります。

$ docker-compose exec mysql bash

mysqlに入ります。ユーザは"root", パスワードはdocker-compose.ymlで設定した"golang"になっていると思います。

root@937857e6b01b:/# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.21 MySQL Community Server - GPL

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

1_create.sqlの通りに作成されているか確認するために以下のコマンドでデータベース、テーブル、レコードの一覧を取得してみましょう。

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| golang_db          |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.01 sec)

mysql> use golang_db
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> desc users;
+----------+-------------+------+-----+---------+----------------+
| Field    | Type        | Null | Key | Default | Extra          |
+----------+-------------+------+-----+---------+----------------+
| id       | int         | NO   | PRI | NULL    | auto_increment |
| name     | varchar(64) | NO   |     | NULL    |                |
| password | char(30)    | NO   |     | NULL    |                |
+----------+-------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)

mysql> SELECT * FROM users;
+----+--------+----------+
| id | name   | password |
+----+--------+----------+
|  1 | gophar | 5555     |
|  2 | octcat | 0000     |
+----+--------+----------+
2 rows in set (0.00 sec)

ちゃんと設定通り出来てることが確認出来ました。

goからデータベースにアクセスする

データベースのコンテナを作成出来たので、サーバから接続します。まず、必要なモジュールをインストールするためにDockerfileを以下のように書き換えます。

Dockerfile
# 2020/10/14最新versionを取得
FROM golang:1.15.2-alpine
# アップデートとgitのインストール
RUN apk update && apk add git
# appディレクトリの作成
RUN mkdir /go/src/app
# ワーキングディレクトリの設定
WORKDIR /go/src/app
# ホストのファイルをコンテナの作業ディレクトリに移行
ADD . /go/src/app
RUN go get -u github.com/oxequa/realize \
    # sqlを使うためのモジュール
    && go get github.com/go-sql-driver/mysql
CMD ["realize", "start"]

docker-compose.ymlは先ほどのままでいいです。ここで一度ビルドしてください。ビルドしたらmain.goを編集していきます。
まず、モジュールをインポートしましょう。データベースを扱うモジュール①とmysqlを扱うモジュール②をインポートします。

import (
	"database/sql" // ①
	"html/template"
	"log"
	"net/http"
	"time"
	_ "github.com/go-sql-driver/mysql" // ②
)

データベースから読み込んだユーザデータを格納する構造体とmysqlに接続する際に必要となるユーザやパスワードなどの情報を代入した定数DriverName, DataSourceNameを定義します。

ユーザやパスワードをハードコーディングしてます。一般的にネットワークに直接つながっているウェブサーバに機密情報を置くのは好ましくありません。
goで作ったサーバの場合どうなんでしょう?コンパイルするから大丈夫なのかな?詳しい人いたら教えてください。今回は練習なのでそこらへんは無視します。

main.go
// User db users
type User struct {
	ID       int
	Name     string
	Password string
}
const (
	// DriverName ドライバ名(mysql固定)
	DriverName = "mysql"
	// DataSourceName user:password@tcp(container-name:port)/dbname
	DataSourceName = "root:golang@tcp(mysql-container:3306)/golang_db"
)
var usr = make(map[int]User)

main関数の上部に以下を追記してください。これがデータベースからデータを取得する処理です。

main.go
func main() {
    // database
    // データベースへの接続 ①
	db, dbErr := sql.Open(DriverName, DataSourceName)
	if dbErr != nil {
		log.Print("error connecting to database:", dbErr)
	}
    defer db.Close()
    // usersテーブルの全てのレコードを取得するクエリの実行 ②
    rows, queryErr := db.Query("SELECT * FROM users")
    if queryErr != nil {
        log.Print("query error :", queryErr)
    }
    defer rows.Close()
    // ループを回してrowsからScanでデータを取得する。 ③
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Name, &u.Password); err != nil {
            log.Print(err)
        }
        usr[u.ID] = User{
            ID:       u.ID,
            Name:     u.Name,
            Password: u.Password,
        }
    }
    // 略
  1. sql.Openは*DB型を返します。DB型は基本的なコネクションプール[1]を持つハンドラーです。

  2. db.Queryはクエリを実行しRows型で返します。Rows型はクエリの実行結果です。

  3. 今回はSELECTでデータを取得しているので、上のコードだとrowsに取得したデータが含まれています。ただそのままrowsにアクセスしてもデータは取得出来ません。Scanメソッドを使って取得します。なのでrows.Next()でループを回します。rows.Nextは、Scanメソッドを実行しているレコードの次が存在するかどうかでtrueかfalseを返します。
    Scanメソッドは引数に与えられた変数にフィールドの値を代入します。今回はID, name, passwordの三つのフィールドがあるので三つの引数を与えます。また引数に渡した変数に書き込むのでポインタで渡します。

これで取得したデータが先で定義したusrに格納されます。次にこれをhtmlテンプレートに埋め込みましょう。html/templateについてはgoでWebサービス No.3に書いています。埋め込み用の変数であったtempにusrを追加します。

main.go
// Embed htmlファイルに埋め込むデータ構造体
type Embed struct {
	Title   string
	Message string
	Users   map[int]User
	Time    time.Time
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
	temp := Embed{"Hello Golang!", "こんにちは!", usr, time.Now()}

htmlファイルも同様に書き換えます。

index.html
<ul>
    {{ range $user := .Users }}
        <li>name: {{ $user.Name }}, password: {{ $user.Password }}</li>
    {{ end }}
</ul>

これで実行すると以下のようになります。先ほど確認したレコードと同じものが取得できていますね。
実行結果

これでDBとの接続が出来ました。

脚注
  1. コネクションプールとは、フロー実行終了時に使用したデータベースのコネクションをプールに保存しておいて、次のフロー実行時に再利用する機能です。再利用によりデータベースへの接続が短縮されます。 ↩︎