この章で行うこと

  • プロジェクトの初期化
  • Dockerで環境構築
  • Makefileでタスク自動化
  • GithubActionsでCI自動化

プロジェクトの初期化

Githubリポジトリを作成

本プロジェクトのコードを管理&CIを行うために、gituhubにリポジトリを作成することから始めましょう。ブラウザでリポジトリを作成してからローカル環境と繋げる方法もありますが、ghコマンドを利用するとローカル環境から直接リモートリポジトリに繋ぐことができるので便利です。ghコマンドが利用できない場合は以下に従ってインストール〜認証してください。

https://docs.github.com/ja/github-cli/github-cli/quickstart

セットアップできたら、

gh repo create

とターミナルにコマンドを打ってリモートリポジトリを作成しましょう。プロジェクト名などはお任せします!以下のように進めてみてください。 適宜githubのリポジトリを確認してみてください。

Goプロジェクトを始める

この段階で、設定したプロジェクト名がついたディレクトリがローカルにも作成されているはずですので、エディタで開いてください。そして、/appディレクトリを作成したのちに/appディレクトリで

go mod init github.com/[任意の名前]/app

とコマンドを打ってください。

ちなみに、筆者はgo mod init github.com/kakkky/appとしました。そのため、ハンズオンの中で示すコードでは、import先にgithub.com/kakkky/app/xxxx/xxxxといったパッケージ名が羅列されているかと思います。実装の際はkakkkyの部分を書き換える必要があると把握しておいてください(エディタの補完でうまくやってくれると思いますが)。

コマンドを打つと、go.modファイルが作成されます。これによってGo Modulesで依存関係を管理できるようになります。

また、次のDockerfileで正しくビルドするためにgo.sumも手動で作成しておいてください。本来は、go getで自動作成・編集されるものですが、dockerによる開発環境の構築のために必要なので行っています。

中には何も書かずに空のままにしておいてください。 Go Modulesについては以下を参照してください(そこまで詳しく知らなくても開発は進められます)

https://go.dev/blog/using-go-modules https://qiita.com/TakanoriVega/items/6d7210147c289b45298a#go-mod-init%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B https://zenn.dev/optimisuke/articles/105feac3f8e726830f8c

Dockerで環境構築

docker,docker-composeを使ってカンタンに環境を構築しましょう。 dockerについてあまりよく知らない方は以下の本がおすすめです。

https://zenn.dev/suzuki_hoge/books/2022-03-docker-practice-8ae36c33424b59

今回は、MySQLとRedisを使用するので、コンテナとしてその二つを用意します。また、それら二つを利用する、APIの「本体(=golangの環境)」とも言えるappコンテナを作成します。

appコンテナ

Dockerfileでappコンテナイメージを作成

まずはgolangのイメージを用いて、カスタムしたイメージを作成するためのDockerfileを記述していきます。現在は/appディレクトリにいるかと思われますが、移動してルートディレクトリにDockerfile.appを作成してください。

Dockerfile.appのコード
# Dockerfile.app
FROM golang:1.23.1

WORKDIR /src

COPY ./app/go.mod ./app/go.mod
COPY ./app/go.sum ./app/go.sum
WORKDIR /src/app
# github.com/[あなたの名前]/appモジュールの依存関係を全てコンテナ内にダウンロード
RUN go mod download
# srcディレクトリ配下にローカルのソースコードをコピー
COPY ./ ./

WORKDIR /src/app

# 必要なツールをインストール

RUN go install github.com/air-verse/air@latest
RUN go install go.uber.org/mock/[email protected]
RUN go install github.com/sqlc-dev/sqlc/cmd/[email protected]
RUN go install github.com/swaggo/swag/cmd/swag@latest
RUN go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

# airを起動
CMD ["air"]

ポイントは、

  • ローカルのapp/go.mod、app/go.sumをコピーしてgo mod downloadをしている
    • 最初に依存関係だけコンテナにコピーし、ローカルの依存関係を反映させています(まだプロジェクトを初期化しただけで依存関係はありませんが、再ビルドする際などに有効です)
  • その後、ローカルのディレクトリを全てコンテナ内にコピー
  • 主にCLIツールをコンテナ内にインストール
  • airを起動

となっています。CLIツールはローカルでインストールして使う、でも全く問題ないのですが、開発環境は全てコンテナに閉じ込めたい&dockerに明記したい感が筆者にはありまして、このような形を取っています。

ただデメリットはあって、Dockerfileイメージのビルド時間がかなり伸びてしまう問題があります。特に、今後使用するsqlcのインストールは1分弱かかる場合があります。

開発環境をDockerで全て完結させる&カンタンに配布するならこの方法はメリットがありそうですが、ビルドが遅くなるのは超難点ですね。

インストールしているそれぞれのツールについては、後々触れていきます。

docker-compose

appコンテナ、dbコンテナ、kvsコンテナを作成し、appコンテナがdb、kvsを利用できるようにする...そのためにはdocker-composeが必要です。以下にコードを示します。

compose.ymlのコード
# compose.yml
services:
  app:
    container_name: todo-api
    build:
      dockerfile: ./Dockerfile.app
      context: .
    volumes:
      - type: bind
        source: ./
        target: /src
    ports:
      - "8080:8080"
    tty: true
    environment:
      PORT: ":8080"
      TODO_DB_HOST: db
      TODO_DB_PORT: 3306
      TODO_DB_USER: user
      TODO_DB_PASSWORD: pswd
      TODO_DB_NAME: todo-db
      TODO_KVS_HOST: kvs
      TODO_KVS_PORT: 6379
    depends_on:
      - db
      - kvs
  db:
    container_name: todo-db
    image: mysql:8.0.40-debian
    platform: linux/amd64
    volumes:
      - type: volume
        source: db-store
        target: /var/lib/mysql
      - type: bind
        source: ./app/infrastructure/db/mysql
        target: /etc/mysql/conf.d:cached
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: todo-db
      MYSQL_USER: user
      MYSQL_PASSWORD: pswd
      MYSQL_ALLOW_EMPTY_PASSWORD: true
    tty: true
  kvs:
    container_name: todo-kvs
    image: redis:latest
    ports:
      - "6379:6379"
    volumes:
      - type: volume
        source: kvs-store
        target: /data
    tty: true
volumes:
  db-store:
  kvs-store:

appコンテナには、先に作成したカスタムDockerイメージを使用することを記述しています。また、先述したようにvolumes:のあたりにbindという記述がありますね。これは、ローカルのカレントディレクトリ配下のコードを全てsrc配下にマウントするということを示しています。Dockerfile.appでsrcディレクトリを作成していましたね、それです。

また、dbコンテナに関して、以下の記述があります。

# compose.yml
      - type: bind
        source: ./app/infrastructure/db/mysql
        target: /etc/mysql/conf.d:cached

これは、MySQLのデフォルト設定をオーバーライドするためのものです。従って、app/infrastructure/db/mysql/conf.d/my.confを作成し、以下の記述をしておいてください。

# my.conf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

[client]
default-character-set=utf8mb4

その他の記述はどれも比較的基本的なものなので、composeファイルについて詳しく理解したい方は、別途お調べください🙏。特に理解していなくても、開発は進めることができます。

最後に、Dockerfile.appをビルド&コンテナ起動のために、以下のコマンドを打ちましょう。 airなどが立ち上がっていれば成功です。

docker-compose up --build

以下のコマンドを打って一旦コンテナを削除しておきましょう。

docker-compose down

ホットリロードツール air

golangでプログラムを実行するgo runは、内部でビルドしたバイナリファイルを実行しているので、変更を反映したいときはその都度、go runしなければなりません。テストコードでしかコードをほとんど動かさない、場合はこのままでも問題ないのですが、動作確認も開発中に行いたい場合は、変更のたびにいちいちコマンドを打つのは手間です。

そこで、ホットリロードツールAirを使用すれば、Airがプロジェクト全体を監視してくれて、コード変更時に自動でビルド〜実行を行ってくれるのです。

Dockerfile.app内では、ここでインストールしています。

# Dockerfile.app
RUN go install github.com/air-verse/air@latest

そして、最後に以下の部分で、airを起動しています。

# Dockerfile.app
# airを起動
CMD ["air"]

airを使用するためには、設定ファイルが必要です。以下のコマンドで設定ファイル.air.tomlを生成します。airがインストールされているのはコンテナ内なので、コンテナを一時的に立ち上げてコマンドを実行するdocker compose runを使用します。--rmをつけることで、実行後はコンテナが削除されます。

docker compose run --rm app air init

appディレクトリにファイルが自動で生成されたはずです。設定は特にいじりません。以下の部分だけ注目しておきます。

# .air.toml
[build]
  args_bin = []
  bin = "./tmp/main" #ビルドされたバイナリファイルが配置されるところ
  cmd = "go build -o ./tmp/main ." # ビルドコマンド

コンテナ内のワーキングディレクトリをsrc/appとしているので、airを実行するとローカルではapp配下にtmp/mainが配置されるようになります。

また、コードの変更を検出→ビルド&再起動についてですが、あくまでairが動いているのはコンテナ内です。ローカルのソースコードを編集してなぜコンテナ内のairがそれを察知する...?という疑問が生じますが、それはcompose.ymlで記述するバインドマウントによるものです。ローカルのディレクトリ・ファイルがコンテナ内にマウントされ、ローカルの変更はコンテナ内に即座に反映されるようになります。

ホットリロード、超便利です。

https://github.com/air-verse/air https://qiita.com/urakawa_jinsei/items/44b27d2c23d1c8bb68d7

ホットリロードを試してみる

app/main.goを作成し、以下の記述をしてください。

// app/main.go
package main

import "fmt"

func main() {
	fmt.Println("Hello,world")
}

次に、コンテナを起動させてみましょう。

docker compose up

goのプログラムが実行されていることを確認しましょう。

todo-api  | 
todo-api  |   __    _   ___  
todo-api  |  / /\  | | | |_) 
todo-api  | /_/--\ |_| |_| \_ v1.61.5, built with Go go1.23.1
todo-api  | 
todo-api  | watching .
todo-api  | watching infrastructure
todo-api  | watching infrastructure/db
todo-api  | watching infrastructure/db/mysql
todo-api  | watching infrastructure/db/mysql/conf.d
todo-api  | !exclude tmp
todo-api  | building...
todo-api  | running...
todo-api  | Hello,world //ここ

次に、好きなようにHello,worldの文字を編集してみてください。その後、ファイルを保存すると自動で再ビルド&再起動されていることがログで確認できるはずです。

todo-api  | main.go has changed
todo-api  | building...
todo-api  | running...
todo-api  | Hello,world!!!!!!!!!!! //!!!!!!!を追加してみた
todo-api  | Process Exit with Code 0

動作確認を行えるのは、エンドポイントを通してからなのでハンズオン内ではまだ先ですが、こんなのもあると知っておくと便利です!

確認が終わったら、適宜コンテナを削除してください。

docker-compose down

Makefileでタスク自動化

golangの開発では、複数のコマンドをMakefileに記載することで作業を効率化するケースが多いようです。以下にコードを記載します。現段階では使用しないものばかりですが、いくつかのタスクについては後に使用していきます。ルートディレクトリにMakefileを作成してください。

Makefileコード
# Makefile
help: # コマンド確認
	@echo "\033[32mAvailable targets:\033[0m"
	@grep "^[a-zA-Z\-]*:" Makefile | grep -v "grep" | sed -e 's/^/make /' | sed -e 's/://'


##############
### テスト ####
##############

# テスト処理の共通化
# パラメータ:
# - path=<テスト対象のパス> (デフォルト: ./...)
# - opts=<追加オプション> (デフォルト: なし)
# - tags=<ビルドタグ> (デフォルト: なし)
define tests
	$(if $(TEST_TAGS),\
		go test -v -timeout 10m -tags=$(TEST_TAGS) $(TEST_PATH) $(TEST_OPTIONS),\
		go test -v -timeout 10m $(TEST_PATH) $(TEST_OPTIONS)\
	)
endef

# appディレクトリの全体テスト
# コマンド例: $ make test-app opts="-run TestXxx"
test-app:
	$(eval TEST_PATH=./...)
	$(eval TEST_TAGS=$(tags))
	$(eval TEST_OPTIONS=${opts})
	@echo "Running all tests in app..."
	cd ./app && $(call tests)

# ドメイン層のテスト
# コマンド例: $ make test-domain path=./... opts="-run TestXxx"
test-domain:
	$(eval TEST_PATH=$(or $(path),./...))
	$(eval TEST_TAGS=$(tags))
	$(eval TEST_OPTIONS=${opts})
	@echo "Running tests in domain..."
	cd ./app/domain && $(call tests)

# ユースケースのテスト
# コマンド例: $ make test-usecase path=./... opts="-run TestXxx"
test-usecase:
	$(eval TEST_PATH=$(or $(path),./...))
	$(eval TEST_TAGS=$(tags))
	$(eval TEST_OPTIONS=${opts})
	@echo "Running tests in usecase..."
	cd ./app/application/usecase && $(call tests)

# リポジトリのテスト
# コマンド例: $ make test-repo path=./... opts="-run TestXxx"
test-repo:
	$(eval TEST_PATH=$(or $(path),./...))
	$(eval TEST_TAGS=$(tags))
	$(eval TEST_OPTIONS=${opts})
	@echo "Running tests in repository..."
	cd ./app/infrastructure/repository_test && $(call tests)

# クエリサービスのテスト
# コマンド例: $ make test-query path=./... opts="-run TestXxx"
test-query:
	$(eval TEST_PATH=$(or $(path),./...))
	$(eval TEST_TAGS=$(tags))
	$(eval TEST_OPTIONS=${opts})
	@echo "Running tests in queryservice..."
	cd ./app/infrastructure/queryservice_test && $(call tests)

# pkgディレクトリのテスト
# コマンド例: $ make test-pkg path=./... opts="-run TestXxx"
test-pkg:
	$(eval TEST_PATH=$(or $(path),./...))
	$(eval TEST_TAGS=$(tags))
	$(eval TEST_OPTIONS=${opts})
	@echo "Running tests in pkg..."
	cd ./pkg && $(call tests)

test-integration:
	$(eval TEST_PATH=$(or $(path),./...))
	$(eval TEST_TAGS=$(tags))
	$(eval TEST_OPTIONS=${opts})
	@echo "Running tests in integration"
	cd ./app/infrastructure/api_test/integration && $(call tests) -tags=integration

#####################
##### コンテナ操作 ####
#####################

# docker-composeにおけるDockerfileのビルド
build:
	@echo "Building Docker images..."
	docker compose build 

# docker compose up
up:
	@echo "Starting containers with docker-compose up..."
	docker compose up

# docker compose down
down:
	@echo "Stopping containers with docker-compose down..."
	docker compose down

# docker compose logs -f
logs:
	@echo "Fetching logs with docker-compose logs..."
	docker compose logs -f

# docker compose ps
ps:
	@echo "Viewing running containers with docker-compose ps..."
	docker compose ps

ls:
	@echo "Viewing running containers with docker-compose ls..."
	docker container ls

exec-db:
	docker compose exec db /bin/bash

exec-kvs:
	docker compose exec kvs /bin/bash

########################
### DBマイグレーション ####
########################

MIGRATE_PATH = infrastructure/db/migrations
DB_URL = mysql://user:pswd@tcp(db:3306)/todo-db?parseTime=true

# マイグレーションファイルを作成
# コマンド例: $ make migrate-create name=<migration-name>
migrate-create:
	$(eval NAME=$(or $(name),$(error "Error: Please specify a migration name using name=<name>")))
	@echo "Creating migration file..."
	cd app/ && migrate create -ext sql -dir $(MIGRATE_PATH) -seq $(NAME)

# マイグレーションを適用
# コマンド例: $ make migrate-up
migrate-up:
	@echo "Applying migrations..."
	docker compose run --rm app migrate --path $(MIGRATE_PATH) --database "$(DB_URL)" -verbose up

# マイグレーションをロールバック
# コマンド例: $ make migrate-down
migrate-down:
	@echo "Rolling back migrations..."
	docker compose run --rm app migrate --path $(MIGRATE_PATH) --database "$(DB_URL)" -verbose down

# マイグレーションのバージョンをセット
# migration-upが失敗した時は、最後のバージョンにforceしなおしてから再度upするようにする
# コマンド例: $ make migrate-force version=<version_number>
migrate-force:
	@echo "Forcing database migration version to $(version)..."
	docker compose run --rm app migrate --path $(MIGRATE_PATH) --database "$(DB_URL)" force $(version)


#############
### sqlc ####
#############

# sqlcでコードを生成
sqlc-gen:
	@echo "Generating query in sql by sqlc..."
	cd ./app/infrastructure/db/sqlc && sqlc generate

#############
## swagger ##
#############

# コメントをパースしてドキュメント生成
swag:
	@echo "Generating document by swagger..."
	cd ./app && swag fmt && swag init

###################
### パッケージ管理 ###
###################

# github.com/kakkky/appにおいてgo getする
# コマンド例: $ make get-app name=github.com/xxxx/xxx
get-app:
	cd ./app && go get $(name)

get-pkg:
	cd ./pkg && go get $(name)

###################
### go generate ###
###################
go-gen:
	cd ./app && go generate ./...
  • コンテナ操作
  • テスト実行
  • マイグレーション操作
  • sqlc(orm)のコード生成
  • swaggoのドキュメント生成
  • パッケージ管理(go get)

のコマンドを登録してあります。テスト実行に関しては、今後様々な層を実装するにあたって、特定の層(例えばドメイン層)のみのテストを行いたい時に、いちいちディレクトリを降りていって(または長々とパスを指定して)行うのは冗長で面倒なので、コマンドを登録してあります。

また、パッケージ管理(go get)に関しては、今回マルチモジュールで開発していくため、それぞれのモジュールでgo getするためにコマンドを用意しています。マルチモジュールについては2章で触れます。

コピペで構いません。次に進みましょう!

GithubActionsでCI自動化

GithubActionsのコードがよくわからん&知らん!な方は以下の記事がおすすめです。

https://zenn.dev/kiwichan101kg/articles/2d6850ff72bc98

ドメイン層、リポジトリ、ユースケース等々の大量の単体テスト、エンドポイントの統合テストと、開発が進むにつれてテスト項目は膨れ上がっていきます。コードを実装し、そのコードのテストコードを書いたならば、もちろん手元でテストを実行しますが、それ以外のテストもいちいちテストしていると面倒です。Makefileでタスク登録はしたものの、git pushする前にいちいちテストするのは面倒&テストし忘れる可能性もあるわけです。そこで便利なのが、Github Actionsを用いた継続的インテグレーションです。今回は、git pushでmainブランチにプッシュするたびにテストを走らせるようにします。

https://docs.github.com/ja/actions/about-github-actions/about-continuous-integration-with-github-actions

具体的には、CIワークフローをymlファイルに記述していきます。ルートディレクトリから.github/workflows/test.ymlを作成して、以下のコードを記述orコピペしてください。

workflowsのコードを見る
# .github/workflows/test.yml
name: test

on:
    # トリガー
    push:
        branches:
            - main
        # 非機能要件のファイルの変更は除外
        paths-ignore:
            - "app/docs/**"
            - "Dockerfile.**"
            - "compose.yml"
            - "air.toml"
jobs:
    # ユニットテスト
    unit-test:
        name: Run Unit Tests
        runs-on: ubuntu-latest
        steps:
            # チェックアウト
            - name: Checkout Code
              uses: actions/checkout@v4
            # Goのセットアップ
            - name: Go Setup
              uses: actions/setup-go@v5
              with:
                # goバージョンを参照するファイル
                go-version-file: app/go.mod
                # キャッシュ
                cache: true
                # 依存関係のキャッシュにおける追跡ファイル
                cache-dependency-path: app/go.sum
            # マルチモジュールを1つのワークスペースで管理
            - name: Setup Workspace
              run: go work init ./pkg ./app
            # appモジュールのテスト
            - name: Run App Test
              run: make test-app
            # pkgモジュールのテスト
            - name: Run Pkg Test
              run: make test-pkg
     # インテグレーションテスト
    integration-test:
        name: Run Integration Tests
        runs-on: ubuntu-latest
        steps:
            # チェックアウト
            - name: Checkout Code
              uses: actions/checkout@v4
            # Goのセットアップ
            - name: Go Setup
              uses: actions/setup-go@v5
              with:
                go-version-file: app/go.mod
                cache: true
                cache-dependency-path: app/go.sum
            # マルチモジュールを1つのワークスペースで管理
            - name: Setup Workspace
              run: go work init ./pkg ./app
            - name: Run Api Integration Test
              run: make test-integration

今回は、単体テストと結合テストしかワークフローに組み込んでいませんが、静的解析を行うケースも多いようです。興味のある方は、そちらも組み込んでみてもいいかもしれません。

https://qiita.com/Ixy_194/items/0e90dd74c5089d5db9b4 https://zenn.dev/sanpo_shiho/books/61bc1e1a30bf27

まだテスト項目がないのでメリットがわかりませんが、後半になるにつれて、テストを自動化していることの便利さを実感できると思います。git commit -mやgit pushするタイミングはご自身で適宜お願いいたします! ただ、現段階では/pkgディレクトリを作成していないせいで、CIが通りません!(stepの中でまずSetup Workspaceの段階でコケます)。

なのでまだpushは必要ありません。

現段階のディレクトリ

.
├── .github
│   └── workflows
│       └── test.yml
├── .gitignore
├── Dockerfile.app
├── Makefile
├── app
│   ├── .air.toml
│   ├── go.mod
│   ├── go.sum
│   ├── infrastructure
│   │   └── db
│   │       └── mysql
│   │           └── conf.d
│   │               └── my.conf
│   ├── main.go
│   └── tmp //airによって作成されたディレクトリ
│       ├── build-errors.log
│       └── main
└── compose.yml