go開発環境をDockerで作成する④ － realizeでホットリロード

# 最終目的
- go言語でWebアプリを構築するためのgo開発環境をDockerで作成する。

# 目標
- [Docker上にコンソール立ち上げ環境を作る。](https://qiita.com/eigetsu/items/555efb6f1fec0425d35f)
- [フレームワークginを使った簡単なREST APIサーバーを立ち上げる。](https://qiita.com/eigetsu/items/5482149b4f5cf7874698)
- [イメージビルド時にプログラムをコンパイルしコンテナ起動と同時にホストPCからAPIにアクセスできるようにする。](https://qiita.com/eigetsu/items/a3eeffa3df2bac413dde)
- realizeでホットリロードできるようにする。**←今ここ**

# やること
- realizeをイメージビルド時にインストール
- コンテナ立ち上げ時にrealizeでgo-blogをホットリロード

## 開発環境の構成

開発環境 go-env の構成は下記の通り。

~~~text: 開発環境
go-env/                           ★ 開発環境構成go-envはGitHubのレポジトリ
  ├── docker-compose.dev.yml
  ├── env/
  │   └── Dockerfile.blog.dev     ★ go-blogのDockerイメージを作成するためのDockerfile
  ├── Makefile
  └── src/
    └── go-blog/                  ★ go-blogディレクトリはgo-envとは別のGitHubレポジトリ
        ├── .realize.yaml         ★ realizeの設定ファイル
        └── app/
            ├── entrypoints/
            │   └── production/
            │       └── main.go
            ├── go.mod
            └── go.sum
~~~

## realizeをイメージビルド時にインストール

ホットリロードとは、ソースコードが変更されたことを検知し、自動的にリビルドおよびプログラムを再起動すること。便利。
realizeとは、go言語で書かれたホットリロードアプリ。

https://github.com/oxequa/realize

> realizeは2年近く更新されておらず、最近ではairが使われているようだが、airは使いにくい。
> いつまで使えるかはわからないが、まだ手放せない...

### Dockerfile.blog.devの修正(realizeのイメージビルド時インストール)

下記の通り、root権限で動作する位置に、realizeをインストールするように定義を追記する。

> realizeの@latestはmodule modeでの`go install`でのインストールが現在(2022/06/12)できない。
> 一度、module modeをoffにして、realizeを`go get`でインストールする。
> なお、現在、go1.15以下では`go get`でのインストールもエラーとなる。


~~~diff: realizeのインストール
--- a/env/Dockerfile.blog.dev
+++ b/env/Dockerfile.blog.dev
@@ -11,12 +11,18 @@ RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime

 ENV DEBIAN_FRONTEND=noninteractive
 RUN apt update && apt install -y sudo

 RUN apt install -y jq tree diffutils nano vim
 
+# realizeのインストール
+# module modeではインストールできない。
+ENV DEBIAN_FRONTEND=noninteractive
+RUN GO111MODULE=off go get github.com/oxequa/realize
+ENV GO111MODULE=on
+
 # システムアカウントの作成(ホームディレクトリは作成しない)
 RUN groupadd -g $GROUP_ID $GROUP_NAME && \
     useradd -s /bin/bash -u $USER_ID -g $GROUP_ID -G sudo $USER_NAME -r -d /home/$USER_NAME -M && \
     echo "$USER_NAME   ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
~~~

## realizeの設定

realizeの設定は.realize.yamlに記述する。コンテナ内のディレクトリ構成を意識して、設定を記述する。

コンテナ内のディレクトリ構成は下記の通り。
~~~text: コンテナ内のディレクトリ構成(抜粋)
/
├── dist/                                  ★ go-blogアプリを起動するディレクトリ
│   └── go-blog                            ★ webアプリケーションの実行バイナリ
└── src/ 
    └── go-blog/                           ★ コンテナ起動時にホストPCのディレクトリをマウント
        ├── .realize.yaml                  ★ realizeの設定ファイル
        └── app/
            ├── go.mod
            └── entrypoints/production
                └── main.go                ★ mainパッケージのエントリープログラム
~~~

### realizeでやりたいこと

/src/go-blog/appディレクトリ配下のgoソースファイルが変更になったとき...
- `go mod tidy`を実行したい
- entrypoints/production/main.goから/dist/go-blogコマンドをビルドしたい
- /dist/go-blogコマンドを再起動したい

> airを使わない理由
> - Mac/Linuxではファイルの変更を検知できるが、WSL上ではある条件下でファイル変更が検知できない。
>   - `/mnt/<drive>`配下のファイルはinotifyが働かないため。
> - `go mod tidy`をどうやったら実行できるかわからない...

### realizeの設定ファイルを作成

`.realize.yaml`ファイルを作成。
> WindowsでDockerを動かしているときは注意。

~~~yaml: .realize.yaml
settings:
  # WindowsのWSLでdockerを動作させている場合で、コードが/mnt/<drive>に配置されている場合、
  # inotifyが働かないので、ポーリングで動作するようにforce:trueに変更する
  legacy:
    force: false      # ★ Windowsではtrueにする ★
    interval: 1s      # ポーリング間隔になるので短くするとPCに負荷がかかる

# サーバー設定(状態をWebで確認できる)
server:
  open: false
  port: 5002
  host: 0.0.0.0

# ホットロード設定
schema:
  - name: go-blog     # プロジェクト名(好きな名前で)
    path: app         # 監視対象パス(.realize.yamlがあるディレクトリからの相対パス)

    # コマンド実行設定
    commands:
      build:
        status: true  # ビルドを再起動前に実施する
        # ビルド時のコマンド(/dist/go-blogにビルド)
        method: go build -o /dist/go-blog entrypoints/production/main.go
      run:
        status: true  # コマンド再起動する
        # 再起動時のコマンド
        method: /dist/go-blog

    # ファイル監視設定
    watcher:
      extensions:    # 監視対象ファイルの拡張子
        - go
      paths:         # 監視対象ディレクトリ(schema/pathからの相対パスで指定)
        - .
      scripts:
        # go mod tidyを実施する
        - command: go mod tidy
          type: before     # ビルドの前
          path: app        # 実行ディレクトリ(.realize.yamlがあるディレクトリからの相対パス)
          output: true
      ignored_paths:
        - .git
        - .realize
~~~

## コンテナ起動時にrealizeを起動させる

### Dockerfile.blog.devの修正(realizeを立ち上げるように変更)

コンテナ起動時にrealizeを起動するように Dockerfile.blog.dev を修正する。

~~~dockerfile
# ワークディレクトリを /dist に切り替え(システムアカウント実行)
WORKDIR /dist

# テンポラリの/tmp/appディレクトリを削除(システムアカウント実行)
RUN rm -rf /tmp/app

# # コンテナ起動時にgo-blogコマンドが実行されるように設定 --------------
# CMD ["./go-blog"]

# realizeを使ったホットリロード --------------------------------------
WORKDIR /src/go-blog
CMD [ "realize", "start", "--run", "--build" ]
# CMD [ "realize", "start", "--run", "--build", "--server" ]  # realizeサーバーを建てる場合
~~~

## 起動確認

コンテナを起動する。

~~~shell-session: コンテナ起動
$ docker-compose -f docker-compose.dev.yml up --build blog
Building blog
...
Recreating dev-env-blog ... done
Recreating dev-env-blog ... done
Attaching to dev-env-blog
dev-env-blog | [01:30:09][GO-BLOG] : Watching 4 file/s 8 folder/s
dev-env-blog | [01:30:09][GO-BLOG] : Command "go mod tidy"        ★ビルド前にgo mod tidyが走ってる
dev-env-blog | [01:30:09][GO-BLOG] : Build started                ★ビルド
dev-env-blog | [01:30:11][GO-BLOG] : Build completed in 1.971 s
dev-env-blog | [01:30:11][GO-BLOG] : Running..                    ★go-blogの実行
dev-env-blog | [01:30:11][GO-BLOG] : [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
...
dev-env-blog | [01:30:11][GO-BLOG] : [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
dev-env-blog | [01:30:11][GO-BLOG] : [GIN-debug] Listening and serving HTTP on :8080
~~~

コンテナで起動されているプロセスを確認。
realizeのプロセスは17504で、go-blogのプロセスは17623で親プロセスは17504。realizeがgo-blogを立ち上げていることがわかる。

~~~shell-session: コンテナで動作しているプロセス
docker-compose -f docker-compose.dev.yml top
dev-env-blog
UID     PID    PPID    C   STIME   TTY     TIME                 CMD
-------------------------------------------------------------------------------
1000   17504   17483   0   16:32   ?     00:00:00   realize start --run --build
1000   17623   17504   0   16:32   ?     00:00:00   /dist/go-blog
~~~

ソースコードを変更すると、
~~~
dev-env-blog | [01:35:16][GO-BLOG] : GO changed /src/go-blog/app/routers/api_router.go   ★変更検知
dev-env-blog | [01:35:16][GO-BLOG] : Command "go mod tidy"        ★ビルド前にgo mod tidyが走ってる
dev-env-blog | [01:35:16][GO-BLOG] : Build started                ★ビルド
dev-env-blog | [01:35:17][GO-BLOG] : Build completed in 0.818 s
dev-env-blog | [01:35:17][GO-BLOG] : Running..                    ★go-blogの実行
...
dev-env-blog | [01:35:17][GO-BLOG] : [GIN-debug] Listening and serving HTTP on :8080
~~~

再度、コンテナで起動されているプロセスを確認。
realizeのプロセスは17504で変わりなく、go-blogの親プロセスは17504で変わりがないが、go-blogのプロセスが変更になっている。realizeがgo-blogを立ち上げ直していることがわかる。
~~~
docker-compose -f docker-compose.dev.yml top
dev-env-blog
UID     PID    PPID    C   STIME   TTY     TIME                 CMD
-------------------------------------------------------------------------------
1000   17504   17483   0   16:32   ?     00:00:01   realize start --run --build
1000   17744   17504   0   16:35   ?     00:00:00   /dist/go-blog
~~~


## realizeサーバー(必要であれば)

コンテナをdeamonで起動するとgo-blogの動作が見えにくい。機能としてはログ確認しかできないが、簡易的にrealizeの状態をブラウザで確認することができる。
> 実際にはlogを出力するようにして確認できるようにするのが正当。

![realize-server.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703014/463883e2-fe8b-f3fd-cd61-759fbe97ac87.png)

### realizeサーバーを立ち上げるようにDockerfileを変更

コンテナ起動時にrealizeのサーバーも起動するように Dockerfile.blog.dev を修正する。

~~~dockerfile
# realizeを使ったホットリロード --------------------------------------
WORKDIR /src/go-blog
# CMD [ "realize", "start", "--run", "--build" ]
CMD [ "realize", "start", "--run", "--build", "--server" ]  # realizeサーバーを建てる場合
~~~

realizeサーバーは、.realize.yamlの設定どおりポート5002で待ち受けるので、ホストPCから接続できるように docker-compose.ymlにポートフォワード設定を追加する。

~~~diff
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -19,12 +19,13 @@ services:
       - ./src/go-blog:/src/go-blog
     env_file: .env.dev
     environment:
       - GO111MODULE=on
     ports:
       - 8888:8080
+      - 5002:5002      # realizeサーバー用のPortFoward設定
     networks:
       dev-env-link:
         ipv4_address: $NETWORK_BASE.20

   golang:
~~~

コンテナを立ち上げると、下記のようにサーバーが起動したことがわかる。

~~~
Recreating dev-env-blog ... done
Attaching to dev-env-blog
dev-env-blog | [01:41:02][REALIZE] : Started on 0.0.0.0:5002     ★realizeサーバー起動
dev-env-blog | ⇨ http server started on [::]:5002
dev-env-blog | [01:41:02][GO-BLOG] : Watching 4 file/s 8 folder/s
dev-env-blog | [01:41:03][GO-BLOG] : Command "go mod tidy"
...
dev-env-blog | [01:41:04][GO-BLOG] : [GIN-debug] Listening and serving HTTP on :8080   ★go-blogも起動
~~~

後は、ホストPC上のブラウザで、`http://localhost:5002`にアクセスするとrealizeの状態を確認できる。

## まとめ

ここまでで、go言語でWebアプリを構築し、プログラム変更をすぐに確認できるgo開発環境をDockerで作成できた。
現状の環境は下記の図の通り。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703014/328023fd-4dfd-ea09-e0fd-e44b52dab67b.png)

ここまでの一式

* [go-env(tag:qiita_20220612)](https://github.com/jun00rbiter/go-env/tree/qiita_20220612)
* [go-blog(tag:qiita_20220612)](https://github.com/jun00rbiter/go-blog/tree/qiita_20220612)