go開発環境をDockerで作成する③

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

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

## 開発環境の構成

開発環境 `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レポジトリ
        └── app/                  ★ go-blogアプリのソースコードルートディレクトリ
            ├── entrypoints/
            │   └── production/
            │       └── main.go   ★ go-blogアプリのmainパッケージファイル
            ├── go.mod
            └── go.sum
~~~

やりたいことは下記の通り。
- go-blogコマンドを`docker-compose build`でイメージ作成時にビルドしたい。
    - 別レポジトリ管理のgo-blogソースコードをgo-envレポジトリ管理下のDockerfile.blog.devでビルドする。
- go-blogコマンドを`docker-compose up`でコンテナ起動時に自動起動したい。
    - go-blogコマンドはrootアカウントではなく、システムアカウントで起動する。
- コンテナ起動時に起動したgo-blogアプリにホストPCからアクセスしたい。
    - コンテナ上で起動したgo-blogは8080ポートで受け付けるので、ホストのlocalhost:8888でアクセスできるようにポートフォワードする。

## blogサービスのコンテナ内ディレクトリ構成

blogコンテナ内のディレクトリ構成（抜粋）は下記の通りとする。

~~~text: コンテナ内のディレクトリ構成(関係するものの抜粋)
/
├── dist/              ★ go-blogアプリを起動するディレクトリ
│   └── go-blog
├── home/ 
│   └── gouser/        ★ コンテナ起動時にホストPCのディレクトリをマウント(イメージビルド時には存在しない)
├── src/ 
│   └── go-blog/       ★ コンテナ起動時にホストPCのディレクトリをマウント(イメージビルド時には存在しない)
│       └── app/       ★ コンテナ動作時のgo-blogのソースディレクトリ
└── tmp/
    ├── app/           ★ イメージビルド時のgo-blogのソースディレクトリ(イメージビルドの最後で削除)
    └── go-cache/      ★ go build時に作成されるキャッシュを格納するディレクトリ
~~~

## 構成ファイルの変更
変更するのは、`Dockerfile.blog.dev`と`docker-compose.dev.yml`の2ファイル。

### Dockerfile.blog.devの変更

go-blogコマンドの格納/実行ディレクトリ、go build時のキャッシュディレクトリをシステムアカウント用に作成、go-blogコマンドをイメージビルド時にコンパイルし、コンテナ起動時にgo-blogコマンドを実行するように変更。

~~~dockerfile: Dockerfile.blog.dev
FROM golang:1.18.3-buster

ARG USER_NAME=$USER_NAME
ARG USER_ID=$USER_ID
ARG GROUP_NAME=$GROUP_NAME
ARG GROUP_ID=$GROUP_ID
ARG TZ=$TZ
ENV TZ=$TZ

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

# システムアカウントの作成(ホームディレクトリは作成しない)
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

# ------ これより下が書き変わった部分 ------

# ビルドしたgo-buildコマンドの格納先ディレクトリ作成(root実行)
# コンテナ上でのgo-blogコマンド実行ディレクトリでもある
# ディレクトリのオーナーはシステムアカウント
RUN mkdir /dist && \
    chown $USER_NAME.$GROUP_NAME /dist && \
    chmod 750 /dist

# go buildに必要なキャッシュディレクトリの作成(root実行)
# ディレクトリのオーナーはシステムアカウント
RUN mkdir /tmp/go-cache && \
    chown $USER_NAME.$GROUP_NAME /tmp/go-cache && \
    chmod 755 /tmp/go-cache
# go buildに必要なキャッシュディレクトリの設定(root実行)
ENV GOCACHE=/tmp/go-cache

# ホスト環境のソースコードをテンポラリの/tmp/appにコピー(root実行)
COPY ./src/go-blog/app /tmp/app
# ソースコードディレクトリのオーナー変更(root実行)
# ビルド時にgo.mod、go.sumを書き換えるため、オーナー変更が必要
RUN chown -R $USER_NAME.$GROUP_NAME /tmp/app

# システムアカウントに切り替え
USER $USER_NAME

# ワークディレクトリをソースコードディレクトリに切り替え(システムアカウント実行)
WORKDIR /tmp/app
# ソースコードをコンパイルし、/distディレクトリgo-blogコマンドを作成(システムアカウント実行)
RUN go mod tidy
RUN go build -o /dist/go-blog entrypoints/production/main.go

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

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

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

### docker-compose.dev.ymlの変更

コンテナ起動時にbashを起動せずDockerfileのCMDを実行する、ホストのポート8888をコンテナの8080ポートにポートフォワードするように変更する。

~~~diff: 
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 38dffdc..3a7b975 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -1,42 +1,44 @@
 version: "3"
 services:
   blog:
     build:
       context: .
       dockerfile: ./env/Dockerfile.blog.dev
       args:
         - USER_NAME=$USER_NAME
         - USER_ID=$USER_ID
         - GROUP_NAME=$GROUP_NAME
         - GROUP_ID=$GROUP_ID
         - TZ=$TZ
     image: dev-env-blog-image
     container_name: dev-env-blog
-    command: bash
+    # command: bash    # DockerfileのCMDでgo-blogを起動するように変更したためコメントアウト
     tty: true
     volumes:
       - ./src/golang:/home/$USER_NAME
       - ./src/go-blog:/src/go-blog
     env_file: .env.dev
     environment:
       - GO111MODULE=on
+    ports:
+      - 8888:8080      # ホストPCのポート8888への通信をコンテナの8080ポートにフォワード
     networks:
       dev-env-link:
         ipv4_address: $NETWORK_BASE.20

   golang:
~~~

## 動作確認

Makefaileは下記のような設定。
~~~makefile: Makefile(抜粋)
# 変数 --------------------------
DC=docker-compose -f docker-compose.dev.yml

# 環境 --------------------------
# top
env-top:
	$(DC) top

# blog環境 ---------------------
# blog環境 imageビルド
blog-build:
	$(DC) build --force blog

# blog環境 container実行(deamon)
blog-up:
	$(DC) up -d blog

# blog環境 リビルド、container実行(コンソール出力)
blog-run:
	$(DC) up --build blog

# blog環境 container実行、同一コンテナでbash実行
blog-console: blog-up
	$(DC) exec blog bash

# blog環境 container停止
blog-stop:
	-$(DC) stop blog

# blog環境 container停止/破棄
blog-down: blog-stop
	-$(DC) rm -f blog

# blog環境 container停止/破棄、image破棄
blog-rmi: blog-down
	-$(D) rmi -f dev-env-blog-image
~~~

### イメージビルドとコンテナ起動

イメージビルドとコンテナ起動
~~~shell-session: イメージビルドとコンテナ起動
$ make blog-build && make blog-run
docker-compose -f docker-compose.dev.yml build --force blog
Building blog
[+] Building 0.2s (19/19) FINISHED
...
 => => naming to docker.io/library/dev-env-blog-image                                                              0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
docker-compose -f docker-compose.dev.yml up blog
Starting dev-env-blog ... done
Attaching to dev-env-blog
dev-env-blog | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attache
d.
dev-env-blog |
dev-env-blog | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
dev-env-blog |  - using env:    export GIN_MODE=release
dev-env-blog |  - using code:   gin.SetMode(gin.ReleaseMode)
dev-env-blog |
dev-env-blog | [GIN-debug] GET    /api/health-check         --> github.com/jun00rbiter/go-blog/controllers.GetHealthChec
k (3 handlers)
dev-env-blog | [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
dev-env-blog | Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
dev-env-blog | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
dev-env-blog | [GIN-debug] Listening and serving HTTP on :8080
~~~

別コンソールでコンテナを確認すると`go-blog`プロセスがUID:1000(gouser)で動作しているのが分かる。
また、`docker ps`コマンドでコンテナ状態を確認し、ホストPCポート8888からコンテナポート8080にポートフォワードしていることを確認。

~~~shell-session: dockerコマンドでのコンテナ動作確認
$ make env-top
docker-compose -f docker-compose.dev.yml top
dev-env-blog
UID     PID    PPID    C   STIME   TTY     TIME        CMD
-------------------------------------------------------------
1000   15965   15945   2   15:54   ?     00:00:00   ./go-blog

$ docker ps
CONTAINER ID   IMAGE                COMMAND       CREATED         STATUS         PORTS                    NAMES
a3c5331c38ae   dev-env-blog-image   "./go-blog"   4 minutes ago   Up 4 minutes   0.0.0.0:8888->8080/tcp   dev-env-blog
~~~

### 動作確認

ホストPCのコンソールから`localhost:8888`に対してhealth-checkのAPIを叩くと、応答が帰ってくることを確認。
もちろん、Chrome等のブラウザからアクセスしても応答が帰ってくる。

~~~shell-session: ホストPCからの応答確認
$ curl -v http://localhost:8888/api/health-check | jq
...
> GET /api/health-check HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.74.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Tue, 07 Jun 2022 16:00:35 GMT
< Content-Length: 15
<
{ [15 bytes data]
100    15  100    15    0     0   1363      0 --:--:-- --:--:-- --:--:--  1500
* Connection #0 to host localhost left intact
{
  "status": "OK"
}
~~~

もちろん、今まで通り別の`golang`コンテナのコンソールからも通信は可能。

~~~shell-session: 別コンテナ`golang`からの応答確認
gouser@c25cfea93c64:~$ curl http://blog:8080/api/health-check | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    15  100    15    0     0   7500      0 --:--:-- --:--:-- --:--:--  7500
{
  "status": "OK"
}
~~~

## ソースコード修正と再起動

go-blogの動作中に、ソースコードを書き換えても、起動中のコンテナには反映されない。
コンテナを停止し、イメージビルド、コンテナ再起動が必要。

~~~
$ make blog-run                                                 ★ ビルド/コンテナ起動
docker-compose -f docker-compose.dev.yml build --force blog
Building blog
...
dev-env-blog | [GIN-debug] Listening and serving HTTP on :8080
Gracefully stopping... (press Ctrl+C again to force)            ★ Ctrl+Cで強制終了
Stopping dev-env-blog ... done

$ make blog-rmi && make blog-run                                ★ イメージ破棄およびビルド/コンテナ起動
docker-compose -f docker-compose.dev.yml stop blog
docker-compose -f docker-compose.dev.yml rm -f blog
Going to remove dev-env-blog
Removing dev-env-blog ... done
docker rmi -f dev-env-blog-image
Untagged: dev-env-blog-image:latest
Deleted: sha256:4438573e4a774f522ec7d8425ee02aa399126c5fb35401f6d0c31276f29ee43a
docker-compose -f docker-compose.dev.yml up --build blog
Building blog
...
dev-env-blog | [GIN-debug] Listening and serving HTTP on :8080
~~~

ひとまず、go言語でWebアプリを構築するためのgo開発環境をDockerで作成することができた。
現状の環境は下記の図の通り。ポートフォワード設定をしたことで、ホストPCの8888ポートに接続することで、blogコンテナの8080ポートにアクセスできる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703014/037dc6a6-51e3-c143-53d0-b92a99459189.png)

課題としては、ソースコードを変更しても即座にコンテナに反映されない点。
この課題については、ホットリロードのプログラムを用いることで解決できる。

ここまでの一式
- [go-env(tag:qiita_20220608)](https://github.com/jun00rbiter/go-env/tree/qiita_20220608)
- [go-blog(tag:qiita_20220606_fix1)](https://github.com/jun00rbiter/go-blog/tree/qiita_20220606_fix1)