Docker/Kubernetes 実践コンテナ開発入門:書籍案内|技術評論社
途中からですが、学習ログ取っていきます。
4.3 API Service の構築
- TODO アプリのドメインを担当する API の構築
$ git clone https://github.com/gihyodocker/todoapi
4.3.1 todoapi の基本構造
- アプリケーションの実行は cmd/main.go から始まる。
- windows なので tree コマンドの表示が気に入らない…
$ tree /f todoapi
フォルダー パスの一覧: ボリューム Windows
ボリューム シリアル番号は 6607-7B7E です
C:\USERS\SNYT45\DESKTOP\DOCKER_KUBERNETES 実践コンテナ開発入門\4-2\TODOAPI
│ .dockerignore
│ .gitignore
│ db.go
│ Dockerfile
│ env.go
│ go.mod
│ go.sum
│ handler.go
│
└─cmd
main.go
- Docker 関係ない。go の話。
- cmd/main.go
- 環境変数の取得、MySQL への DB 接続処理、HTTP リクエストのハンドラ作成・エンドポイントの登録、サーバーの実行
package main
import (
"fmt"
"log"
"net/http"
"os"
"github.com/gihyodocker/todoapi"
)
func main() {
// (1) 必要な環境変数を格納した構造体を作成
env, err := todoapi.CreateEnv()
if err != nil {
fmt.Fprint(os.Stderr, err.Error())
os.Exit(1)
}
// (2) MySQL Masterへの接続するための構造体を作成
masterDB, err := todoapi.CreateDbMap(env.MasterURL)
if err != nil {
fmt.Fprintf(os.Stderr, "%s is invalid database", env.MasterURL)
return
}
// (3) MySQL Slaveへの接続するための構造体を作成
slaveDB, err := todoapi.CreateDbMap(env.SlaveURL)
if err != nil {
fmt.Fprintf(os.Stderr, "%s is invalid database", env.SlaveURL)
return
}
mux := http.NewServeMux()
// (4) ヘルスチェック用APIのハンドラを作成
hc := func(w http.ResponseWriter, r *http.Request) {
log.Println("[GET] /hc")
w.Write([]byte("OK"))
}
// (5) TODO操作APIのハンドラを作成
todoHandler := todoapi.NewTodoHandler(masterDB, slaveDB)
// (6) ハンドラをAPIエンドポイントとして登録
mux.Handle("/todo", todoHandler)
mux.HandleFunc("/hc", hc)
// (7) サーバのポートやハンドラを設定し、Listenを開始
s := http.Server{
Addr: env.Bind,
Handler: mux,
}
log.Printf("Listen HTTP Server")
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
4.3.2 アプリケーションでの環境変数の制御
- Docker 関係ない。go の話。
- env.go
- 環境変数を取得するための処理
- cmd/main.go から呼びだす関数を定義
- CreateEnv 関数
- 必要な環境変数を取得して Env という構造体に設定する
- os.Getenv は環境変数を取得する関数
- コンテナに設定した環境変数を取得
package todoapi
import (
"errors"
"os"
)
// 必要な環境変数を格納するための構造体
type Env struct {
Bind string
MasterURL string
SlaveURL string
}
func CreateEnv() (*Env, error) {
env := Env{}
bind := os.Getenv("TODO_BIND") // APIをListenするポート設定
if bind == "" {
env.Bind = ":8080"
}
env.Bind = bind
masterURL := os.Getenv("TODO_MASTER_URL") // MySQL Masterへの接続情報
if masterURL == "" {
return nil, errors.New("TODO_MASTER_URL is not specified")
}
env.MasterURL = masterURL
slaveURL := os.Getenv("TODO_SLAVE_URL") // MySQL Slaveへの接続情報
if slaveURL == "" {
return nil, errors.New("TODO_SLAVE_URL is not specified")
}
env.SlaveURL = slaveURL
return &env, nil
}
4.3.3 MySQL 接続、テーブルマッピング
- Docker 関係ない。go の話。
- db.go
- MySQL に接続するための処理
- CreateDbMap 関数
[DBユーザー名]:[DBパスワード]@tcp([DBホスト]:[DBポート])/[DB名]
形式の接続情報を受け取る- MySQL とのコネクションを確立する
- Todo 構造体
- todo テーブルのマッピングするための構造体
- SQL の SELECT 文で取得されたレコードの値設定や、INSERT/UPDATE 文を生成するのに利用
package todoapi
import (
"database/sql"
"time"
_ "github.com/go-sql-driver/mysql"
gorp "gopkg.in/gorp.v1"
)
func CreateDbMap(dbURL string) (*gorp.DbMap, error) {
ds, err := createDatasource(dbURL)
if err != nil {
return nil, err
}
db := &gorp.DbMap{
Db: ds,
Dialect: gorp.MySQLDialect{
Engine: "InnoDB",
Encoding: "utf8mb4",
},
}
db.AddTableWithName(Todo{}, "todo").SetKeys(true, "ID")
return db, nil
}
func createDatasource(dbURL string) (*sql.DB, error) {
db, err := sql.Open("mysql", dbURL)
if err != nil {
return nil, err
}
db.SetMaxIdleConns(2)
return db, nil
}
type Todo struct {
ID uint `db:"id" json:"id"`
Title string `db:"title" json:"title"`
Content string `db:"content" json:"content"`
Status string `db:"status" json:"status"`
Created time.Time `db:"created" json:"created"`
Updated time.Time `db:"updated" json:"updated"`
}
4.3.4 Handler を実装する
- Docker 関係ない。go の話。
- handler.go
- HTTP リクエストを処理するための Handler
- TODO の操作に対応した HTTP リクエストに応じて、参照・作成、更新を行う。Rails でいう Controller のような存在。
- cmd/main.go で NewTodoHandler 関数で作成した Handler を/todo というエンドポイントに設定。ServerHTTP 関数がリクエストを受ける。
package todoapi
import (
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
gorp "gopkg.in/gorp.v1"
)
// Handlerの中でDBへクエリを発行するため、DB接続の構造体を持つ
type TodoHandler struct {
master *gorp.DbMap
slave *gorp.DbMap
}
// Handlerの作成関数
func NewTodoHandler(master *gorp.DbMap, slave *gorp.DbMap) http.Handler {
return &TodoHandler{
master: master,
slave: slave,
}
}
// HTTPリクエストを受け、ビジネスロジックを実行してレスポンスを返す
func (h TodoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] RemoteAddr=%s\tUserAgent=%s", r.Method, r.RemoteAddr, r.Header.Get("User-Agent"))
switch r.Method {
case "GET":
h.serveGET(w, r)
return
case "POST":
h.servePOST(w, r)
return
case "PUT":
h.servePUT(w, r)
return
default:
NewErrorResponse(http.StatusMethodNotAllowed, fmt.Sprintf("%s is Unsupported method", r.Method)).Write(w)
return
}
}
type errorResponse struct {
Status int `json:"status"`
Message string `json:"message"`
}
func NewErrorResponse(status int, message string) *errorResponse {
return &errorResponse{
Status: status,
Message: message,
}
}
func (e *errorResponse) Write(w http.ResponseWriter) {
data, err := json.Marshal(e)
if err != nil {
log.Println("marshal error json is failed")
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(e.Status)
w.Write(data)
}
func (h TodoHandler) serveGET(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get("status")
if status == "" {
status = "TODO"
}
result, err := h.slave.Select(Todo{}, "SELECT * FROM todo WHERE status = ? ORDER BY updated DESC", status)
if err != nil {
log.Println(err.Error())
NewErrorResponse(http.StatusInternalServerError, "Execute Query is failed").Write(w)
return
}
todoItems := make([]*Todo, 0)
for _, e := range result {
todo := e.(*Todo)
todoItems = append(todoItems, todo)
}
data, err := json.Marshal(todoItems)
if err != nil {
log.Println(err.Error())
NewErrorResponse(http.StatusInternalServerError, "Marshal JSON is failed").Write(w)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write(data)
}
type TodoPostPayload struct {
Title string `json:"title"`
Content string `json:"content"`
}
func (h TodoHandler) servePOST(w http.ResponseWriter, r *http.Request) {
raw, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println(err.Error())
NewErrorResponse(http.StatusInternalServerError, "Read payload is failed").Write(w)
return
}
var payload TodoPostPayload
if err := json.Unmarshal(raw, &payload); err != nil {
log.Println(err.Error())
NewErrorResponse(http.StatusInternalServerError, "Parse payload is failed").Write(w)
return
}
now := time.Now()
todo := Todo{
Title: payload.Title,
Content: payload.Content,
Status: "TODO",
Created: now,
Updated: now,
}
if err := h.master.Insert(&todo); err != nil {
log.Println(err.Error())
NewErrorResponse(http.StatusInternalServerError, "Insert Data is failed").Write(w)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusCreated)
}
type TodoPutPayload struct {
ID uint `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Status string `json:"status"`
}
func (h TodoHandler) servePUT(w http.ResponseWriter, r *http.Request) {
raw, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println(err.Error())
NewErrorResponse(http.StatusInternalServerError, "Read payload is failed").Write(w)
return
}
var payload TodoPutPayload
if err := json.Unmarshal(raw, &payload); err != nil {
log.Println(err.Error())
NewErrorResponse(http.StatusInternalServerError, "Parse payload is failed").Write(w)
return
}
var target Todo
if err := h.slave.SelectOne(&target, "SELECT * FROM todo WHERE id = ?", payload.ID); err != nil {
if err == sql.ErrNoRows {
NewErrorResponse(http.StatusNotFound, fmt.Sprintf("id=%d is not found", payload.ID)).Write(w)
} else {
log.Println(err.Error())
NewErrorResponse(http.StatusInternalServerError, "Select todo is failed").Write(w)
}
return
}
now := time.Now()
todo := Todo{
ID: payload.ID,
Title: payload.Title,
Content: payload.Content,
Status: payload.Status,
Created: target.Created,
Updated: now,
}
if _, err := h.master.Update(&todo); err != nil {
log.Println(err.Error())
NewErrorResponse(http.StatusInternalServerError, "Update Data is failed").Write(w)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
4.3.5 API の Dockerfile
- Dockerfile
- ベースイメージは golang:1.13
- WORKDIR で実行ディレクトリを指定
- go get で依存ライブラリをインストール
- todoapi ディレクトリに移動&go build
- 出来上がった実行ファイルの bin/todoapi を/usr/local/bin にコピー
- CMD で todoapi を実行
FROM golang:1.13
WORKDIR /
ENV GOPATH /go
COPY . /go/src/github.com/gihyodocker/todoapi
RUN go get github.com/go-sql-driver/mysql
RUN go get gopkg.in/gorp.v1
RUN cd /go/src/github.com/gihyodocker/todoapi && go build -o bin/todoapi cmd/main.go
RUN cd /go/src/github.com/gihyodocker/todoapi && cp bin/todoapi /usr/local/bin/
CMD ["todoapi"]
- ch04/todoapi:latest というイメージ作成
$ docker image build -t ch04/todoapi:latest .
[+] Building 71.0s (11/11) FINISHED
=> [internal] load .dockerignore 0.1s
=> => transferring context: 68B 0.0s
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 381B 0.0s
=> [internal] load metadata for docker.io/library/golang:1.13 3.3s
=> [internal] load build context 0.0s
=> => transferring context: 8.52kB 0.0s
=> [1/7] FROM docker.io/library/golang:1.13@sha256:8ebb6d5a48deef738381b56b1d4cd33d99a5d608e0d03c5fe8dfa3f68d41a1f8 52.5s
=> => resolve docker.io/library/golang:1.13@sha256:8ebb6d5a48deef738381b56b1d4cd33d99a5d608e0d03c5fe8dfa3f68d41a1f8 0.0s
=> => sha256:d6ff36c9ec4822c9ff8953560f7ba41653b348a9c1136755e653575f58fbded7 50.40MB / 50.40MB 23.6s
=> => sha256:c958d65b3090aefea91284d018b2a86530a3c8174b72616c4e76993c696a5797 7.81MB / 7.81MB 4.1s
=> => sha256:8ebb6d5a48deef738381b56b1d4cd33d99a5d608e0d03c5fe8dfa3f68d41a1f8 2.36kB / 2.36kB 0.0s
=> => sha256:24bd48a274920bf47ead96c5a2db8e6a3fbe26e8ae27557c2caa9aeae562a998 1.79kB / 1.79kB 0.0s
=> => sha256:d6f3656320fe38f736f0ebae2556d09bf3bde9d663ffc69b153494558aec9a79 6.19kB / 6.19kB 0.0s
=> => sha256:edaf0a6b092f5673ec05b40edb606ce58881b2f40494251117d31805225ef064 10.00MB / 10.00MB 4.3s
=> => sha256:80931cf6881673fd161a3fd73e8971fe4a569fd7fbb44e956d261ca58d97dfab 51.83MB / 51.83MB 28.7s
=> => sha256:813643441356759e9202aeebde31d45192b5e5e6218cd8d2ad216304bf415551 68.67MB / 68.67MB 34.4s
=> => sha256:799f41bb59c9731aba2de07a7b3d49d5bc5e3a57ac053779fc0e405d3aed0b9e 120.17MB / 120.17MB 49.4s
=> => extracting sha256:d6ff36c9ec4822c9ff8953560f7ba41653b348a9c1136755e653575f58fbded7 1.4s
=> => extracting sha256:c958d65b3090aefea91284d018b2a86530a3c8174b72616c4e76993c696a5797 0.2s
=> => extracting sha256:edaf0a6b092f5673ec05b40edb606ce58881b2f40494251117d31805225ef064 0.2s
=> => extracting sha256:80931cf6881673fd161a3fd73e8971fe4a569fd7fbb44e956d261ca58d97dfab 1.5s
=> => sha256:16b5038bccc853e96f534bc85f4f737109ef37ad92d877b54f080a3c86b3cb3a 126B / 126B 29.0s
=> => extracting sha256:813643441356759e9202aeebde31d45192b5e5e6218cd8d2ad216304bf415551 1.4s
=> => extracting sha256:799f41bb59c9731aba2de07a7b3d49d5bc5e3a57ac053779fc0e405d3aed0b9e 2.6s
=> => extracting sha256:16b5038bccc853e96f534bc85f4f737109ef37ad92d877b54f080a3c86b3cb3a 0.0s
=> [2/7] COPY . /go/src/github.com/gihyodocker/todoapi 2.2s
=> [3/7] RUN go get github.com/go-sql-driver/mysql 3.5s
=> [4/7] RUN go get gopkg.in/gorp.v1 6.9s
=> [5/7] RUN cd /go/src/github.com/gihyodocker/todoapi && go build -o bin/todoapi cmd/main.go 2.0s
=> [6/7] RUN cd /go/src/github.com/gihyodocker/todoapi && cp bin/todoapi /usr/local/bin/ 0.4s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:cb5b230ef7bb2db9bfde1201e30a897a57ce5a4cebe2184275ca48f9b3ae40c1 0.0s
=> => naming to docker.io/ch04/todoapi:latest
- localhost:5000/ch04/todoapi:latest としてイメージをコピー
$ docker image tag ch04/todoapi:latest localhost:5000/ch04/todoapi:latest
- registry にイメージを push
$ docker image push localhost:5000/ch04/todoapi:latest
The push refers to repository [localhost:5000/ch04/todoapi]
6c107bf012b7: Pushed
a83e8e7419b6: Pushed
9f89592b8d08: Pushed
60cf81dd6ed7: Pushed
2133a099401e: Pushed
ec7523d82c84: Pushed
73b60240f5be: Pushed
7279468fdfad: Pushed
e5df62d9b33a: Pushed
7a9460d53218: Pushed
b2765ac0333a: Pushed
0ced13fcf944: Pushed
latest: digest: sha256:4cb1de36df9abf748c7c35c49109249f2db91237257360b3f0e123f298462227 size: 2847
4.3.6 Swarm 上で todoapi サービスを実行する
- stack ディレクトリに todo-app.yml 作成
version: "3"
services:
api:
image: registry:5000/ch04/todoapi:latest
deploy:
replicas: 2
environment:
TODO_BIND: ":8080"
TODO_MASTER_URL: "gihyo:gihyo@tcp(todo_mysql_master:3306)/tododb?parseTime=true"
TODO_SLAVE_URL: "gihyo:gihyo@tcp(todo_mysql_slave:3306)/tododb?parseTime=true"
networks:
- todoapp
networks:
todoapp:
external: true
- todo_app という Stack 名でデプロイ
$ docker container exec -it manager docker stack deploy -c stack/todo-app.yml todo_app
Creating service todo_app_api
- docker service logs -f で todo_app_api が正常に起動しているか確認
- 「Listen HTTP Server」が表示されていれば API サーバとしてリクエストを受けられる状態
$ docker container exec -it manager docker service logs -f todo_app_api
todo_app_api.2.nuymt58kziy1@095bc5ce9b86 | 2020/10/31 17:14:45 Listen HTTP Server
todo_app_api.1.ztwpncv4aqg1@38cac483943e | 2020/10/31 17:14:45 Listen HTTP Server
今日の学び
- 今回はほぼ go の話。
- Docker 関係ないけど、go 言語のみ FW なしで todoapi が作れるのが凄い。あと、ファイル数少なくてうらやましい(FW じゃないから当たり前か)。
- docker service logs でサービスで稼働しているコンテナのログが確認できる。