最終目的
- go言語でWebアプリを構築するためのgo開発環境をDockerで作成する。
目標
- Docker上にコンソール立ち上げ環境を作る。
- フレームワークginを使った簡単なREST APIサーバーを立ち上げる。← 今ここ
- イメージビルド時にプログラムをコンパイルしコンテナ起動と同時にホストPCからAPIにアクセスできるようにする。
Goのフレームワークの選定
フロントエンドはWebだけとは限らない。バックエンドとフロントエンドは分け、JSONでやり取りするRESTful APIとする。
GitHubのStars、Forks、日本での利用数、Echoはリリースが頻繁なのも(良いも悪いも)気になったので、ginを選択。
簡単なAPIサーバーを考える
環境構築がまずの目的なので、APIサーバーは単純なものをginを使用して作る。
状態(とりあえずは必ずOK)を返すヘルスチェック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される方。
ざっと役割を考えると、下記の通り。環境構築の目的とは離れるので詳しくは説明しない。
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.dev
をgo-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サービスを複製して変更)。
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用のコマンドを追加する。
# 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
--- 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
を付けるように修正。
/src/go-blog$ mkdir app && cd app
/src/go-blog/app$ go mod init github.com/YOURNAME/go-blog/app
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
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()
}
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) // ヘルスチェック
}
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)
}
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の生成を行う。
/src/go-blog/app$ go mod tidy
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ポートで待ち受けるサービスが起動する。
/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
でもかまわない。
$ 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
となる。
$ 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
の時と同様、動作確認をしておく。
/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からも接続できるようにする
ここまでの一式
(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のモジュールを利用できなくなる。
github.com/YOURNAME/go-blog/
└── app/
└── go.mod
下記のようにgo.modのmodule名定義を変更、appディレクトリ配下の.goファイルで、自moduleのpackageをimportしている個所を修正している。
正: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 app/v0.0.10
他のモジュールのgo.modを下記のように記述すると、ソースコードからimportできるようになる。
module example
go 1.18
require github.com/jun00rbiter/go-blog/app v0.0.10