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

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

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

## goの選定

https://go.dev/doc/devel/release

現在(2022/06/04)、最新のgoは1.18.3。

https://go.dev/doc/go1.18

1.17と1.18で大きな仕様変更(Genericsの導入、Fuzzingが標準機能)あり。今までのアップグレードに比べて最大の変更ではないか？
Genericsは1.19で正式になり、1.18では試験的実装とのこと。
安定している1.17にしようかと思っていたが、勉強のためなので最新の1.18にして、Genericsを使用し始めよう。
なので、1.18系とする。

## dockerイメージの選定

クラウド上にDeployすることを考えると、linux環境になる。destributionを考える。開発中にいろいろやりたいとするとubuntu。

https://hub.docker.com/_/golang?tab=tags&page=1&name=1.18

ubuntuのLTSベースとしたかったが、ubuntuベースのgolangイメージなし...
DebianはイメージがあるのでDebianにする。

https://wiki.debian.org/LTS

DebianのCurrent LTSはStretchだが、6/30で切れる...
次は、Debian 10 “Buster” で2024年6月まで。busterにしよう。

golangのバージョンは固定にして、必要なタイミングでマイナー(3桁目)をアップグレードしよう。
イメージは、`1.18.3-buster`とする。

> イメージ落としたらサイズがでかい。Deployの環境はalpineにするか別途検討。

## Dokerfileの作成

クラウドにDeployする時のDockerfileと開発環境用のDockerfileは分けたい。
`make`コマンドで立ち上げができるようにenvディレクトリにDockerfileをまとめる。

~~~sh
./
+ env/
  + Dockerfile.golang.dev
~~~

`env/Dockerfile.golang.dev`作成

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

試しにDockerイメージ作成、`bash`を立ち上げてみて、環境を確認。
~~~shell-session:Dokerfileの動作確認
$ docker build -f env/Dockerfile.golang.dev -t dev-env-golang:latest .
$ docker run --rm -it golang-dev bash

root@48a707dd66d1:/go# uname -a
Linux 48a707dd66d1 5.10.102.1-microsoft-standard-WSL2 #1 SMP Wed Mar 2 00:30:59 UTC 2022 x86_64 GNU/Linux

root@48a707dd66d1:/go# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

root@48a707dd66d1:/go# hostname
48a707dd66d1

root@48a707dd66d1:/go# ip -4 address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
15: eth0@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default  link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

root@48a707dd66d1:/go# ip -4 route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.2

root@48a707dd66d1:/go# go version
go version go1.18.3 linux/amd64
~~~

- goは1.18.3
- distributionはDebian 10 buster
- rootでアクセス
- ホスト名、IPアドレスは`docker run`ごとに変わる
- ネットワークIPはdocker設定のIP(作業者で異なる可能性)

rootアクセスはやめたい、ホスト環境のディレクトリをコンテナ環境のホームディレクトリに割り当てたい、ネットワークアドレスを指定したい、立ち上げをmakeコマンドで実施したい。

docker-composeとMakefileで制御することにする。

## docker-compose環境の作成

方針
- ネットワークアドレスを指定したい
- rootアクセスはやめて`gouser`ユーザーアクセス
- `src/golang`ディレクトリをコンテナ環境の`/home/gouser`に割り当てる
- 環境設定はファイルで設定(環境設定はコード管理しない)
- makeコマンドでいろいろ操作(長いコマンド打ちたくない)

構成は下記
~~~
./
+ env/
|   + Dockerfile.golang.dev
+ src/
|   + golang/
|      + <home dir>
+ docker-compose.dev.yml
+ Makefile
+ .env           # 環境変数(コード管理外)
+ .env.dev       # コンテナ実行時の共通環境変数
~~~

### ネットワークIPを固定化
dockerコマンドでコンテナが使用するネットワーク`dev-env-link`を作成する。
- コンテナからinternetにアクセスできる
- ネットワークアドレスは`192.168.10.0/24`
- gatewayは`192.168.10.254` (コンテナのIPは1から振りたい)
> docker-compose環境を複数立てたときに同一ネットワーク環境に同居させたいので、docker-compose.ymlでネットワークを定義しない。

~~~shell-session: docker networkの作成
$ docker network create --driver=bridge \
  --subnet=192.168.10.0/24 \
  --gateway=192.168.10.254 \
  env-link
~~~

ネットワーク削除は下記
~~~shell-session: docker networkの破棄
docker network remove dev-env-link
~~~

コマンド類は後でMakefileで定義する。

### docker-compose.dev.ymlの作成

開発環境の各種設定は`.env`ファイルで管理して、docker-compose.ymlで読み込み、Dockerイメージ作成時に継承させる。
`.env`ファイルはコード管理外にする。

~~~sh:.env
NETWORK_BASE=192.168.10
NETWORK_ADDR=192.168.10.0/24
NETWORK_GATEWAY_IP=192.168.10.254
TZ=Asia/Tokyo

USER_NAME=gouser

USER_ID=1000
GROUP_NAME=gouser
GROUP_ID=1000
~~~

コード管理してよい環境変数設定は`.env.dev`で管理する。

~~~sh:.env.dev
LANGUAGE=C
LANG=C.UTF-8
LC_ALL=C.UTF-8
~~~

docker-compose.dev.ymlを作成

~~~yml:docker-compose.dev.yml
version: "3"
services:
  golang:
    build:
      context: .
      dockerfile: ./env/Dockerfile.golang.dev
      args:
        - USER_NAME=$USER_NAME
        # - USER_PW=$USER_PW # いらない
        - USER_ID=$USER_ID
        - GROUP_NAME=$GROUP_NAME
        - GROUP_ID=$GROUP_ID
        - TZ=$TZ
    image: dev-env-golang-image
    container_name: dev-env-golang
    command: bash
    tty: true
    volumes:
      - ./src/golang:/home/$USER_NAME
    env_file: .env.dev
    environment:
      - GO111MODULE=on
    networks:
      dev-env-link:
        ipv4_address: $NETWORK_BASE.1    # .envのNETWORK_BASEと合わせて、コンテナのIPは192.168.10.1になる

networks:
  dev-env-link:
    external: true
~~~

### Dockerfaleの書き換え

docker-compose.ymlからの環境変数を使用して以下の設定を行うように書き換える。
- logaletimeの設定
- システムユーザーの作成

~~~dockerfile:Dockerfile.golang.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 USER_PW=$USER_PW # いらない
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 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:$USER_PW | chpasswd && \
    echo "$USER_NAME   ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

USER $USER_NAME
WORKDIR /home/$USER_NAME
~~~

docker-composeでbuild、up、execを実施。runで立ち上げないのは、stopでコンテナを止めたい、同一コンテナ上で複数のコンソールを立ち上げたいため。
~~~shell-session: docker-composeでの環境立ち上げ確認
$ docker-compose -f docker-compose.dev.yml build --force golang
$ docker-compose -f docker-compose.dev.yml up -d golang
Creating dev-env-golang ... done
$ docker-compose -f docker-compose.dev.yml exec golang bash
gouser@e0dd620928c2:~$

gouser@e0dd620928c2:~$ ip -4 address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
173: eth0@if174: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default  link-netnsid 0
    inet 192.168.10.1/24 brd 192.168.10.255 scope global eth0
       valid_lft forever preferred_lft forever

gouser@e0dd620928c2:~$ ip -4 route
default via 192.168.10.254 dev eth0
192.168.10.0/24 dev eth0 proto kernel scope link src 192.168.10.1

gouser@e0dd620928c2:~$ grep gouser /etc/passwd
gouser:x:1000:1000::/home/gouser:/bin/bash

gouser@e0dd620928c2:~$ grep gouser /etc/group
sudo:x:27:gouser
gouser:x:1000:

gouser@e0dd620928c2:~$ export | grep -E '(LAN|LC|TZ|GO)'
declare -x GO111MODULE="on"
declare -x GOLANG_VERSION="1.18.3"
declare -x GOPATH="/go"
declare -x LANG="C.UTF-8"
declare -x LANGUAGE="C"
declare -x LC_ALL="C.UTF-8"
declare -x TZ="Asia/Tokyo"
~~~

試しに別ターミナルでdocker-compose top、execをしてみる。upで立ち上がったbashと、execで立ち上げたbashの2プロセスが起動している。もう一度execすると3つ目のbashプロセスが同一コンテナで立ち上がる。

~~~shell-session: docker-composeでの環境立ち上げ確認(別ターミナルで)
$ docker-compose -f docker-compose.dev.yml top
dev-env-golang
UID     PID    PPID    C   STIME   TTY     TIME        CMD
-------------------------------------------------------------
1000   18191   18170   0   14:20   ?     00:00:00   /bin/bash
1000   18260   18170   0   14:20   ?     00:00:00   bash

$ docker-compose -f docker-compose.dev.yml exec golang bash
gouser@e0dd620928c2:~$
~~~

### Makefile作成
環境操作を簡単に操作できるようにMakefileを作っておく。

~~~makefile:Makefile
.PHONY: \
	env-create env-destroy env-down env-rmi env-build env-top env-images \
	net-init net-deinit \
	go-build go-up go-console go-stop go-down go-rmi

# 変数 --------------------------
# network addressを.envrcから取得
NETWORK_ADDR := $(shell grep 'NETWORK_ADDR' .env | sed -r 's/NETWORK_ADDR=(.+)/\1/g' )
# gateway addressを.envrcから取得
NETWORK_GATEWAY_IP := $(shell grep 'NETWORK_GATEWAY_IP' .env | sed -r 's/NETWORK_GATEWAY_IP=(.+)/\1/g' )

# 環境 --------------------------
env-create: net-init env-build
env-destroy: env-rmi net-deinit

env-down:
	# 全コンテナ停止
	-$(DC) down

env-rmi:
	# 全コンテナ停止とイメージ削除
	-$(DC) down --rmi all

env-build: go-build

env-top:
	$(DC) top

# ネットワーク -------------------
net-init:
	-$(D) network create --driver=bridge --subnet=$(NETWORK_ADDR) --gateway=$(NETWORK_GATEWAY_IP) dev-env-link

net-deinit:
	-$(D) network remove dev-env-link

# golang環境 ---------------------
go-build:
	$(DC) build --force golang

go-up:
	$(DC) up -d golang

go-console: go-up
	$(DC) exec golang bash

go-stop:
	-$(DC) stop golang

go-down: go-stop
	-$(DC) rm -f golang

go-rmi: go-down
	-$(D) rmi -f dev-env-golang-image
~~~

ここまで。
作成した環境は [GitHub](https://github.com/jun00rbiter/go-env/tree/qiita_20220605) を参照。

## その他
この作業で便利なVSCode extension
- Docker(Microsoft)