🀄

Docker環境にGo(Air/Echo/Gorm)でAPIを作る

2024/02/01に公開

はじめに

Go学習の一環として、Goのメジャーなパッケージ(Air, Echo,Gorm)を使ってDocker環境にAPIとDB(MySQL)を作ります。

作業環境

分類 バージョンなど
PC mac(m1)
OS Sonoma 14.2.1
Go 1.21.4
Docker 24.0.7
docker-compose 2.23.3-desktop.2

最終的なディレクトリ構成

.
├── app
│   ├── db
│   │     └── db.go
│   ├── models
│   │     └── user.go
│   ├── routes
│   │     ├── routes.go
│   │     └── user.go
│   ├── .air.toml
│   ├── go.mod 
│   ├── go.sum 
│   └── main.go 
├── db
│   ├── initdb.d
│   │     └── user.sql      
│   └── conf.d
│   │     └── custom.cnf
├── env
│   ├── api.env 
│   └── db.env
├── docker-compose.yml
└── Dockerfile

作成手順

手順1: Dockerの準備

  1. 以下のディレクトリを作成
sample/
.
├── app
├── db
└── env

2. API用にDockerfileを作成

Dockerfile
# goのイメージをDockerHubから流用する(Alpine Linux)
FROM golang:1.21.5-alpine3.18
# Linuxパッケージ情報の最新化+gitがないのでgitを入れる
RUN apk update && apk add git
# ログのタイムゾーンを指定
ENV TZ /usr/share/zoneinfo/Asia/Tokyo
# コンテナ内の作業ディレクトリを指定
WORKDIR /app
# ソースコードをコンテナ内にコピー
COPY /app/* ./
# /app/go.modに記載された依存関係の解決+必要なパッケージのダウンロードを実行
RUN go mod download
# Airのバイナリをインストール
RUN go install github.com/cosmtrek/air@latest
# コンテナの公開するポートを指定
EXPOSE 5050
# 起動時のコマンド(airを使用するため)
CMD ["air", "-c", ".air.toml"]

3. APIとDB用にdocker-compose.ymlファイルを作成

docker-compose.yml
version: '3.9' # docker-compose.ymlファイルの構文バージョン
services:
  # DB用コンテナ作成
  db:
    container_name: sample-db
    # イメージの指定(docker-hubから直接流用)
    image: 'mysql:8.2.0'
    # DBデータ保持用のボリュームをバインド
    volumes: 
      - sample_db_data:/var/lib/mysql
    env_file:
      - ./env/db.env # 環境変数ファイルへのパス
  # API用コンテナ作成
  api:
    container_name: sample-api
    build: . # イメージのビルドに使用するDockerfileへの相対パス
    volumes:
    # バインドマウント
      - type: bind 
        source: ./app
        target: /app
    ports:
      - 5050:5050
    env_file:
      - ./env/api.env
    # 依存するサービス名(先に起動させたいサービス)
    depends_on:
      - db
# DBデータ保持用のボリューム
volumes:
  sample_db_data:

【現時点でのディレクトリ】

.
├── app  【New】                            
├── db    【New】
├── env  【New】
├── docker-compose.yml  【New】
└── Dockerfile 【New】

手順2: データベースの準備(MySQL)

1. envディレクトリにdb.envファイルを作成

env/db.env
# イメージ構築時に最初に作成されるDB名
MYSQL_DATABASE=sample
# rootユーザのパスワード
MYSQL_ROOT_PASSWORD=root
# タイムゾーンの設定
TZ=Asia/Tokyo

2. dbディレクトリにMySQLの設定ファイルcustom.cnfを作成
データベースが日本語に対応するよう設定ファイルを追加する。
ファイルを追加するディレクトリは db/conf.d/ とする。

db/conf.d/custom.cnf
[mysqld]
character-set-server=utf8
collation-server=utf8_general_ci

[client]
default-character-set=utf8
  • character-set-server=utf8
    MySQLサーバーのデフォルト文字セットをutf-8に指定している。
  • collation-server=utf8_general_ci
    MySQLサーバーのデフォルト照合順序をutf8_general_ciに指定している。
  • default-character-set=utf8
    MySQLクライアント接続のデフォルトの文字セットをutf-8に指定している。

3. 初期データの作成
データベースに初期テーブルとデータを挿入するsqlを用意する。
ファイルを追加するディレクトリは db/initdb.d/ とする。

db/initdb.d/users.sql
DROP TABLE IF EXISTS users;

CREATE TABLE IF NOT EXISTS users (
  user_id VARCHAR(36) PRIMARY KEY,
  user_name VARCHAR(256) NOT NULL,
  age TINYINT UNSIGNED NOT NULL,
  gender VARCHAR(256) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO users (user_id, user_name, age, gender) VALUES (UUID(), '田中太郎', 40, 'male');
INSERT INTO users (user_id, user_name, age, gender) VALUES (UUID(),'小保方晴子', 30, 'female');
INSERT INTO users (user_id, user_name, age, gender) VALUES (UUID(),'佐村河内守', 60, 'male');

【現時点でのディレクトリ】

.
├── app
├── db
│   ├── initdb.d
│   │     └── users.sql  【New】     
│   └── conf.d
│   │     └── custom.cnf 【New】
├── env
│   └── db.env 【New】
├── docker-compose.yml
└── Dockerfile

手順3: API作成の準備

1. dockerイメージ作成+コンテナ起動してAPI用コンテナのシェルにアクセス

  • イメージのビルド、コンテナの起動
docker-compose up -d
  • API用コンテナのシェルにアクセス
docker-compose exec -it api sh

シェル内ではデフォルトでDockerfileに指定したWORKDIRである/appディレクトリが表示される。
2. goプロジェクトの作成

  • go.modファイルの作成
    モジュールの定義と依存関係の管理を行うファイルを作成。
go mod init sample # sample はモジュール名
  • main.goファイルの作成
    アプリケーションのエントリーポイントとなるファイルを作成。
touch main.go

3. Goの各種パッケージをダウンロード

  • Echo
  • Gorm
  • Gormの/MySQLドライバー
go get -u github.com/labstack/echo/v4 gorm.io/gorm gorm.io/driver/mysql 
余談:go get/install/mod downloadの違い

※間違っていたらご指摘ください。

  • get
    指定されたパッケージとその依存関係をgo.modファイルに記載する+ダウンロードする。
  • install
    指定されたパッケージのバイナリをインストールする。airとか、開発環境に必要だけどアプリ内で使用するパッケージとしてはいらないものはこれを使う感じが良いかも。(go.modにも依存関係が記録されないため)
  • mod download
    go.modに記載されたパッケージをダウンロードする。実際go runで実行時にダウンロード、インストールは実行されるけど、事前にダウンロードしておきたい場合に使う。

4. Airの設定ファイルを配置
Airのgithubリポジトリからair_example.tomlをコピーしてappディレクトリに.air.tomlにrenameして配置し、内容を好みに応じて修正する。そのままでも良い。

修正内容
.air.toml
# アプリのルート
root = "."
# ビルド結果の保管場所
tmp_dir = "tmp"

[build]
cmd = "go build -o ./tmp/main ./main.go"
bin = "tmp/main"
# 検知の対象とする拡張子
include_ext = ["go", "tpl", "tmpl", "html"]
# 検知の対象外とするファイル
exclude_file = []
# 検知の対象外とするディレクトリ
exclude_dir = ["assets", "tmp"]
# 検知の対象外とする拡張子
exclude_regex = ["_test\\.go"]
# 変更されていないファイルに対するリロード有無
exclude_unchanged = true
# 参照先のファイルが変更された際のリロード有無
follow_symlink = true
# tmp_dirに配置されるビルド結果のログファイル
log = "air.log"
# fsnotifyを使わずにポーリングによる変更検知を行うか否か(Windows環境の場合はtrueにする必要あり。)
poll = false
# ポーリング時のインターバル(ms)
poll_interval = 0
# ファイル変更検知からビルドまでの遅延(ms)
delay = 0
# ファイル監視イベントの処理中にエラーが発生した場合、監視を止めるか否か
stop_on_error = true
# プロセスを修了する前にシグナルを送信するか否か
send_interrupt = false
# 実行中のアプリのダウンタイム
kill_delay = 500 # nanosecond
# 再実行の設定
rerun = false
# 再実行時の遅延(ms)
rerun_delay = 500

[log]
# ログへの時刻出力有無
time = false
# メインのみのログ設定
main_only = false

[color]
# ログのカラー設定
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# 終了時にビルド先のディレクトリを削除するか否か
clean_on_exit = true

[screen]
# リビルド時のクリア設定
clear_on_rebuild = true
# 自動スクロール設定
keep_scroll = true

5. envディレクトリにapi.envファイルを用意
データベース接続に必要な環境変数を用意する。

env/api.env
DB_USER=root
DB_PASSWORD=root
DB_NAME=sample

【現時点でのディレクトリ】

.
├── app
│   ├── .air.toml 【New】 
│   ├── go.mod    【New】
│   ├── go.sum    【New】
│   └── main.go   【New】
├── db
│   ├── initdb.d
│   │     └── user.sql      
│   └── conf.d
│   │     └── custom.cnf
├── env
│   ├── api.env 【New】 
│   └── db.env
├── docker-compose.yml
└── Dockerfile

手順4: API作成

1. db接続用のモジュール作成

app/db/db.go
package db

import (
	"fmt"
	"os"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var DB *gorm.DB

func Init() {
    // api.envに定義したDB関係の環境変数を取得
	dbUser := os.Getenv("DB_USER")
	dbPassword := os.Getenv("DB_PASSWORD")
	dbName := os.Getenv("DB_NAME")
        // tcp()の中にdocker-composeで定義したDB用コンテナのサービス名を入れれば、
        // 自動的にホストとポートを読み取ってくれる
	dsn := fmt.Sprintf(
		"%s:%s@tcp(db)/%s?charset=utf8mb4&parseTime=true&loc=Local",
		dbUser,
		dbPassword,
		dbName,
	)
	var err error
	DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})

	if err != nil {
		panic("Could not connect to database.")
	}
}

2. modelsモジュールを作成
データモデルを定義するモジュールを作成する。

app/models/user.go
package models

import (
	"sample/db"
	"time"

	"github.com/labstack/echo/v4"
)

type User struct {
// mysqlのカラム名をタグとして記載する
	UserId    string    `json:"userId" mysql:"user_id"`
	UserName  string    `json:"userName" mysql:"user_name"`
	Age       uint8     `json:"age" mysql:"age"`
	Gender    string    `json:"gender" mysql:"gender"`
	UpdatedAt time.Time `json:"updatedAt" mysql:"updated_at"`
	CreatedAt time.Time `json:"createdAt" mysql:"created_at"`
}
// 全ユーザー取得処理
func GetAllUsers() ([]User, error) {
	users := []User{}
	if db.DB.Find(&users).Error != nil {
		return nil, echo.ErrNotFound
	}
	return users, nil
}
// ユーザー取得処理(id)
func GetUserById(id string) (*User, error) {
	user := User{}
	if db.DB.Where("user_id = ?", id).First(&user).Error != nil {
		return nil, echo.ErrNotFound
	}
	return &user, nil
}

3. routesモジュールを作成
ルーティングとハンドラ用のモジュールを作成する。

app/routes/routes.go
package routes

import (
	"github.com/labstack/echo/v4"
)

func RegisterRoutes(server *echo.Echo) {
	server.GET("/user", getAllUsers)
	server.GET("/user/:userId", getUserById)
}
app/routes/user.go
package routes

import (
	"net/http"
	"sample/models"

	"github.com/labstack/echo/v4"
)

func getAllUsers(context echo.Context) error {
	users, err := models.GetAllUsers()
        // ※注意:エラーハンドリングはテキトーです
	if err != nil {
		return context.JSON(http.StatusInternalServerError, "ユーザーを取得できませんでした。")
	}
	return context.JSON(http.StatusOK, users)
}

func getUserById(context echo.Context) error {
	userId := context.Param("userId")
	user, err := models.GetUserById(userId)
        // ※注意:エラーハンドリングはテキトーです
	if err != nil {
		return context.JSON(http.StatusInternalServerError, "ユーザーを取得できませんでした。")
	}
	return context.JSON(http.StatusOK, user)
}

4. main.goにてDB初期化処理とサーバー実行処理を記載

app/main.go
package main

import (
	"sample/db"
	"sample/routes"

	"github.com/labstack/echo/v4"
)

func main() {
	// DB接続
	db.Init()

	server := echo.New()
	routes.RegisterRoutes(server)

	server.Logger.Fatal(server.Start(":5050"))
}

以上で完成です。

実行してみる

コンテナ起動

docker-compose up -d

あとはブラウザにlocalhost:5050/userと入力してデータが返ってきていればOK。

参考

https://hub.docker.com/_/mysql
https://gorm.io/ja_JP/docs/connecting_to_the_database.html
https://echo.labstack.com/docs/quick-start
https://github.com/cosmtrek/air

Discussion