MF KESSAIのマイクロサービス化を支える、Dockerをフル活用した開発環境の歴史

MF KESSAIの篠原(@shinofara)です。

前回「MF KESSAIはどういう技術・体制なのか」の続きになります。

今回お話すること

創業した3月から数ヶ月前までは、1つのdocker-compose.ymlで開発できるとてもシンプルなサービスでした。 ですが数ヶ月前から徐々にマイクロサービスへと舵をきり進みだした事で色々辛みが生まれてきました。 この問題の対応として、サービス単位で分離した所、以前より開発しやすくなりました。今回は開発環境の歴史と問題、そして現在の構成についてお話します。

MF KESSAIの開発環境の歴史

創業初期(2017年3月〜6月)

6月頃までは、ユーザー向けWEBサービスではなく社内ツールの開発をしていました。

当時の docker-compose.yml

ブログの為に色々省略して書き出しています。またmfkessai/hogehoge のイメージは実際はGoogle Container Registry上で管理しています

version: '3'
services:
  haproxy:
    image: haproxy:1.7.9-alpine
    ports: 443

  admin_nginx:
    image: mfkessai/admin_nginx

  admin_web:
    build: ./docker/go
    volumes:
      - /gopath/src/github.com/mfkessai/admin_web:/go/src/github.com/mfkessai/admin_web

  mysql:
    image: mysql:5.7

  smtp:
    image: schickling/mailcatcher

本番上ではGCP Load Balancerで443を受けSSL Terminationを行っています。開発環境ではLBとしてhaproxy を使う事で可能な限り近い状態としています。 この通りシンプルなWEBサービスという構成でしたので、特に問題という問題は発生しませんでした。

この時のGoを実行させていた環境は、以下のようになっています。

# ./docker/go/Dockerfile
FROM golang:1.9

WORKDIR /work
RUN go get -u github.com/githubnemo/CompileDaemon

ENV TZ  Asia/Tokyo
CMD /go/bin/CompileDaemon -build="go build -o /server main.go -command="/server -conf /config/config.yml

golang:1.9 を直接 docker-compose.yml に記述しても良かったのですが、ファイルの変更時に自動で再起動してほしかったので、CompileDaemonを採用してました。

先日まで(2017年6月〜11月)

開発した社内ツールで最低限のビジネスを回せる様になってきました。 このタイミングから、エンジニア数も増え、ユーザ向けのWEB,APIサービスの開発も始まりました。

この時まで社内ツールを開発していたので、開発した資産をパッケージ化する。もしくはモノリシックアプリケーションで行くか、将来を見据えてAPIとして分離をするか議論が始まりました。結果としてはgRPC Endpointを開発していくながれとなりました。 この辺りはまた別のタイミングでお話できればとおもいます。

サービス拡張とマイクロサービス化が進みだした頃の docker-compose.yml

version: '3'
services:
  # 全サービスのSSL化を担当
  haproxy:
    image: haproxy:1.6.9-alpine

  # 管理ツール
  admin_nginx:
    image: asia.gcr.io/mfk-shared/dwarf-nginx:master

  admin_web:
    build: ./docker/go
    volumes:
      - /gopath/src/github.com/mfkessai/admin_web:/go/src/github.com/mfkessai/admin_web

  # ユーザ向けAPI Aggregater(認証も担当)
  external_kong:
    image: mfkessai/kong

  external_kong_postgres:
    image: postgres:9.4

  # ユーザ向けAPI
  externa_api_app:
    build: ./docker/go
    volumes:
      - /gopath/src/github.com/mfkessai/externa_api_app:/go/src/github.com/mfkessai/externa_api_app

  # ユーザ向けWEB
  web_nginx:
    image: mfkessai/web_nginx

  web_app:
    build: ./docker/go
    volumes:
      - /gopath/src/github.com/mfkessai/web_app:/go/src/github.com/mfkessai/web_app

  # 各サービス向け、サービスメッシュ
  linkerd:
    image: buoyantio/linkerd:1.3.0

  namerd:
    image: buoyantio/namerd:1.3.0

  # MF KESSAIのドメインサービス
  internal_grpc:
    image: golang:1.9

  mysql:
    image: mysql:5.7

  migrate:
    image: mfkessai/migration
    volumes:
    command: [up]

  smtp:
    image: schickling/mailcatcher

# ---------
# Volume
volumes:
  mysql-data:
    driver: local

全て書きだすととても長くなってしまった為、一部抜粋して書き出しています。全部で21サービス存在していました。

この段階で発生してきた問題

ここでは開発環境だけではなく、本番での運用に関しての悩み・課題も含まれます。

  • webだけ開発できればいいのに、adminとapiも立ち上がるため重い遅い
  • webを開発しようとしたら、手元のapiを最新にし忘れていてエラーが出る。そして追従がめんどくさい
  • 依存するコンテナの起動を待てずにコンテナが立ち上がらない状態
  • 少しずつマイクロサービス化が進みだした事もありgrpc, kong apiなどAPIが増えてきた事でネットワーク管理も大変になってきた

そして現在(2017年11月〜)

今後更にサービスは拡大していくのですが、このままだと開発環境のスケールも辛くなり更には開発効率も落ちてしまうのではという声が上がり始めました。 そこで新しく取り入れだした「1週間通常業務ではなく開発効率改善だけにコミットする」仕組みが僕の番でしたので、この時間を使って改善を行いました。

対応した事など

ここまでに発生した課題や要望などを踏まえて、今後のサービス開発にどうしたら良いかを考え、下記のように対応しました。

  • docker-compose.yml関連
    • 1つのdocker-compose.ymlに全てを集約する事を辞めて、各サービス単位でdocker-compose.ymlを作成
    • docker-compose.ymlのoverrideの仕組みを使って、build serviceとimage serviceを切り分ける
    • Makefileで立ち上げられる様にして、依存するサービスも立ち上げる
    • 各サービスの立ち上げ時にgithub、もしくはDocker Registryから最新のものをpullして常に開発環境を最新にする
  • Goの開発環境改善
    • CompileDaemonを辞めて、よく使われていてメンテも続いている、Realizeに変更
  • ネットワーク複雑化対応
    • Linkerd(Service Mesh)を導入して、各APIの監視・管理を集約

全部紹介したいのですが、今回はdocker-compose.yml関連について書きます。

Dockerをサービス単位で立ち上げられるように

docker-composeを含むディレクトリ構成としては、このようになっています。

.
├── Makefile
├── README.md
├── docker //haproxyの本体は全体で使うので、curerntに置いてる
│   └── haproxy
│       ├── Dockerfile
│       └── wait-for-app.sh
├── service
│   ├── admin
│   │   ├── docker //ここにビルドで使うファイルや、Dockerfileが置いてあるイメージ
│   │   ├── docker-compose.read.yml
│   │   └── docker-compose.yml
│   ├── web
│   │   ├── docker
│   │   ├── docker-compose.read.yml
│   │   └── docker-compose.yml
│   ├── kong
│   │   ├── docker
│   │   └── docker-compose.yml
│   ├── linkerd
│   │   ├── docker
│   │   └── docker-compose.yml
│   ├── mail
│   │   └── docker-compose.yml
│   ├── external_api
│   │   ├── docker
│   │   ├── docker-compose.read.yml
│   │   └── docker-compose.yml
│   └── internal_grpc
│       ├── docker
│       ├── docker-compose.read.yml
│       └── docker-compose.yml
└── src
    └── docker-gobuildpack
        ├── Dockerfile
        └── docker-entrypoint.sh

こんなにdocker-compose.ymlが多い構成でどのように使うのか

依存順に例えばinternal_grpcは全てのサービスから利用されるので、internal_grpcから順にdocker-compose up を実行していくという事もできます。 ですが手間ですので、弊社では Makefile を使って操作できるようにしています。

# 全て立ち上げる場合
$ make up

# サービス毎に立ち上げる場合
# web,admin,external_apiの場合は、internal_grpcも一緒に立ち上がる
$ make up-[web, admin, external_api, internal_grpc]
[web] build mode ? [y/n][default n] =>

各サービスをup するタイミングで、[y/n]と確認が入るようにしています。 [y]だとGOPATH以下のプロジェクトがマウントされたコンテナを立ち上げます。 [n]だとGoogle Container RegistryにUpload済みのイメージでコンテナを立ち上げます。 それぞれの選択時に実行されるコマンドは下記になります。

cd ./service/web
# Build Mode = yなら
docker-compose -f docker-compose.yml up

# Build Mode = n(Image Mode)
docker-compose -f docker-compose.yml -f docker-compose.read.yml up

それぞれのファイルの内容は以下になります。

service/web/docker-compose.yml には何を書いているのか。

version: '3.4'
services:
  haproxy:
    build:
      context: ../../docker/haproxy
    ports:
      - XX443:XX443
    volumes:
      - ./docker/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
      - ../../ssl:/etc/haproxy/ssl:ro
    depends_on:
      - nginx

  nginx:
    image: mfkessai/web_nginx
    depends_on:
      - app

  app:
    build: ./docker/web_app
    volumes:
      - /gopath/src/github.com/mfkessai/web:/go/src/github.com/mfkessai/web
    external_links:
      - linkerd
    networks:
      - linkerd
      - default

networks:
  default:
  linkerd:
    external:
      name: linkerd_default

service/web/docker-compose.read.yml には何を書いているのか。

version: '3.4'
services:
  app:
    image: mfkessai/web_app
    volumes:
      - ./docker/app/config:/config:ro
    external_links:
      - linkerd
    networks:
      - linkerd
      - default

docker-compose に対して、-f を複数渡すと、同じサービス名があれば後で渡されたファイルの記述が上書きする仕様になっています。 docker-compose.override.yml を使う方法もあるのですが、今回はこのようにしています。

そして docker-compose.ymldocker-compose.read.ymlを分ける理由ですが、Golang の開発にはGolang が必要だけど、実行イメージでは、Golang が不要になるからです。 その為コンパイル後のバイナリが動けばいいので、alpine を使っています。 もしコンパイル言語ではなくスクリプト言語系を採用していたら、下記のコードで済みます。

app:
  build: ./docker/app
  image: mfkessai/app

サービス間の接続

1ファイルに全て書いていた頃は、コンテナ間の名前解決は意識しなくてもできていました。 ですが今回分割した事で、dockerのnetworkも分かれてしてしまいましたので、external_link を使って接続しています。

例えば、linkerd は標準で linkerd_default というネットワークが作成されますので、linkerd を利用するサービス web では以下のように記述しています。

 app:
    build: ./docker/web_app
    external_links:
      - linkerd // linkerd_defaultネットワークの、linkerdコンテナ
    networks:
      - linkerd
      - default

networks:
  default:
  linkerd:
    external:
      name: linkerd_default

こうする事で、dockerネットワークが複数になってもdefaultネットワークを超えて接続できます。

結果

デメリット

  • docker-compose up だけで立ち上げれなくなった
  • up する順番に依存が生まれた
  • 1ファイルで全てを見通せなくなった

メリット

  • 1つのdocker-compose.yml の見通しは良くなった
  • 各サービス毎にどのネットワーク、コンテナを利用(依存)しているのかが明確になった
  • 各サービスの責務がネットワークレベルで明確になった
  • 開発に必要なコンテナしか立ち上がらないので、Docker Hostのリソース利用率が下がった

最後に

弊社では下記のようなエンジニアを募集しております。

  • Goを書きたい(Gopherになりたい)
  • クラウドを活かしたアーキテクチャ設計も出来るアプリケーションエンジニアになりたい
  • Fintechに興味がある

是非気軽にお話しませんか。一緒にお話、働ける事を楽しみにしております。