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.yml
とdocker-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に興味がある
是非気軽にお話しませんか。一緒にお話、働ける事を楽しみにしております。