Microsoft製OSS、daprでRoadrunner + PHPのアプリケーションを動かす

[弁護士ドットコム Advent Calendar 2019](https://qiita.com/advent-calendar/2019/bengo4com)- Qiitaの19日目の記事です。

先日Microsoftが公開した[dapr](https://dapr.io/)というOSSの上でPHPを動かせるか検証した内容になっています。


## ことはじめ

2019年10月、Microsoftは**dapr(Distributed Application Runtime)** というOSSを公開しました。現状まだα版なのでプロダクションには載せられないですが、**メッセージングを中心としたマイクロサービス開発を容易にする**と書かれています。pub/subを中心にしたアーキテクチャの実現、というのは面白そうなプロダクトだと思ったのでこの機会に触ってサンプルを作ってみました。

(その他、daprの詳細は[こちらのQiitaの記事](https://qiita.com/y10exxx/items/b16e5f92a53bf8f55df9)や[こちらの記事](https://www.publickey1.jp/blog/19/dapr.html)を見るのが良いかもしれません。)


## どんなの作ったの

このような構成のサンプルをPHPで作りました。
[コードはこちら](https://github.com/shotanue/dapr-php)

想定したユースケース（適当）としては、Twitterみたいなフォローフォロワーの概念があったとして、フォロワーが増えた時に"followerAdded"みたいなイベントを発火させて、emailとpush通知のマイクロサービスを叩くようなフローです。（適当）

<img width="441" alt="スクリーンショット 2019-12-18 0.14.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144581/eb5bf59b-d92c-4fa8-99a7-876a627041af.png">




## なんでPHPなの

dapr自体、まだ公開されて間も無いため、日本語はおろか英語でもなかなか情報がなく、チュートリアルも**node,go,python**はあるものの、**PHPはなかったため**です。PHPでも動くといいなーと思ったので今回調べてみました。(動きました)

## daprのしくみ

daprはpub/subを実現するために、**daprdというプロキシ**を提供しています。アプリケーションはこのプロキシを介してWebサーバーを動作させる必要があります。

ローカル環境(ホストマシン上)で直接動かしたり、kubernetes上でサイドカーとして動かす例がチュートリアルに載っています。ホストマシンの環境を汚さずに簡単に使いたいのであれば、プロキシを同じコンテナで動作させることも可能でした。（やろうと思えば）

今回はdocker-composeで簡単に構築したいので、プロキシを同じDockerイメージに含めて動作させます。コンテナはこのようなイメージです。

<img width="369" alt="スクリーンショット 2019-12-17 22.28.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144581/767990de-a70c-0631-4c3b-5e1fea8e4f74.png">


Dockerfileはこんな感じになります。

```Dockerfile
FROM php:7.3-cli-alpine AS base-image

RUN apk --update --no-cache add wget bash

# install dapr-cli
RUN wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash

# daprdは`dapr-cli init`を叩くと落ちてくるが、同時にredisとplacementという別のコンテナを立ち上げてしまう。
# イメージの構築で叩けないので、直接curlでバイナリを落とすことにする。
# ↓このへんのソースを読むとdaprdのパッケージが置いてあるURLがわかる。
# ref. https://github.com/dapr/cli/blob/d585612185a4a525c05fb62b86e288ccad510006/pkg/standalone/standalone.go#L265
RUN curl -Lo /tmp/daprd.tar.gz \
      https://github.com/dapr/dapr/releases/download/v0.3.0/daprd_linux_amd64.tar.gz \
  && tar xzvf /tmp/daprd.tar.gz -C /usr/local/bin

...略

FROM base-image AS app
# 本来は自分でdaprdとアプリケーションのプロセスを立ち上げる必要があるが、
# dapr-cliのコマンドを使って簡単な方法で起動させる
EXPOSE 8080
ENTRYPOINT [\
  "dapr",\
  "run",\
  "--app-port",\
  "8080",\
  "sh",\
  "invoke-service.sh"\
]
CMD [""]
```


実際にアプリケーション全体を動作させるにあたり、各サービスのコンテナで動作させるプロキシの他にもいくつかコンテナが必要です。**placement**と名付けられているコンテナ、**メッセージブローカー**の役割を負うミドルウェア、**エンドポイント**となるコンテナです。結果、イメージとしてこのような構成になります。


<img width="1350" alt="スクリーンショット 2019-12-18 0.01.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144581/84238293-6829-a2c1-f4fa-044594c62c37.png">



-  **placement**
    - あまり仕組みが理解できていないのですが、proxyが起動した際にコンテナのIPがログに表示されるのでサービスディスカバリーやロードバランサー的な役割に見えます。

- **メッセージブローカー**
    - dapr-cliを使った場合、デフォルトではredisが起動するようになっています。 チュートリアルをみた感じだと、kafkaなども使えるようです。
- **エンドポイント**
    - daprdがそのままエンドポイント用のプロセスとしても動作します。"/v.1.0/publish/xxxEvent"のようなパスでPOSTするとイベント(トピック)が発火します。トピックのサブスクライブ方法は後述します。
    - 今回は取り上げないですが、サブスクライバ側にはapp-idというものが付与できるため、"/v.1.0/invoke/app-id/method/xxxhogefuga"というような呼び出し方でGETやPOSTを使った同期通信(たぶん)もできるようでした。

placementとメッセージブローカーにあたるコンテナはdapr-cliを使って`dapr init`と叩いても起動できます。今回のようにdocker-composeを使う場合には自前でplacementとredisなど用意する必要があります。
（dapr initで立ち上げたコンテナ情報をdocker-compose.ymlに書くだけなので用意は簡単でした。）


ネットワークは謎です。各daprdとplacement, message brokerが具体的にどういった流れでネットワーク越しに連携するのかまでは調べられませんでした。(ドキュメントとかに載っているのかな？🤔)




##  (余談)RoadRunnerってなに
[   Golang製のPHPのプロセスマネージャ](https://roadrunner.dev/)で、Nginxとかに頼らずWebサーバーを立てられるようになります。(ざっくり)

- 簡単に入れられる(独自調べ)
- 速いらしい
- production readyとのこと

## PHPでつくる部分(api-facade)
クライアントからまず通信を待ち受けるapi-facadeの部分になります。

RoadRunnerを使った場合、同じプロセスで複数のリクエストを処理するため、これを踏まえた実装が必要です。今回サンプルコードを1枚のPHPに収めるべくSlim4を使うことにしており、繋ぎ込む必要があります。繋ぎ込むといっても特に難しいことはなく、RoadRunner側で用意しているHttpClientが返すRequestクラスは**Psr\Http\Message\ServerRequestInterface**を継承しているため簡単にSlim4へ渡すことができます。下記のようなコードで繋ぎ込みは完了です。

```php:api-facade/api-facade.php

$app = AppFactory::create();
...中略
$psr7 = new RoadRunner\PSR7Client($worker);

while ($req = $psr7->acceptRequest()) {
    try {
        // RoadRunnerのリクエストはSlim4へそのまま渡すことができる
        $resp = $app->handle($req);
        $psr7->respond($resp);

```

イベントのパブリッシュはdaprのエンドポイントへPOSTします。
サンプルでは簡易的に下記の関数を定義してあります。
今回はGuzzleを使いました。

```php:api-facade/api-facade.php

function publishTopic(string $topic, array $message)
{
    (new Client)->post(
        "dapr-endpoint:3500/v1.0/publish/${topic}",
        [
            'form_params' => $message
        ]
    );
}
```

POSTだけ待ち受けるfollowersというリソースを用意します。
特に処理はしません。followerが増えた"てい"です。

```php:api-facade/api-facade.php
$app->post('/followers', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    // add follower process
    // ...
    // ...

    publishTopic('followerAdded', []);

    $response->getBody()->write('');
    return $response;
});
```


## PHPでつくる部分(email-notification-service)
daprdは同じコンテナ内で起動しているアプリケーションを検知した際に特定のパスでGETで通信を行います。このタイミングでサブスクライブするトピックなどを、サービス側からdapr側にレスポンスを返して伝えることになります。

- /dapr/config
    - とりあえず200でレスポンス返してあげれば良さそうでした。(記載が見つけられなかった)
- /dapr/subscribe
    - jsonでサブスクライブするトピックのリストを返す必要があります。
    - ここではfollowerAddedをリストに加えてレスポンスを返しています。

```php:email-notification-service.php
$app->get('/dapr/config', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    return $response;
});

$app->get('/dapr/subscribe', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    $subscribeTopics = [
        'followerAdded'
    ];
    $response->getBody()->write(json_encode($subscribeTopics, JSON_THROW_ON_ERROR, 512));
    return $response;
});
```

'followerAdded'をサブスクライブしているので、'/followerAdded'のパスでPOSTを待ち受けています。
アクセスログでpub/subできているか確認するので、実装は特にしていません。

```php:email-notification-service.php
$app->post('/followerAdded', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    $response->getBody()->write('');
    return $response;
});
```




## PHPでつくる部分(push-notification-service)
email-notification-serviceと同じような記述です。

```php
$app->get('/dapr/config', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    return $response;
});

$app->get('/dapr/subscribe', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    $subscribeTopics = [
        'followerAdded'
    ];
    $response->getBody()->write(json_encode($subscribeTopics, JSON_THROW_ON_ERROR, 512));
    return $response;
});
```

```php
$app->post('/followerAdded', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    $response->getBody()->write('');
    return $response;
});
```

## つ・な・げ・て・み・た・い
composerとphpのアプリはコンテナを分けてあるので、動かす場合は最初にsetup用で用意したdocker-composeを動かします。
(依存してるライブラリなどをapi-facadeとサービス側に落とします)
その後、`dokcer-compose up`で色々立ち上がるかと思います。

```bash
docker-compose -f docker-compose-setup.yml up
docker-compose up
```

最後に下記のような感じでapi-facadeを叩くと、サブスクライブしているサービスに通信が飛んでいることがアクセスログからわかるかと思います。

```bash
curl -X POST localhost:8080/followers
```

<img width="1173" alt="スクリーンショット 2019-12-18 2.48.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144581/4cda6323-05db-637f-0504-8186cb1218e8.png">



## まとめ

[ロードマップにはこんなこと書いてる](https://github.com/dapr/dapr/wiki/Roadmap#sdks-and-frameworks)ので、PHPもSDK待ちかなーとか、かと思っていましたが、daprとのインターフェースはhttpかgRPCのようなので、PHPでも実装できました。

今回取り上げませんでしたが、pub/subだけでなく同期通信もでき(チュートリアルでは一番最初にやります)、インターフェースもhttpかgRPCでdaprが間を取り持ってくれるので、pub/subやるために特定のミドルウェアと直接お喋りする必要がなくなるのは良い印象です。

α版なので実際にゴリゴリ使うには早いですし、流行るかも不明ですが、面白そうなのでしばらく追ってみたいなと思いました。