DatadogAgentとアプリケーションをCloud Runに同居させる

## 要約
- Cloud Runで動くPHPアプリケーションのAPMをDatadogに送信したい
- supervisordを使ってコンテナ内にDatadogAgentとApacheを同居させた
- supervisordのEventListener機能を使って、"Apacheの終了=コンテナの終了"を実現した

## 経緯
弊社のプラットフォーム事業本部ではDatadogの利用が進められています。
パフォーマンスの可視化を進めるために、私のいるチームが持っているPHPアプリケーションも、
Datadogに対応してAPMを送信できるようにしようとしています。
PHPアプリケーションはApacheモジュールとして動作する形になっていて、
GCPのフルマネージドなCloud Run上で稼働しています。

さて、Cloud Run上で動くアプリケーションのAPMを取るための方法としては、
[公式ドキュメント](https://docs.datadoghq.com/ja/integrations/google_cloud_run/)にこのような記述があります。

> 注: この機能は、Google Cloud Run for Anthos/GKE でサポートされています (Cloud Run Fully Managed ではサポートされていません)。

😇
というわけでDatadogのGoogle Cloud Platform インテグレーションを使ったAPMの収集はできないらしいので、別の方法を考える必要があります。

コンテナの中でDatadogAgentとアプリケーションの両方を動かすことを考えたので、どのように実現したかを書きます。

## 環境情報
- image: php:x.x-apache

## 実現方法
ざっくりと以下の三つの点について説明します。

- DatadogAgentインストール
- Dockerコンテナで二つのアプリケーションを同居させる
- コンテナごと終了する

### DatadogAgentインストール
datadog-agentとdatadog-php-tracerをインストールします。
datadog-agentがメトリクスをサーバーに送信する役割で、
datadog-php-tracerがアプリケーションのAPMをとってagentに投げる役割です。

基本的にパッケージからインストールするかインストールスクリプトの実行で完了します。

```Dockerfile
# Install datadog-agent
ARG dd_api_key
ENV DD_API_KEY=$dd_api_key
RUN curl -Lo install_agent.sh https://raw.githubusercontent.com/DataDog/datadog-agent/main/cmd/agent/install_script.sh \
    && bash install_agent.sh

# Install datadog-tracer
ENV DDTRACE_VERSION=0.xx.0
RUN curl -Lo datadog-php-tracer.deb https://github.com/DataDog/dd-trace-php/releases/download/${DDTRACE_VERSION}/datadog-php-tracer_${DDTRACE_VERSION}_amd64.deb \
    && dpkg -i ./datadog-php-tracer.deb \
    && rm datadog-php-tracer.deb
```

また、datadog以前はNew RelicでAPMを取っていました。
datadog-tracerのPHP拡張モジュールと両方入れると
`Found incompatible module: newrelic, disabling conflicting functionality`
と言われて競合しました。

### Dockerコンテナで二つのアプリケーションを同居させる
Dockerコンテナで複数のアプリケーションを動かす方法については、
[公式ドキュメント](https://docs.docker.com/config/containers/multi-service_container/)に記載があります。
ドキュメントには二通りの方法が提示されています。

1. shell scriptを使う方法
1. supervisordを使う方法

再起動等のプロセス制御をやりたいのでsupervisordを使います。

supervisordの設定ファイル(supervisord.conf)にdatadog-trace-agentとapacheの設定を記述します。

```supervisord.conf
[supervisord]
nodaemon=true

[program:datadog-tracer]
command=/opt/datadog-agent/embedded/bin/trace-agent
user=dd-agent
autorestart=true

[program:apache2]
command=/bin/bash -c "apache2-foreground"
```

基本的に上記の設定だけで簡単に同居させることができます。
supervisordがdockerコンテナの中でPID1として動くので、`nodaemon=true`にする必要があります。

apacheにautorestartが設定されていないですが、これが次のEventListenerの話になります。

### EventListener
元々はApacheがPID1プロセスとして動いていたため、Apacheが落ちるとコンテナも一緒に落ちる仕組みになっていました。
supervisordの導入でsupervisordがPID1になります。
これによって上記の仕組みが働かなくなるため、別の仕組みを考える必要があります。

supervisordには[Events](http://supervisord.org/events.html)という仕組みがあります。
supervisordがevents notificationを飛ばし、EventListenerと呼ばれるプロセスがこれをハンドリングします。
EventListenerは自分で自由にコードを書いて動かすことができるので、これによって目的を実現できます。


```supervisord.conf
[eventlistener:exit-listener]
command=/usr/local/bin/exit_apache_listener.py
events=PROCESS_STATE_EXITED 
```
`command=/usr/local/bin/exit_apache_listener.py`でEventListenerのコードを指定します。
`events=PROCESS_STATE_EXITED`でハンドリングするイベントを指定しています。
プロセスが正常に起動したことを知らせるPROCESS_STATE_RUNNINGや、プロセスが停止状態になったことを知らせるPROCESS_STATE_STOPPED等、
いくつかのイベントが定義されています。
今回はsupervisord管理外のプロセス停止に反応したいので、PROCESS_STATE_EXITED指定します。

`exit_apache_listener.py`では無限ループでイベントをListenしています。

```python:exit_apache_listener.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import signal
import sys

from supervisor import childutils

def main():
    while True:
        headers, payload = childutils.listener.wait()
        # '\n'を末尾に付けないと、プログラムが異常終了する
        payload_header, data = childutils.eventdata(payload + '\n')
        childutils.listener.ok()

        # apacheのマスタープロセスが終了した時に、親プロセス(supervisord)を終了する
        if (payload_header['processname'] == 'apache2') and (headers['eventname'] == 'PROCESS_STATE_EXITED'):
            os.kill(os.getppid(), signal.SIGTERM)

if __name__ == "__main__":
    main()

```
Apacheが終了したら、Apacheの親プロセス、すなわちsupervisordにSIGTERMを送って終了させています。
supervisordはPID1で動いていたので、supervisordを終了することでコンテナごと終了させることができます。

## 終わりに
Datadogのインテグレーションが用意されていないフルマネージドなCloud Runで、Datadogに対応する方法を書きました。
実験では実際に動作を確認していて、本番環境への適用はまだなので、コストやパフォーマンス等で面白い話があったらまた書くかもしれないです。

ちなみにdatadog以前はNew RelicでPHPアプリケーションのモニタリングをしていたのですが、
New Relicの場合はagentデーモンが勝手に立ち上がる仕組みがあって、簡単にCloud Runに乗せることができます😇😇😇

プラットフォーム事業本部では発信活動を強化しています。

https://qiita.com/organizations/dmmcompf