【環境構築】VSCode：Remote-Container機能を利用した[Docker Compose+Go+MySQL]の開発用コンテナ構築

# はじめに
今回は**VSCodeの**【**Remote-Container機能**】と【**Docker**】を使用した、
「**開発用コンテナ**」が便利ということで環境構築を行いました。
最小構成から自分がやりたいことを増やしていく中で、
つまづいた点・検索してもなかなか効果的なサイトが見つからなかった点についてまとめてみました。

## この記事で分かること
* **VSCodeのRemote-Container機能**を使用した開発用コンテナ作成手順
* **GoおよびMySQLの`docker-compose & Dockerfile` `devcontainer.json`の設定で注意する点**
* 上記2点を含む、**webアプリ開発用Go+DBコンテナのソースコード**

# 環境とコンテナ起動までの流れ
## 動作環境
|               |                                 |
|:--------------|:--------------------------------|
|Docker         |Docker Desktop for Windows 4.12.0|
|Dockerのbackend|WSL2 Ubuntu 20.04 LTS            |
|IDE            |VSCode（latest）                 |
|ホストOS        |Windows11                       |

## コンテナ起動までの流れ
### 事前に必要なもの
* WSL2
* Docker Desktop
* VSCode

3点のインストールと最低限の設定は必要です。
やり方に関しては本記事では取り扱いません。
WSL導入まで知りたい方はコチラを → [【#50 エンジニア転職学習】WSL2でUbuntu20.04のGo開発環境をつくる](https://qiita.com/Chika110/items/68e1e4e13ac75c08453c "【#50 エンジニア転職学習】WSL2でUbuntu20.04のGo開発環境をつくる") 参考に構築ください。
Docker Desktopに関しては公式マニュアル → [Windows に Docker Desktop をインストール](https://docs.docker.jp/docker-for-windows/install.html#windows-docker-desktop "Windows に Docker Desktop をインストール")
どおりに実行すれば問題なくインストールまでいけるはずです。

### 開発用コンテナ起動までの全体流れ
上記【事前に必要なもの】のセットアップまで終わっている状態からスタートとします。
手順6に記述している通り、**`.devcontainer`が直下にあるディレクトリからReopen in Containerを実行**してください。
違う階層で実行すると**VSCodeが`.devcontainer`を認識してくれない**らしく、
設定した各種ファイルが反映されなかったり、指定したコンテナが起動しなかったりします。

1. ローカルでVSCodeを開き、Extensionsから【Remote Development】をinstallする![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2041988/52962727-afa7-ce12-534e-2a7a0457fba2.png)

1. WindowsTerminalでWSL2のUbuntuシェルを開き、任意のプロジェクトディレクトリ作成
1. `cd [プロジェクト]`でプロジェクトに移動して`code .`を実行、WSLからVSCodeを開く
1. `.devcontainer`フォルダ作成
1. `docker-compose.yml`,`Dockerfile`,`devcontainer.json`ファイルの作成（後ほど詳しく解説）
1. Docker Desktopを起動しておく
1. **`.devcontainer`が直下にあるフォルダ（私の例では/build）で`code .`をしてVSCodeを開きなおす**
1. **`F1`を押す**
1. 中から`Remote-Containers:Reopen in Container`を実行![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2041988/6b658280-4162-29eb-bd55-75fa78c97a66.png)
1. すぐに新しいVSCodeウィンドウが立ち上がり、開発用コンテナ起動（初回は結構時間かかります）



# 完成したコードと各ポイント
各コードとポイントを解説します。
今回のRemoteContainerを使用した開発用コンテナに関しては、人によって記述が違うことが多々あったため、
[VSCodeの公式ドキュメント](https://code.visualstudio.com/docs/remote/create-dev-container#_use-docker-compose "Create a development container")や[Dockerの公式ドキュメント](https://docs.docker.com/language/golang/develop/#configure-docker-compose "Docker Compose の構成")をメインに参照していきました。
## プロジェクト構成
全体像になります。
```
.
├── build
│   ├── .devcontainer
│   │   ├── devcontainer.json
│   │   └── docker-compose.yml
│   ├── golang
│   │   └── Dockerfile
│   └── mysql
│       ├── .env
│       ├── Dockerfile
│       └── conf
│           └── my.cnf
├── cmd
│   ├── app
│   │   ├── controllers
│   │   │   ├── route.go
│   │   │   └── webserver.go
│   │   ├── models
│   │   │   ├── albumdb.go
│   │   │   └── base.go
│   │   └── views
│   │       ├── index.html
│   │       ├── keyalbumlist.html
│   │       └── update.html
│   └── main.go
├── go.mod
└── go.sum
```
Goに関するプロジェクト構成で公式っぽいものはありますが、
正確ではないとGo開発チームから異議が唱えられているそうです。([Go のパッケージ構成のデファクトはあるのか？](https://blog.p1ass.com/posts/go-faq/#:~:text=%E3%81%8C%E5%A4%9A%E3%81%84%E3%81%A7%E3%81%99%E3%80%82-,Go%20%E3%81%AE%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E6%A7%8B%E6%88%90%E3%81%AE%E3%83%87%E3%83%95%E3%82%A1%E3%82%AF%E3%83%88%E3%81%AF%E3%81%82%E3%82%8B%E3%81%AE%E3%81%8B%EF%BC%9F,-golang%2Dstandards/project "今までにGoでよく聞かれた質問とその参考リンク"))
JAVAプロジェクトに見えないように、「src」ではなく「cmd」を使用しましょうとのことなのでその点だけ守ってます。
それ以外の構成はコチラ→ ([The Go Blog Package names](https://go.dev/blog/package-names "The Go Blog Package names")) の内容を開発進めながら身に付けていくしかないのかと思います。



## .devcontainer

Remote-Containerで開発用コンテナをビルドするためのディレクトリです。

### devcontainer.json
**開発用コンテナとRemote時のVSCodeの設定をメインにする**ファイルです。
Microsoftの開発チームがGitで公開している → [microsoft/vscode-dev-containers](https://aka.ms/vscode-remote/samples/existing-docker-compose "microsoft/vscode-dev-containers") をベースにしています。
Docker-Composeを使用してコンテナを立ち上げるため、`dockerComposeFile`での指定は必須です。
```build/.devcontainer/devcontainer.json
{
	// リモートコンテナ名 適当でok
	"name": "go-db-devcontainer",

	// docker-composeファイルのパス指定 今回は同階層にあるためファイル名のみ記述
	"dockerComposeFile": [
		"docker-compose.yml"
	],
	
	// docker-compose.ymlの中で開発用コンテナとして作業するサービスを指定
	"service": "app",

	// リモートコンテナのVSCodeで実際に作業するディレクトリ
	"workspaceFolder": "/workspace",

	// VSCodeを閉じたときの動作指定
	"shutdownAction": "stopCompose",

	// 非ルートユーザーの作成
	"remoteUser": "vscode",

	// VSCodeの設定を記述 .vscodeフォルダの代わりになる（らしい）
	"settings": {
		"terminal.integrated.defaultProfile.linux": "bash",
		"go.toolsManagement.checkForUpdates": "local",
		"go.useLanguageServer": true,
		"go.gopath": "/go",
		"go.goroot": "/usr/local/go"
	},

	// VSCodeのExtensionsで追加したパッケージを指定
	"extensions": [
		"golang.Go"
	]
}
```


### docker-compose.yml
**通常のDocker-Composeファイル**です。今回はGo言語とMySQL用のコンテナビルドを指示してます。
```build/.devcontainer/docker-compose.yml
version: '3.8'
services:
   app:
    user: vscode

    # Go用のDockerfileがあるpath、dockerfile名を指示
    build:
      context: ../golang
      dockerfile: Dockerfile
    env_file:
      - ../mysql/.env
    # dbコンテナが起動した後に起動するように指示
    depends_on:
      - db

    # プロジェクトフォルダのpathを指定して、workspace（開発用コンテナ内の作業場所）にマウント
    volumes:
      - ../..:/workspace:cached

    # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
    # 何用かは分からないですがGoで開発する際のデバッガーとして必要らしいです
    cap_add:
      - SYS_PTRACE
    security_opt:
      - seccomp:unconfined

    # appコンテナへのポートフォワード指定
    ports:
      - 8080:8080
    # コンテナ切断をしないように指示
    command:
      /bin/sh -c "while sleep 1000; do :; done"
      /bin/bash

  db:
    container_name: db
    
    # MySQL用のDockerfile指定
    build: 
      context: ../mysql
      dockerfile: Dockerfile
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    platform: linux/x86_64

    # envファイルのpath
    env_file:
      - ../mysql/.env
    # データ永続化
    volumes:
      - mysql_data:/var/lib/mysql
      - ../mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf
    ports:
      - 13306:3306
    # コンテナ間の接続を可能にするためのネットワーク設定
    networks:
      - default

volumes:
  mysql_data:
    driver: local
networks:
  default:
```

### Dockerfile(Go用)
Goアプリ用のDockerfileです。ポイントは2点あります。
* **最後の`sudo chmod -R a+rwX /go/pkg/`を入れておく**
* `RUN`は`&&`で繋いで処理を軽くする（個別にRUNを記述しない）

特にパーミッション変更は、開発時にサードパーティパッケージ等を使用する際に
`go mod`への追加変更をするために必要ですので忘れずいれておきましょう。

```Dockerfile
FROM golang:1.19

ENV GO111MODULE on
WORKDIR /workspace

# コンテナ内の非ルートユーザの設定
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# 非ルートユーザー作成
# sudoコマンドの追加
# Go用の開発ツール
# go mod用のパーミッション変更 など
RUN groupadd --gid $USER_GID $USERNAME \
    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
    && apt update \
    && apt install -y sudo \
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME \
    && go install golang.org/x/tools/gopls@latest \
    && go install github.com/go-delve/delve/cmd/dlv@latest \
    && go install github.com/ramya-rao-a/go-outline@latest \
    && go install github.com/fatih/gomodifytags@latest \
    && go install github.com/josharian/impl@latest \
    && sudo chmod -R a+rwX /go/pkg/
```


## mysql
既に似たようなコンテナがあると正常に起動されない時があるようです。
そのときは`docker system prune`と`docker system prune --volume`を実行して、
Dockerをきれいにしましょう。私の場合はそれで問題なく起動しました。

### my.cnf
文字コード指定やプラグイン方法の指定をしています。
```build/mysql/conf/my.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_bin

default-time-zone = SYSTEM
log_timestamps = SYSTEM

# mysql8.0以降であれば必須↓
default-authentication-plugin = mysql_native_password

[mysql]
default-character-set = utf8mb4

[client]
default-character-set = utf8mb4
```

### .env
環境変数としてDatabaseやパスワードの記述を行います。
```build/mysql/.env
# 任意の値にしてください
MYSQL_DATABASE=test1
MYSQL_USER=dev_user
MYSQL_PASSWORD=password
MYSQL_ROOT_PASSWORD=root_password
```

### Dockerfile(MySQL用)
```Dockerfile
FROM mysql:8.0

EXPOSE 3306

ENV LANG ja_JP.UTF-8
```


## cmd
アプリのGoファイル等を置いています。
ベースは以前作成したGin等をしようしたCRUDアプリ(**[【#46 エンジニア転職学習】GinTutorialのWebApp改造 HTMLに連動させる](https://qiita.com/Chika110/items/a6decf1f5c49001a6ca3 "【#46 エンジニア転職学習】GinTutorialのWebApp改造 HTMLに連動させる")**)になります。
内容自体は変わっていないので、コンテナ化に際して変更した点のみ紹介します。

### route.go
最後の`router.Run(:8080)`は以前までなら`(localhost:8080)`でしたが、
**コンテナ化に伴いlocalhost部分は消します。**
`docker-comopose.yml`でポートフォワーディングしているので、ブラウザで`localhost:8080`を実行すればコンテナからレスポンスが返ってきます。
```cmd/app/controllers/route.go
package controllers

import (
	"net/http"
	"time"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"github.com/jinzhu/gorm"
)

func NewRoutes(dbConn *gorm.DB) {
	//dbConnをインターフェースとして持つalbumhandlerを定義
	albumHandler := AlbumHandler{
		Db: dbConn,
	}
	//gin.Engineインターフェースを作成
	router := gin.Default()

	router.Use(cors.New(cors.Config{
		AllowOrigins:     []string{"*"},
		AllowMethods:     []string{"PUT", "PATCH", "DELETE", "POST", "GET"},
		AllowHeaders:     []string{"Origin"},
		ExposeHeaders:    []string{"Content-Length"},
		AllowCredentials: true,
		AllowOriginFunc: func(origin string) bool {
			return origin == "*"
		},
		MaxAge: 12 * time.Hour,
	}))

	//pattern一致するhtmlファイルをロードして、rendererに渡す
	router.LoadHTMLGlob("app/views/*.html")

	router.GET("/albums", func(c *gin.Context) {
		albums := albumHandler.GetAlbums()
		c.HTML(http.StatusOK, "index.html", gin.H{"albums": albums})
	})
	router.GET("/albums/:id", albumHandler.GetAlbumByID)
	router.POST("/albums/new", albumHandler.InsertAlbums)
	router.POST("/albums/update/:id", albumHandler.UpdateAlbumByID)
	router.POST("/albums/delete/:id", albumHandler.DeleteAlbumByID)
	router.POST("/albums/search", albumHandler.GetAlbumsByKeyword)

	router.Run(":8080")
}
```


### base.go
`dsn`変数部分を2点変更。
* MySQLの`.env`ファイルで設定した`USER`,`PASSWORD`,`DATABASE`を使用
* @のあとに`tcp(db:3306)`の接続方法追加
```cmd/app/models/base.go
package models

import (
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
)

//Initialized DB and retrun gormDBInterface
func DbInit() *gorm.DB {
	var dsn = "dev_user:password@tcp(db:3306)/test1?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open("mysql", dsn)
	if err != nil {
		panic("failed connecting DB")
	}
	db.LogMode(true)
	db.AutoMigrate(&Album{})
	return db
}
```

### main.go
他の個人packageを使用しているファイルも同様ですが、
importをローカルファイル指定から**GitHubリポジトリ指定に変更**。
```cmd/main.go
package main

import (
	"github.com/XXXXXXXXXXX.git/cmd/app/controllers"
	"github.com/XXXXXXXXXXX.git/cmd/app/models"
)

func main() {
	dbConn := models.DbInit()
	controllers.NewRoutes(dbConn)
}

```


# 実際に起動してみる
手順7まで行き、`F1`を押し`Remote-Containers:Reopen in Container`を実行。
VSCodeが開きなおしされ、下図のようにコンテナverに切り替わっていれば成功です。（初回は時間かかります）

↓プロジェクトのサイドバー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2041988/5d5d1d7e-2c4f-9431-6c13-0d5ad47ed05f.png)
↓VSCode左下の表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2041988/297598dd-740f-c77a-62c6-68a115ca684b.png)


試しにブラウザからリクエスト送信すると、、
↓のように無事レスポンスが返ってきました！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2041988/3548f8a7-ed4d-fba5-6496-b0648e83b199.png)

 
# おわりに
今回はRemote-Containerの環境構築を行いました。
Dockerをはじめ技術面的な学習はもちろん、**logを見てエラー原因の仮説立て**することや**公式ドキュメントを読む**ことの大切さを学習できたと思います。
個人的な都合で1か月振りの投稿となりましたが、またこれから再開していきたいと思います。
アドバイスや質問等ございましたらお願い致します。

# 参考
* [Visual Studio Code : Create a development container](https://code.visualstudio.com/docs/remote/create-dev-container#_use-docker-compose "")
* [Docker Docs : Build your Go image](https://docs.docker.com/language/golang/build-images/#create-a-dockerfile-for-the-application "")
* [GitHub : Microsoft vscode-dev-containers](https://github.com/microsoft/vscode-dev-containers "")