Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Design considerations for Container based Go ap...

Design considerations for Container based Go application

Go Conference Tokyo 2019 Spring で発表したコンテナベースのGoアプリケーションの設計・実装の実践紹介です。

Kazuki Higashiguchi

May 18, 2019
Tweet

More Decks by Kazuki Higashiguchi

Other Decks in Technology

Transcript

  1. © - BASE, Inc. Design considerations for Container-based Go application

    . . #gocon Go Conference Tokyo Spring - @hgsgtk
  2. © - BASE, Inc. はじめに • このトークスライドは @hgsgtk より #gocon

    にシェ アしています • 時間内に含めれなかった内容を “Extra Talk” として 盛り込んでいます
  3. © - BASE, Inc. このトークに⾄るまでの背景 • 舞台は新サービスの開発現場 • Go⾔語でのAPI開発をやっていく •

    Dockerコンテナベースで動かす • 本番環境での Go‧Dockerは初めての試み • 試⾏錯誤の末、昨年12⽉本番リリース
  4. © - BASE, Inc. これからトークする⼈ 東⼝和暉 (Kazuki Higashiguchi) Twitter /

    GitHub : @hgsgtk バックエンドエンジニア BASE BANK, Inc. / Dev Division Go歴: - 趣味:2017.7〜 - 仕事:2018.6〜
  5. © - BASE, Inc. トークの舞台:BASE BANKとは 銀⾏をかんたんにし、全ての⼈が挑戦できる世の中に MISSION https://thebase.in/yellbank •

    BASE, Inc の100%⼦会社 • 即座に資⾦調達ができる⾦融サービ ス「YELL BANK(エールバンク)」 を運営 • GoでAPIを開発し、Dockerコンテ ナで動かす • ECS/Fargateで運⽤
  6. © - BASE, Inc. ガイドライン1:”Beyond the Twelve-Factor App” • Pivotal社が公開しているガイド

    ライン • Beyond “The Twelve-Factor App” • https:// factor.net/ • Herokuの中の⼈が書いたクラ ウドアプリケーションのベスト プラクティス • Original + New = の原則 https://content.pivotal.io/blog/beyond-the-twelve- factor-app
  7. © - 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
  8. © - BASE, Inc. Configuration: 設定情報の特性 • 環境ごとに異なる • ex.

    本番/検証/QA • 秘匿情報を含む • ex. DB,Redis等の認証情報 • 規模に応じて情報が多くなる • ex. DB,Redisの(Master/Replica),HTTP Port
  9. © - BASE, Inc. Configuration: 満たしたい要求 • 階層的管理 • 多くなる設定情報を階層的に整理したい

    • 秘匿情報の管理 • パスワードなどの秘匿情報を必要以上に参照可能に したくない
  10. © - BASE, Inc. Configuration: 関連するガイドライン • “ . CONFIGURATION,

    CREDENTIALS, AND CODE” in Beyond the Twelve-Factor App • “イメージ不変性の原則” in コンテナベース‧アプリ ケーションの設計原則(Red Hat)
  11. © - BASE, Inc. 概要:“05. CONFIGURATION, CREDENTIALS, AND CODE” •

    設定情報はコードから取り除く • = Version Control Systemに含めない • “Treat Your Apps Like Open Source” • 設定を分離する⼀番の⽅法は環境変数への格納 • デプロイごとに変更可能 • ⾔語‧OSに依存しない
  12. © - BASE, Inc. 概要:“イメージ不変性の原則” • コンテナ化アプリケーションは不変 • ビルドされた後、異なる環境間で変化することは想 定されていない

    • “ランタイムデータ”の保存は外部⼿段を利⽤する • 設定情報はビルド時ではなく実⾏時に必要なデータ • コンテナ外に保存するべき。 • 外部化した設定を環境によって使い分ける
  13. © - BASE, Inc. Extra Talk: アプリケーション構成要素と設定情報 • アプリケーション構成要 素は次の通り

    • Runtime Engine • Code • Dependencies • Configuration • Configurationはコンテナ 起動時に注⼊されるのが 望ましい
  14. © - BASE, Inc. Configuration: 環境変数を⽤いる実装 • 設定情報を扱う config パッケージを作成

    • API起動時に環境変数から設定情報を取得 • 設定情報を保持する構造体へParse • github.com/caarlos /env を⽤いる
  15. © - BASE, Inc. 設定情報を扱う config パッケージを作成 package config import

    ( "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 }
  16. © - BASE, Inc. API起動時に環境変数から設定情報を取得 package main func main() {

    // Get config conf, err := config.NewConfig() if err != nil { logger.Logger().Error("failed to create configuration.", zap.Error(err)) os.Exit(1) } // Ҏ߱ͷॲཧ }
  17. © - BASE, Inc. 設定情報を保持する構造体 type Config struct { MasterDB

    MasterDBConfig ReplicaDB ReplicaDBConfig HTTP HTTPConfig Redis 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"` }
  18. © - BASE, Inc. 設定情報を保持する構造体 type Config struct { MasterDB

    MasterDBConfig ReplicaDB ReplicaDBConfig HTTP HTTPConfig Redis 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 を設定すると未設定の場合にParse errorを出すことができる
  19. © - BASE, Inc. caarlos /env での構造体へのParse package config import

    ( "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 }
  20. © - BASE, Inc. config パッケージ全体 package config import (

    "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 MasterDBConfig ReplicaDB ReplicaDBConfig HTTP HTTPConfig Redis 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͕ଓ͘
  21. © - BASE, Inc. Extra Talk: 環境変数を⽤いるコードのユニットテスト • 環境変数を使うコードはテストしにくい •

    なるべく使⽤箇所を制限したほうがいい • 環境変数を使⽤する関数のテストコードでは、テスト 実⾏前の状態に戻しておく必要がある • See also: テストしやすいGoコードのデザイン by deeeet さん
  22. © - 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型で定義
  23. © - 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 実⾏して状態を戻す
  24. © - BASE, Inc. package config_test func setEnvs(envs map[string]string) func()

    { prevs := map[string]string{} for k, v := range envs { prev := os.Getenv(k) prevs[k] = prev os.Setenv(k, v) } return func() { for k, v := range prevs { os.Setenv(k, v) } } } Extra Talk: 環境変数を設定するテストヘルパー
  25. © - 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) } }
  26. © - BASE, Inc. ECS/FargateでParameter Storeを利⽤する事例 • Parameter Storeに設定情報を保存 する

    • Key Management Store による暗 号キーで暗号化 • コンテナ起動時に設定情報を取得 • See also: • ECS(Fargate)でコンテナアプリ ケーションを動かすための設定情 報の扱い⽅ - BASE Developer’s Blog
  27. © - BASE, Inc. Logging: 満たしたい要求 • リアルタイムでの参照がしたい • 「今何が起こっているのか」を知る情報

    • トラブルシューティング‧デバッグ • 可⽤性、ログの⽋損を避けたい • 検索しやすさ • 問題の原因調査のための検索しやすさ
  28. © - BASE, Inc. Logging: 関連するガイドライン • “ . LOGS”

    in Beyond the Twelve-Factor App • “⾼観測可能性の原則” in コンテナベース‧アプリ ケーションの設計原則(Red Hat)
  29. © - BASE, Inc. 概要:“06. LOGS” • ログをイベントストリームとして扱う • ファイルシステムに依存しない

    • 全てのログは、STDOUT/STDERRに書き出す • ログの集約‧分析は、ElasticSearch‧Logstash‧ Kibanaといったツールを活⽤する
  30. © - BASE, Inc. 概要:“⾼観測可能性の原則” • コンテナを “ブラックボックス” として扱う •

    重要なイベントを STDOUT/STDERR に記録し、 Fluentdなどツールを活⽤してログ集約をする • 活動状況や準備状況など、様々な状態チェックに対 してAPIを提供する
  31. © - BASE, Inc. 加えて参考になる書籍『⼊⾨ 監視』 • “⼊⾨ 監視—モダンなモニ タリングのためのデザイ

    ンパターン” • 著: Mike Julian / 訳: 松浦 隼⼈ https://www.oreilly.co.jp/books/ /
  32. © - BASE, Inc. 構造化ログを使う • ログをJSON等で構造化するメリット • キー‧値のペアの集合になる •

    意味を理解しやすくなる • 情報を抽出できるようになる • See also: • 書籍『⼊⾨監視』 - 7.4 アプリケーションロギング
  33. © - BASE, Inc. Logging: STDOUT/STDERRに構造化ログを書き出す例 • ログ書き出しを扱う logger パッケージを作成

    • STDOUT にログを書き出す • ログの内容は JSON 形式で構造化する • ログライブラリに github.com/uber-go/zap を⽤い る。
  34. © - BASE, Inc. ログを扱うlogger パッケージ package logger // Writer

    specifies output of logger. var Writer zapcore.WriteSyncer = os.Stdout // Init replace global zap logger to custom logger. func Init(output zapcore.WriteSyncer) { logger := newLogger(output) zap.ReplaceGlobals(logger) } // Logger return logger instance. func Logger() *zap.Logger { return zap.L() }
  35. © - BASE, Inc. STDOUTにログを書き出す package logger // Writer specifies

    output of logger. var Writer zapcore.WriteSyncer = os.Stdout // Init replace global zap logger to custom logger. func Init(output zapcore.WriteSyncer) { logger := newLogger(output) zap.ReplaceGlobals(logger) } // Logger return logger instance. func Logger() *zap.Logger { return zap.L() } ログの書き出し先に os.Stdout を指定
  36. © - BASE, Inc. STDOUTにログを書き出す package logger // Writer specifies

    output of logger. var Writer zapcore.WriteSyncer = os.Stdout // Init replace global zap logger to custom logger. 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() によってカスタムロガーに差し替える
  37. © - BASE, Inc. カスタムロガーを⽤意する package logger func newLogger(writer zapcore.WriteSyncer)

    *zap.Logger { atom := zap.NewAtomicLevel() encoderCfg := zap.NewProductionEncoderConfig() encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder bl := zap.New(zapcore.NewCore( zapcore.NewJSONEncoder(encoderCfg), zapcore.Lock(writer), atom, )) l := bl.With(zap.String("out", "stdout")) return l }
  38. © - BASE, Inc. STDOUTにログを書き出す package logger // Writer specifies

    output of logger. var Writer zapcore.WriteSyncer = os.Stdout // Init replace global zap logger to custom logger. func Init(output zapcore.WriteSyncer) { logger := newLogger(output) zap.ReplaceGlobals(logger) } // Logger return logger instance. func Logger() *zap.Logger { return zap.L() } ログ書き出しのタイミングで、 zapのグローバルロガーを取得する
  39. © - BASE, Inc. ログを実際に書き出す package main func main() {

    // Get config conf, err := config.NewConfig() if err != nil { logger.Logger().Error("failed to create configuration.", zap.Error(err)) os.Exit(1) } // Ҏ߱ͷॲཧ } logger.Logger() からロガーを取り出し利⽤する
  40. © - BASE, Inc. Extra Talk: ECS/FargateからKibanaでログ可視化 • Fargate ->

    CloudWatchLogs -> S -> ElasticSearch & Kibana の流れ • See also • AWS Fargateで動いているプログラムのログをElasticsearch/ Kibanaで可視化 by @tomy rider
  41. © - BASE, Inc. (振り返り)設計⽅針と実装:Logging • uber-go/zap を利⽤して STDOUT に

    JSON形式でロ グを書き出す • ファイルシステムに依存しないのでコンテナの破棄容 易性が⾼まった • 構造化することによる抽出のしやすさ • CloudWatchLogsに流れてくるログを awslogs‧jq コマンドを組み合わせて抽出する
  42. © - BASE, Inc. Monitoring: 満たしたい要求 • 健康状態をチェックしたい • アプリケーションの健康状態のモニタリング

    • メトリクスの継続的な取得 • メトリクスをとって状態をWatchしておきたい
  43. © - BASE, Inc. (振り返り)概要:“⾼観測可能性の原則” • コンテナを “ブラックボックス” として扱う •

    重要なイベントを STDOUT/STDERR に記録し、 Fluentdなどツールを活⽤してログ集約をする • 活動状況や準備状況など、様々な状態チェックに対 してAPIを提供する
  44. © - BASE, Inc. Monitoring: 健康状態を伝えるエンドポイントの実装 • ヘルスチェックを⾏うエンドポイントを作成する • “Health

    endpoint pattern” in 『⼊⾨ 監視』 • 依存サービスとの接続状態もエンドポイントで確認す る • ex. 依存サービス DB‧Redis etc
  45. © - 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 datatabase because 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 because of error: %s", err.Error()), Status: http.StatusServiceUnavailable, } respondJSON(w, res) } w.WriteHeader(http.StatusOK) }
  46. © - BASE, Inc. Extra Talk: Mackerel Container Agentでのコンテナ内 監視

    • Mackerel Container Agent を利⽤したコンテナ監 視 • https://mackerel.io/ja/docs/entry/howto/container-agent • コンテナ内部のCPU, Memory, Networkなどを監視 できる • アプリケーションコンテナのサイドカーとして起動
  47. © - 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
  48. © - 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/ / / /
  49. © - 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/ / / /
  50. © - BASE, Inc. End role: 参考書籍 • 『⼊⾨ 監視

    モダンなモニタリングのためのデザインパターン • https://www.oreilly.co.jp/books/ / • 『分散システムデザインパターン コンテナを使ったスケーラブルな サービスの設計』 • https://www.oreilly.co.jp/books/ /
  51. © - 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
  52. © - 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