Go Conference Tokyo 2019 Spring で発表したコンテナベースのGoアプリケーションの設計・実装の実践紹介です。
© - BASE, Inc.Design considerationsfor Container-basedGo application. . #goconGo Conference Tokyo Spring - @hgsgtk
View Slide
© - BASE, Inc.はじめに• このトークスライドは @hgsgtk より #gocon にシェアしています• 時間内に含めれなかった内容を “Extra Talk” として盛り込んでいます
© - BASE, Inc.このトークに⾄るまでの背景• 舞台は新サービスの開発現場• Go⾔語でのAPI開発をやっていく• Dockerコンテナベースで動かす• 本番環境での Go‧Dockerは初めての試み• 試⾏錯誤の末、昨年12⽉本番リリース
© - BASE, Inc.現場での実践での試⾏錯誤にて...コンテナだとこれまでと何が違う?何を意識したらいい?
© - BASE, Inc.現場での実践での試⾏錯誤にて...実際に雰囲気でGoの実装までやったけどこれでいいのか?みんなどうしてるの‧‧‧。
© - BASE, Inc.このトークで持ち帰ってほしいものコンテナを使う場合に意識した設計考慮点設計⽅針を得る拠り所となるガイドライン設計⽅針に具体的なGoでの設計‧実装事例
© - BASE, Inc.これからトークする⼈東⼝和暉 (Kazuki Higashiguchi)Twitter / GitHub : @hgsgtkバックエンドエンジニアBASE BANK, Inc. / Dev DivisionGo歴:- 趣味:2017.7〜- 仕事:2018.6〜
© - BASE, Inc.トークの舞台:BASE BANKとは銀⾏をかんたんにし、全ての⼈が挑戦できる世の中にMISSIONhttps://thebase.in/yellbank• BASE, Inc の100%⼦会社• 即座に資⾦調達ができる⾦融サービス「YELL BANK(エールバンク)」を運営• GoでAPIを開発し、Dockerコンテナで動かす• ECS/Fargateで運⽤
© - BASE, Inc.トーク構成• 事例の全体構成と設計考慮点⼀覧• 設計⽅針と実装• Configuration• Logging• Monitoring• まとめ
© - BASE, Inc.事例の全体構成:トーク内での登場⼈物
© - BASE, Inc.事例の全体構成:トーク内での登場⼈物今回の主⼈公たち
© - BASE, Inc.コンテナを使う上で最初に持った設計の考慮点設定情報どうする?ログどうやる?動いてるかどう監視する?
© - BASE, Inc.考慮点を設計に落とし込むために• 世の中で語られているベストプラクティスを探す• コンテナベースアプリケーション設計に関する、2つのガイドラインを参考にした
© - BASE, Inc.ガイドライン1:”Beyond the Twelve-Factor App”• Pivotal社が公開しているガイドライン• Beyond “The Twelve-FactorApp”• https:// factor.net/• Herokuの中の⼈が書いたクラウドアプリケーションのベストプラクティス• Original + New = の原則https://content.pivotal.io/blog/beyond-the-twelve-factor-app
© - BASE, Inc.ガイドライン2:”Red Hat White Paper - コンテナベース‧アプリケーションの設計原則”• Red Hat社が公開しているホワイトペーパー• コンテナベースアプリケーションの設計について、 7 個の原則についてまとめられている• “The Twelve-Factor App” などからヒントを得ているhttps://www.redhat.com/ja/resources/cloud-native-container-design-whitepaper
© - BASE, Inc.ガイドラインの使い⽅• ベストプラクティスの⼀つとして参考にする• トピックに関連する原則を参照する• 2つのガイドラインから設計⽅針を⾒出す
© - BASE, Inc.ガイドラインと共に設計⽅針を⾒出す設定情報どうする?ログどうやる?動いてるかどう監視する?Guideline
© - BASE, Inc.ガイドラインと共に設計⽅針を⾒出す設定情報どうする?ログどうやる?動いてるかどう監視する?GuidelineConfiguration Logging Monitoring
© - BASE, Inc.実装に⾄るまでの思考過程
© - BASE, Inc.Configuration: 設定情報の特性• 環境ごとに異なる• ex. 本番/検証/QA• 秘匿情報を含む• ex. DB,Redis等の認証情報• 規模に応じて情報が多くなる• ex. DB,Redisの(Master/Replica),HTTP Port
© - BASE, Inc.Configuration: 設計⽅針と実装
© - BASE, Inc.Configuration: 満たしたい要求• 階層的管理• 多くなる設定情報を階層的に整理したい• 秘匿情報の管理• パスワードなどの秘匿情報を必要以上に参照可能にしたくない
© - BASE, Inc.Configuration: 関連するガイドライン• “ . CONFIGURATION, CREDENTIALS, ANDCODE” in Beyond the Twelve-Factor App• “イメージ不変性の原則” in コンテナベース‧アプリケーションの設計原則(Red Hat)
© - BASE, Inc.概要:“05. CONFIGURATION, CREDENTIALS, ANDCODE”• 設定情報はコードから取り除く• = Version Control Systemに含めない• “Treat Your Apps Like Open Source”• 設定を分離する⼀番の⽅法は環境変数への格納• デプロイごとに変更可能• ⾔語‧OSに依存しない
© - BASE, Inc.概要:“イメージ不変性の原則”• コンテナ化アプリケーションは不変• ビルドされた後、異なる環境間で変化することは想定されていない• “ランタイムデータ”の保存は外部⼿段を利⽤する• 設定情報はビルド時ではなく実⾏時に必要なデータ• コンテナ外に保存するべき。• 外部化した設定を環境によって使い分ける
© - BASE, Inc.Extra Talk: アプリケーション構成要素と設定情報• アプリケーション構成要素は次の通り• Runtime Engine• Code• Dependencies• Configuration• Configurationはコンテナ起動時に注⼊されるのが望ましい
© - BASE, Inc.Configuration: 設計⽅針設定情報を外部化する環境変数を利⽤する階層管理ができ、秘匿情報を取り扱える外部⼿段を利⽤する
© - BASE, Inc.Configuration: 設計⽅針に対する実装設定情報を外部化する環境変数を利⽤する階層管理ができ、秘匿情報を取り扱える外部⼿段を利⽤する
© - BASE, Inc.Configuration: 環境変数を⽤いる実装• 設定情報を扱う config パッケージを作成• API起動時に環境変数から設定情報を取得• 設定情報を保持する構造体へParse• github.com/caarlos /env を⽤いる
© - BASE, Inc.設定情報を扱う config パッケージを作成package configimport ("github.com/caarlos0/env/v5""github.com/pkg/errors")func NewConfig() (Config, error) {c := Config{}if err := env.Parse(&c); err != nil {return Config{}, errors.Wrap(err, "failed to parsemaster configuration from environment variable")}return c, nil}
© - BASE, Inc.API起動時に環境変数から設定情報を取得package mainfunc main() {// Get configconf, err := config.NewConfig()if err != nil {logger.Logger().Error("failed to createconfiguration.",zap.Error(err))os.Exit(1)}// Ҏ߱ͷॲཧ}
© - BASE, Inc.設定情報を保持する構造体type Config struct {MasterDB MasterDBConfigReplicaDB ReplicaDBConfigHTTP HTTPConfigRedis RedisConfig}type MasterDBConfig struct {Host string `env:"MASTER_DB_HOST,required"`Name string `env:"MASTER_DB_NAME,required"`User string `env:"MASTER_DB_USER,required"`Password string `env:"MASTER_DB_PASSWORD,required"`Port int `env:"MASTER_DB_PORT,required"`SQLMode string `env:"MASTER_DB_SQL_MODE,required"`}
© - BASE, Inc.設定情報を保持する構造体type Config struct {MasterDB MasterDBConfigReplicaDB ReplicaDBConfigHTTP HTTPConfigRedis RedisConfig}type MasterDBConfig struct {Host string `env:"MASTER_DB_HOST,required"`Name string `env:"MASTER_DB_NAME,required"`User string `env:"MASTER_DB_USER,required"`Password string `env:"MASTER_DB_PASSWORD,required"`Port int `env:"MASTER_DB_PORT,required"`SQLMode string `env:"MASTER_DB_SQL_MODE,required"`}Struct tag: `env` に対応する環境変数のキーを設定required を設定すると未設定の場合にParseerrorを出すことができる
© - BASE, Inc.caarlos /env での構造体へのParsepackage configimport ("github.com/caarlos0/env/v5""github.com/pkg/errors")func NewConfig() (Config, error) {c := Config{}if err := env.Parse(&c); err != nil {return Config{}, errors.Wrap(err, "failed to parsemaster configuration from environment variable")}return c, nil}
© - BASE, Inc.config パッケージ全体package configimport ("github.com/caarlos0/env/v5""github.com/pkg/errors")func NewConfig() (Config, error) {c := Config{}if err := env.Parse(&c); err != nil {return Config{}, errors.Wrap(err, "failed to parse master configuration from environment variable")}return c, nil}type Config struct {MasterDB MasterDBConfigReplicaDB ReplicaDBConfigHTTP HTTPConfigRedis RedisConfig}type MasterDBConfig struct {Host string `env:"MASTER_DB_HOST,required"`Name string `env:"MASTER_DB_NAME,required"`User string `env:"MASTER_DB_USER,required"`Password string `env:"MASTER_DB_PASSWORD,required"`Port int `env:"MASTER_DB_PORT,required"`SQLMode string `env:"MASTER_DB_SQL_MODE,required"`}// MasterDBConfig Ҏ֎ͷͷઃఆใͷstruct͕ଓ͘
© - BASE, Inc.Extra Talk: 環境変数を⽤いるコードのユニットテスト• 環境変数を使うコードはテストしにくい• なるべく使⽤箇所を制限したほうがいい• 環境変数を使⽤する関数のテストコードでは、テスト実⾏前の状態に戻しておく必要がある• See also: テストしやすいGoコードのデザイン bydeeeet さん
© - BASE, Inc.Extra Talk: configパッケージのユニットテストfunc TestNewConfig(t *testing.T) {inputEnvs := map[string]string{"MASTER_DB_HOST": "test_db_host","MASTER_DB_NAME": "test_db_name","MASTER_DB_USER": "test_db_user","MASTER_DB_PORT": "3306","MASTER_DB_PASSWORD": "test_db_password","MASTER_DB_SQL_MODE": "TEST_SQL_MODE",}restore := setEnvs(inputEnvs)defer restore()// ଓ͘}テスト⽤に設定したい環境変数のkey-valueをmap[string]string型で定義
© - BASE, Inc.Extra Talk: configパッケージのユニットテストfunc TestNewConfig(t *testing.T) {inputEnvs := map[string]string{"MASTER_DB_HOST": "test_db_host","MASTER_DB_NAME": "test_db_name","MASTER_DB_USER": "test_db_user","MASTER_DB_PORT": "3306","MASTER_DB_PASSWORD": "test_db_password","MASTER_DB_SQL_MODE": "TEST_SQL_MODE",}restore := setEnvs(inputEnvs)defer restore()// ଓ͘}環境変数を設定するテストヘルパーを実⾏戻り値で返ってくる関数を defer 実⾏して状態を戻す
© - BASE, Inc.package config_testfunc setEnvs(envs map[string]string) func() {prevs := map[string]string{}for k, v := range envs {prev := os.Getenv(k)prevs[k] = prevos.Setenv(k, v)}return func() {for k, v := range prevs {os.Setenv(k, v)}}}Extra Talk: 環境変数を設定するテストヘルパー
© - BASE, Inc.Extra Talk: configパッケージのユニットテスト全体func TestNewConfig(t *testing.T) {inputEnvs := map[string]string{"MASTER_DB_HOST": "test_db_host","MASTER_DB_NAME": "test_db_name","MASTER_DB_USER": "test_db_user","MASTER_DB_PORT": "3306","MASTER_DB_PASSWORD": "test_db_password","MASTER_DB_SQL_MODE": "TEST_SQL_MODE",}restore := setEnvs(inputEnvs)defer restore()got, err := config.NewConfig()if err != nil {t.Fatalf("config.NewConfig got unexpected error %#v", err)}want := config.Config{MasterDB: config.MasterDBConfig{User: "test_db_user",Password: "test_db_password",Host: "test_db_host",Name: "test_db_name",Port: 3306,SQLMode: "TEST_SQL_MODE",},}if diff := cmp.Diff(got, want); diff != "" {t.Errorf("NewConfig() got differs: (-got +want)\n%s", diff)}}
© - BASE, Inc.設定外部化のキーマン
© - BASE, Inc.ECS/FargateでParameter Storeを利⽤する事例• Parameter Storeに設定情報を保存する• Key Management Store による暗号キーで暗号化• コンテナ起動時に設定情報を取得• See also:• ECS(Fargate)でコンテナアプリケーションを動かすための設定情報の扱い⽅ - BASE Developer’sBlog
© - BASE, Inc.(振り返り)設計⽅針と実装:Configuration• 設定情報を外部に保存、環境変数を利⽤する⽅式にした• 本番/検証/QAと異なる環境に同じコンテナイメージを利⽤できる• 外部化しコードから分離することで、情報閲覧権限を絞ることができた
© - BASE, Inc.(振り返り)ガイドラインと共に設計⽅針を⾒出す設定情報どうする?ログどうやる?動いてるかどう監視する?GuidelineConfiguration Logging Monitoring
© - BASE, Inc.(CM)トークの舞台:BASEとはネットショップ作成サービス「BASE」ショッピングアプリ「BASE」価値の交換をよりシンプルにし、世界中の⼈々が最適な経済活動を⾏えるようにする。MISSION
© - BASE, Inc.
© - BASE, Inc.Logging: 設計⽅針と実装
© - BASE, Inc.Logging: 満たしたい要求• リアルタイムでの参照がしたい• 「今何が起こっているのか」を知る情報• トラブルシューティング‧デバッグ• 可⽤性、ログの⽋損を避けたい• 検索しやすさ• 問題の原因調査のための検索しやすさ
© - BASE, Inc.Logging: 関連するガイドライン• “ . LOGS” in Beyond the Twelve-Factor App• “⾼観測可能性の原則” in コンテナベース‧アプリケーションの設計原則(Red Hat)
© - BASE, Inc.概要:“06. LOGS”• ログをイベントストリームとして扱う• ファイルシステムに依存しない• 全てのログは、STDOUT/STDERRに書き出す• ログの集約‧分析は、ElasticSearch‧Logstash‧Kibanaといったツールを活⽤する
© - BASE, Inc.概要:“⾼観測可能性の原則”• コンテナを “ブラックボックス” として扱う• 重要なイベントを STDOUT/STDERR に記録し、Fluentdなどツールを活⽤してログ集約をする• 活動状況や準備状況など、様々な状態チェックに対してAPIを提供する
© - BASE, Inc.加えて参考になる書籍『⼊⾨ 監視』• “⼊⾨ 監視—モダンなモニタリングのためのデザインパターン”• 著: Mike Julian / 訳: 松浦隼⼈https://www.oreilly.co.jp/books/ /
© - BASE, Inc.構造化ログを使う• ログをJSON等で構造化するメリット• キー‧値のペアの集合になる• 意味を理解しやすくなる• 情報を抽出できるようになる• See also:• 書籍『⼊⾨監視』 - 7.4 アプリケーションロギング
© - BASE, Inc.Logging: 設計⽅針ログを構造化するSTDOUT/STDERRに書き出すツールを活⽤したログの集約‧分析
© - BASE, Inc.Logging: STDOUT/STDERRに構造化ログを書き出す例• ログ書き出しを扱う logger パッケージを作成• STDOUT にログを書き出す• ログの内容は JSON 形式で構造化する• ログライブラリに github.com/uber-go/zap を⽤いる。
© - BASE, Inc.ログを扱うlogger パッケージpackage logger// Writer specifies output of logger.var Writer zapcore.WriteSyncer = os.Stdout// Init replace global zap logger to customlogger.func Init(output zapcore.WriteSyncer) {logger := newLogger(output)zap.ReplaceGlobals(logger)}// Logger return logger instance.func Logger() *zap.Logger {return zap.L()}
© - BASE, Inc.STDOUTにログを書き出すpackage logger// Writer specifies output of logger.var Writer zapcore.WriteSyncer = os.Stdout// Init replace global zap logger to customlogger.func Init(output zapcore.WriteSyncer) {logger := newLogger(output)zap.ReplaceGlobals(logger)}// Logger return logger instance.func Logger() *zap.Logger {return zap.L()}ログの書き出し先に os.Stdout を指定
© - BASE, Inc.STDOUTにログを書き出すpackage logger// Writer specifies output of logger.var Writer zapcore.WriteSyncer = os.Stdout// Init replace global zap logger to customlogger.func Init(output zapcore.WriteSyncer) {logger := newLogger(output)zap.ReplaceGlobals(logger)}// Logger return logger instance.func Logger() *zap.Logger {return zap.L()}起動時に main関数から呼び出す。zap.ReplaceGlobals() によってカスタムロガーに差し替える
© - BASE, Inc.カスタムロガーを⽤意するpackage loggerfunc newLogger(writer zapcore.WriteSyncer) *zap.Logger {atom := zap.NewAtomicLevel()encoderCfg := zap.NewProductionEncoderConfig()encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoderbl := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg),zapcore.Lock(writer),atom,))l := bl.With(zap.String("out", "stdout"))return l}
© - BASE, Inc.STDOUTにログを書き出すpackage logger// Writer specifies output of logger.var Writer zapcore.WriteSyncer = os.Stdout// Init replace global zap logger to customlogger.func Init(output zapcore.WriteSyncer) {logger := newLogger(output)zap.ReplaceGlobals(logger)}// Logger return logger instance.func Logger() *zap.Logger {return zap.L()}ログ書き出しのタイミングで、zapのグローバルロガーを取得する
© - BASE, Inc.ログを実際に書き出すpackage mainfunc main() {// Get configconf, err := config.NewConfig()if err != nil {logger.Logger().Error("failed to createconfiguration.",zap.Error(err))os.Exit(1)}// Ҏ߱ͷॲཧ}logger.Logger() からロガーを取り出し利⽤する
© - BASE, Inc.ログの集約‧分析のキーマン
© - BASE, Inc.Extra Talk: ECS/FargateからKibanaでログ可視化• Fargate -> CloudWatchLogs -> S ->ElasticSearch & Kibana の流れ• See also• AWS Fargateで動いているプログラムのログをElasticsearch/Kibanaで可視化 by @tomy rider
© - BASE, Inc.(振り返り)設計⽅針と実装:Logging• uber-go/zap を利⽤して STDOUT に JSON形式でログを書き出す• ファイルシステムに依存しないのでコンテナの破棄容易性が⾼まった• 構造化することによる抽出のしやすさ• CloudWatchLogsに流れてくるログを awslogs‧jqコマンドを組み合わせて抽出する
© - BASE, Inc.Monitoring: 設計⽅針と実装
© - BASE, Inc.Monitoring: 満たしたい要求• 健康状態をチェックしたい• アプリケーションの健康状態のモニタリング• メトリクスの継続的な取得• メトリクスをとって状態をWatchしておきたい
© - BASE, Inc.Monitoring: 関連するガイドライン• “⾼観測可能性の原則” in コンテナベース‧アプリケーションの設計原則(Red Hat)
© - BASE, Inc.(振り返り)概要:“⾼観測可能性の原則”• コンテナを “ブラックボックス” として扱う• 重要なイベントを STDOUT/STDERR に記録し、Fluentdなどツールを活⽤してログ集約をする• 活動状況や準備状況など、様々な状態チェックに対してAPIを提供する
© - BASE, Inc.Monitoring: 設計⽅針依存サービスの利⽤状況も確認する健康状態を伝えるAPIの提供コンテナ内のメトリクス取得
© - BASE, Inc.Monitoring: 健康状態を伝えるエンドポイントの実装• ヘルスチェックを⾏うエンドポイントを作成する• “Health endpoint pattern” in 『⼊⾨ 監視』• 依存サービスとの接続状態もエンドポイントで確認する• ex. 依存サービス DB‧Redis etc
© - BASE, Inc.ヘルスチェックを⾏うエンドポイントを作成するfunc (c *Handler) Check(whttp.ResponseWriter, r *http.Request) {w.WriteHeader(http.StatusOK)}
© - BASE, Inc.依存サービスとの接続状態もエンドポイントで確認するfunc (c *Handler) DeepCheck(w http.ResponseWriter, r *http.Request){if err := c.masterDB.Ping(); err != nil {res := ErrResponse{Message: fmt.Sprintf("failed to ping master datatabasebecause of error: %s", err.Error()),Status: http.StatusServiceUnavailable,}respondJSON(w, res)return}if err := c.redis.Ping().Err(); err != nil {res := ErrResponse{Message: fmt.Sprintf("failed to ping redis server becauseof error: %s", err.Error()),Status: http.StatusServiceUnavailable,}respondJSON(w, res)}w.WriteHeader(http.StatusOK)}
© - BASE, Inc.Extra Talk: Monitoring: 設計⽅針依存サービスの利⽤状況も確認する健康状態を伝えるAPIの提供コンテナ内のメトリクス取得
© - BASE, Inc.Extra Talk: Mackerel Container Agentでのコンテナ内監視• Mackerel Container Agent を利⽤したコンテナ監視• https://mackerel.io/ja/docs/entry/howto/container-agent• コンテナ内部のCPU, Memory, Networkなどを監視できる• アプリケーションコンテナのサイドカーとして起動
© - BASE, Inc.(振り返り)設計⽅針と実装:Monitoring• ヘルスチェックを⾏うエンドポイントを作成した• ALBやMackerelからの外形監視に利⽤できた• 起動したかどうかのデバッグにも使える
© - BASE, Inc.まとめ• コンテナアプリケーションの設計ベストプラクティスから設計⽅針を⾒出した• 設計⽅針から実践したGoでの実装事例を紹介した• 現場によって様々な設計‧実装判断があるが、⼀つの参考事例として参考になれば嬉しい
End role
© - BASE, Inc.End role: 関連アウトプット• Container based application Design Real Practices - #dockertokyo• https://speakerdeck.com/hgsgtk/container-based-application-design-real-practices• Container-based Application Design Reference and Practice -#dockertokyo• https://speakerdeck.com/hgsgtk/container-based-application-design-reference-and-practice-number-dockertokyo
© - BASE, Inc.End role: 関連アウトプット• ECS(Fargate)でコンテナアプリケーションを動かすための設定情報の扱い⽅ - BASE Developer’s Blog• https://devblog.thebase.in/entry/ / / /• アプリケーション監視のパターン「Health エンドポイントパターン」を実践する - BASE Developer’s Blog• https://devblog.thebase.in/entry/ / / /
© - BASE, Inc.End role: 関連アウトプット• AWS Fargateで動いているプログラムのログをElasticsearch/Kibanaで可視化 by @tomy rider• https://qiita.com/tomy rider/items/ aa dd• CircleCIとecspressoによるECSへのデプロイメントパイプライン by@fumikony• https://devblog.thebase.in/entry/ / / /
© - BASE, Inc.End role: 参考書籍• 『⼊⾨ 監視 モダンなモニタリングのためのデザインパターン• https://www.oreilly.co.jp/books/ /• 『分散システムデザインパターン コンテナを使ったスケーラブルなサービスの設計』• https://www.oreilly.co.jp/books/ /
© - BASE, Inc.End role: 設計時参考URL• Beyond the Twelve-Factor App• https://content.pivotal.io/blog/beyond-the-twelve-factor-app• Red Hat White paper - コンテナベース‧アプリケーションの設計原則• https://www.redhat.com/ja/resources/cloud-native-container-design-whitepaper (⽇本語版)• https://www.redhat.com/en/resources/cloud-native-container-design-whitepaper (English Edition)• AWS Cloud Design Patterns• http://en.clouddesignpattern.org/index.php/Main_Page
© - BASE, Inc.End role: 実装時参考URL• テストしやすいGoコードのデザイン by deeeet さん• https://go-talks.appspot.com/github.com/tcnksm/talks/ / /golang-tokyo/golang-tokyo.slide#• コンテナを監視する by Mackerel• https://mackerel.io/ja/docs/entry/howto/container-agent
Any Question or Suggestion?