2
Help us understand the problem. What are the problem?

posted at

updated at

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

最終目的

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

目標

Goのフレームワークの選定

フロントエンドはWebだけとは限らない。バックエンドとフロントエンドは分け、JSONでやり取りするRESTful APIとする。
GitHubのStars、Forks、日本での利用数、Echoはリリースが頻繁なのも(良いも悪いも)気になったので、ginを選択。

簡単なAPIサーバーを考える

環境構築がまずの目的なので、APIサーバーは単純なものをginを使用して作る。
状態(とりあえずは必ずOK)を返すヘルスチェックAPIのみとする。

API仕様
# GET /api/health-check
+ Response 200 (application/json)
  + Body
    { "status": "OK" }

サンプルとしてはblogぽいものを目指すとして、go-blogとする。
GitHubに 「github.com/YOURNAME/go-blog」 でアクセスできるレポジトリを作成しておく。

YOURNAME は、GitHubアカウント名

開発環境構成の検討

前回のgo-env環境は複数のコンテナを配置して開発できることを目的としている。
go-env構成のsrc配下にgo-blogをgit cloneして配置することにする。

./
+ env/
|   + Dockerfile.golang.dev
|   + Dockerfile.blog.dev ★
+ src/
|   + golang/
|   + go-blog/   ★「github.com/YOURNAME/go-blog」をgit cloneしたディレクトリ
+ docker-compose.dev.yml
+ Makefile
+ .env
+ .env.dev

go-blogの構成

go言語はpackageの階層構成がそのままディレクトリ構成となり、言語の制約上、packageが相互にimportするような記述はできない。
ある程度のプログラム構成を考えて、ディレクトリ構成を考える必要がある。矢印はimportの方向性、矢印の頭がimportされる方。

image.png

ざっと役割を考えると、下記の通り。環境構築の目的とは離れるので詳しくは説明しない。

package 役割
entrypoint goのmain packageを配置する。用途によってサブディレクトリに分ける。
routes 要求されたリクエストPATHに応じて、controllerを呼び分ける層。
controllers 要求されたリクエスト毎の処理を受け持つ層。
logics 複数のコントローラーから呼び出されるような共通のビジネスロジックを受け持つ層。
resoures データを受け持つ層。DBや外部のAPIサービス等とのデータやり取り。
types APIサーバでやり取りするデータの型を定義する。用途によってサブディレクトリに分ける。
utils 複数サービスで共通となる処理や型を定義する。大きくなるようであれば、別レポジトリ管理にした方がよい。

今回は固定処理しかしないので、controllers,entrypoints,routers,typesのみを使う。
ファイル構成は下記の通り。プログラムは記載するが、説明は目的と離れるので省略する。

ディレクトリ構成
go-blog/
└── app/
    ├── controllers/
    ├── entrypoints/
    │   └── production/
    ├── routers/
    └── types/
        └── rest/

go-blogの環境作成

まずは、go-blogディレクトリを作成し、Dockerfileを作成。docker-compose.dev.ymlとMakefileに追加する。

./
+ env/
|   + Dockerfile.golang.dev
|   + Dockerfile.blog.dev    ★新規作成(Dockerfile.golang.devをコピーして編集)
+ src/
|   + golang/
|   + go-blog/               ★ディレクトリ作成
+ docker-compose.dev.yml     ★設定追加
+ Makefile                   ★設定追加

Dockerfile.golang.devgo-blog/Dockerfileにコピーして、コードを配置するディレクトリの追加だけを行う。

--- env/Dockerfile.golang.dev
+++ src/go-blog/Dockerfile
@@ -14,7 +14,7 @@

 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

 USER $USER_NAME
 WORKDIR /home/$USER_NAME
+WORKDIR /src/go-blog

docker-compose.dev.ymにblogサービスの設定を追加する(golangサービスを複製して変更)。

docker-compose.dev.ymにblogサービスの設定を追加
version: "3"
services:
  # ===== 追加ここから =====
  blog:                                   # ★サービス名変更
    build:
      context: .
      dockerfile: ./env/Dockerfile.blog.dev # ★blog用のDockerファイルを指定
      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
    tty: true
    volumes:
      - ./src/golang:/home/$USER_NAME
      - ./src/go-blog:/src/go-blog       # ★割り当てるディレクトリを追加する
    env_file: .env.dev
    environment:
      - GO111MODULE=on
    networks:
      dev-env-link:
        ipv4_address: $NETWORK_BASE.20    # ★IPアドレスを変える(192.168.10.20)
  # ===== 追加ここまで =====
  golang:

Makefileにblog用のコマンドを追加する。

Makefileにblogサービスのコマンドを追加
# blog環境 ---------------------
# blog環境 imageビルド
blog-build:
	$(DC) build --force blog

# blog環境 container実行
blog-up:
	$(DC) up -d 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
env-buildでblogイメージもビルドされるように変更
--- a/Makefile rm -f blog
+++ b/Makefile
@@ -34,7 +34,7 @@ env-rmi:破棄、image破棄
 blog-rm-$(DC) down --rmi all
        -$(D) rmi -f dev-env-blog-image
 # docker imageビルド
-env-build: go-build
+env-build: go-build blog-build

下記コマンドでblogサービスのコンテナが立ち上がり、ディレクトリが/src/go-blog、IPアドレス(192.168.10.20)を確認しておく。

コンテナ立ち上げ確認
$ make blog-build && make blog-console

gouser@e4d2effc351f:/src/go-blog$ 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
243: eth0@if244: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default  link-netnsid 0
    inet 192.168.10.20/24 brd 192.168.10.255 scope global eth0
       valid_lft forever preferred_lft forever

go-blogの実装

go.modファイルの作成

go-blog/appディレクトリを作成し、go.modファイルを作成する。

(2022/06/11修正) module名をレポジトリのパスに合わせてappを付けるように修正。

go.modファイルの作成
/src/go-blog$ mkdir app && cd app
/src/go-blog/app$ go mod init github.com/YOURNAME/go-blog/app
作成されたgo.modの内容
module github.com/YOURNAME/go-blog/app     //(2022/06/11修正)
go 1.18

実装

下記構成でファイルを作成していく。

(2022/06/11修正) module名をレポジトリのパスに合わせてappを付けるように修正したことに伴い、自moduleのpackageをimportしている個所を修正。

ディレクトリ構成
/src/go-blog/
└── app/
    ├── go.mod
    ├── go.sum
    ├── controllers/
    │   └── health_check_controller.go
    ├── entrypoints/
    │   └── production/
    │       └── main.go
    ├── routers/
    │   └── api_router.go
    └── types/
        └── rest
            └── health_check_type.go
main.go
package main

import (
	"github.com/gin-gonic/gin"
	"github.com/YOURNAME/go-blog/app/routers"     //(2022/06/11修正)
)

func main() {
	engine := gin.Default()
	routers.RegisterAPIRouter(engine)
	engine.Run()
}
api_router.go
package routers

import (
	"net/http"
	"github.com/gin-gonic/gin"
	"github.com/YOURNAME/go-blog/app/controllers"     //(2022/06/11修正)
)

// RegisterAPIRouter は、APIのルーティングを定義する
func RegisterAPIRouter(engine *gin.Engine) {
	// ルーティングしていないURLにアクセスが来た時にエラーJSONで返す
	engine.NoRoute(func(c *gin.Context) {
		c.AsciiJSON(http.StatusNotFound, gin.H{"error": gin.H{"message": "not found"}})
	})

	apiGroup := engine.Group("/api")
	apiGroup.GET("/health-check", controllers.GetHealthCheck) // ヘルスチェック
}
health_check_controller.go
package controllers

import (
	"net/http"
	"github.com/gin-gonic/gin"
	"github.com/YOURNAME/go-blog/app/types/rest"     //(2022/06/11修正)
)

// GetHealthCheck はヘルスチェックのController
func GetHealthCheck(c *gin.Context) {
	// 無条件でstatus:OKを返す
	res := rest.HealthCheckResponse{
		Status: "OK",
	}
	c.SecureJSON(http.StatusOK, res)
}
health_check_type.go
package rest

// HealthCheckResponse は、ヘルスチェックのレスポンス情報
type HealthCheckResponse struct {
	Status string `json:"status"`
}

go.modの更新とgo.sumの生成

go.modがあるgo-blog/appディレクトリでgo mod tidyを実行し、go.modの更新とgo.sumの生成を行う。

go.modファイルの作成
/src/go-blog/app$ go mod tidy
go mod tidy後の変更されたgo.modの内容
module github.com/YOURNAME/go-blog/app      //(2022/06/11修正)

go 1.18

require github.com/gin-gonic/gin v1.8.0

require (
	github.com/gin-contrib/sse v0.1.0 // indirect
	github.com/go-playground/locales v0.14.0 // indirect
	github.com/go-playground/universal-translator v0.18.0 // indirect
	github.com/go-playground/validator/v10 v10.10.0 // indirect
	github.com/goccy/go-json v0.9.7 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/leodido/go-urn v1.2.1 // indirect
	github.com/mattn/go-isatty v0.0.14 // indirect
	github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/pelletier/go-toml/v2 v2.0.1 // indirect
	github.com/ugorji/go/codec v1.2.7 // indirect
	golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
	golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
	golang.org/x/text v0.3.6 // indirect
	google.golang.org/protobuf v1.28.0 // indirect
	gopkg.in/yaml.v2 v2.4.0 // indirect
)

実行と動作確認

go runでサービスを実行する。8080ポートで待ち受けるサービスが起動する。

go-blogサービスの起動
/src/go-blog/app$ go run entrypoints/production/main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/health-check         --> github.com/jun00rbiter/go-blog/controllers.GetHealthCheck (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

別ターミナルから動作を確認する。
golangコンテナ環境かblogコンテナ環境上のbashでcurl等で確認。
結果が200 OKで、JSONの内容は正しいか、レスポンスのContent-Typeがapplication/jsonであることを確認。

ホストからでは通信できない。

golangコンテナからの確認。通信先はblog:8080でも、192.168.10.20:8080でもかまわない。

golangコンテナからの確認
$ make go-console
gouser@2a6b62e670e5:~$ curl -v http://blog:8080/api/health-check | jq
...
> GET /api/health-check HTTP/1.1
> Host: blog:8080
> User-Agent: curl/7.64.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sun, 05 Jun 2022 15:29:46 GMT
< Content-Length: 15
<
{ [15 bytes data]
100    15  100    15    0     0   7500      0 --:--:-- --:--:-- --:--:--  7500
* Connection #0 to host blog left intact
{
  "status": "OK"
}

blogコンテナからの確認。通信先はlocalhost:8080となる。

blogコンテナからの確認
$ make blog-console
gouser@8a476b984d4a:/src/go-blog$ curl -v http://localhost:8080/api/health-check | jq
...
> GET /api/health-check HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sun, 05 Jun 2022 15:35:37 GMT
< Content-Length: 15
<
{ [15 bytes data]
100    15  100    15    0     0   5000      0 --:--:-- --:--:-- --:--:--  5000
* Connection #0 to host localhost left intact
{
  "status": "OK"
}

go runを実行したコンソールでは下記のように通信記録が表示される。

[GIN] 2022/06/05 - 21:29:46 | 200 |  55.5µs | 192.168.10.1 | GET "/api/health-check"  ★golangコンテナからのアクセス
[GIN] 2022/06/05 - 21:35:37 | 200 |    78µs |    127.0.0.1 | GET "/api/health-check"  ★blogコンテナからのアクセス

アプリのビルド方法

go buildコマンドで実行バイナリgo-blogをコンパイルする。go runの時と同様、動作確認をしておく。

go-blogのビルドと実行
/src/go-blog/app$ go build -o go-blog entrypoints/production/main.go

/src/go-blog/app$ ls -l go-blog
-rwxr-xr-x 1 gouser gouser 10112511 Jun  6 00:55 go-blog

/src/go-blog/app$ ./go-blog
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/health-check         --> github.com/jun00rbiter/go-blog/controllers.GetHealthCheck (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2022/06/06 - 00:55:07 | 200 |        39.4µs |       127.0.0.1 | GET      "/api/health-check"

今の状況。

blogサービスコンテナ、golangサービスコンテナが起動し相互通信できる状態。docker network内でdocker-composeで立ち上げたコンテナにはサービス名(golang、blog)をホスト名としてアクセスすることができる。もちろんIPでアクセスしても大丈夫。
ただし、ホストPCからdocker network内のサービスに対して通信することはできない。実施するにはport forwardが必要だが、また今度。

次にやることは

  • blogイメージ作成時にgo-blogを自動ビルド
  • docker-compose upでgo-blogを自動起動
  • ホストPCからも接続できるようにする

image.png

ここまでの一式

(2022/06/07追記) ツールのイメージビルド時インストール

上記コンソール操作の中で利用しているコマンドでインストールされていないものはaptを使ってインストールしている。
イメージビルドの度にインストールし直すのが面倒であれば、Dockerfileであらかじめインストールしておけばよい。

diff --git a/env/Dockerfile.blog.dev b/env/Dockerfile.blog.dev
index e83cbf0..e8c8968 100644
--- a/env/Dockerfile.blog.dev
+++ b/env/Dockerfile.blog.dev
@@ -12,6 +12,8 @@ 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

(2022/06/11修正) go.modのモジュール名をレポジトリパスに合わせて修正

go.modに記載のmodule名は、go.modのURLに合わせておく必要がある。
合わせていないと、他のモジュールからgo-blogのモジュールを利用できなくなる。

go.modのレポジトリパス
github.com/YOURNAME/go-blog/
    └── app/
        └── go.mod

下記のようにgo.modのmodule名定義を変更、appディレクトリ配下の.goファイルで、自moduleのpackageをimportしている個所を修正している。

go.modの正誤
正:modules github.com/YOURNAME/go-blog/app
↑
誤:modules github.com/YOURNAME/go-blog

他のモジュールから、このようなレポジトリルートにgo.modがないモジュールを利用できるようにしたい場合は、下記のようにgitのtagを打っておく。go-blogのgo.modはappディレクトリにあるので、app/vX.X.Xの形式でtagを打つ。

git tagのつけ方
git tag app/v0.0.10

他のモジュールのgo.modを下記のように記述すると、ソースコードからimportできるようになる。

利用する側のgo.mod
module example

go 1.18

require github.com/jun00rbiter/go-blog/app v0.0.10

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
2
Help us understand the problem. What are the problem?