DatadogとAmazon Connectを用いた電話通知実施してみた【監視】

# TL; DR
監視において様々な通知方法が求められると思いますが、影響度の高いものに関しては電話連絡が必要だったりすると思います。
今回はその要件があり**Datadog**と**Amazon Connect**を用いて実装しました。
思った以上に簡単に実装できたので備忘録も兼ねて書き起こしていきます。

簡単な概要図としては以下です。
![dd_connect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/a22ebd05-0a5a-f47c-421a-9e0a502bc83e.png)

では早速やっていきます。

# Amazon Connect
Amazon Connectはコールセンターを容易に作成できるという触れ込みでGAされたと記憶してましたので、正直結構限定的なのかなーと思っていて今まで触ってきませんでした。
しかし今回のような単純に電話かけたいみたいな要件でも簡単に利用することができます。

## インスタンス作成
まずはAmazon Connectインスタンスを作成します。
赤枠のインスタンスを追加するをクリックします。
<img width="1127" alt="sc.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/b1ad462e-a0af-e684-9408-c2092270668b.png">
作成後アクセスできる設定用の画面があるのですが、おそらくそれとConnectの中身が動くためのターゲットグループ的なのを作るのでしょう。
Connect用コンソールのURLを決めます
<img width="1300" alt="sc 1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/e13af157-39e7-6b70-3e83-219f47489fee.png">
管理者を決めますが、ここで作らなくてもあとから作成できるのでスキップしても大丈夫です。
個人的にはここで作っといた方がいいと思います。
<img width="1302" alt="sc 2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/6b61eae1-a139-fafa-a4e4-d22e978557c1.png">
ここはそのままで次へ
<img width="1266" alt="sc 3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/32a84503-37d4-183b-0586-6dd434cf931c.png">
ログなどはS3バケットに保存されるのですがデフォルトは新規で作成されますが、設定カスタマイズからバケットを指定することも可能です。
<img width="1242" alt="sc 4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/a34dede1-8d34-ff54-5f72-bde7291d1f50.png">
設定を確認をして、インスタンスの作成を押下すると数分で作成されます。
<img width="1226" alt="sc 5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/ae167d87-f122-653c-dea1-936d9778366e.png">
作成後はアクセスURLからアクセスが可能です。
<img width="1233" alt="sc 6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/bbe9c069-f331-a77b-0c18-ded2447e3eda.png">


## 電話番号取得
上記のURLへアクセスするとダッシュボードが表示されます。
<img width="1322" alt="sc 7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/0aa59053-255e-4458-5ce4-aa86afcb2233.png">
左ペインから電話番号を選択します
<img width="362" alt="sc 8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/96e5953f-b53b-a58a-b994-8c7c51828379.png">
電話番号の取得を押下します。
<img width="957" alt="sc 9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/98cab1ec-45b0-397a-14c6-2f050f0721a4.png">
すると画面に遷移するのですが、日本を選択すると注意画面がでてきます。
最初反社チェック的なものかと思ったのですが、一旦クリックするとその後保存ボタンがアクティブになりました。
問い合わせたところバグらしくすみません…とおっしゃってたので仕様だと思ってぽちぽちしてくださいw
![sc_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/36b5e638-b8d9-f9aa-7448-0f990dcfd0f0.png)
無事に電話番号が取得できました。
<img width="1167" alt="sc 10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/5c18e2a4-be8e-7ad3-d75a-ce6ec312534e.png">

## 問い合わせフローの設定
次にこの番号がどのようなアクション(問い合わせフロー)をするのかをGUIで作成していきます。

左ペインから問い合わせフローを選択
<img width="331" alt="sc 11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/c2993b39-7ac8-ba79-e0e0-f53f43aacd2c.png">
問い合わせフローの作成をポチします。
<img width="1103" alt="sc 12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/e4900a28-ec32-4203-5244-6f68192de260.png">
こんな感じで作成してください。大事なのは赤枠です。
<img width="1309" alt="sc 13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/12cd3159-7bdc-5733-2f9f-3817d2e54608.png">
プロンプトの再生をクリックすると右側ににょっと出てくるのでこんな感じで設定してください。赤枠は任意で大丈夫ですが、Lambdaで利用するものなのでお好みで設定してください。
<img width="462" alt="sc 14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/f55169f0-fe87-ca1b-59cf-d2ab1a8dc94e.png">
設定が終わったら保存と公開を押しましょう。後ほどLambdaで利用するのでURLを控えておきましょう。
<img width="1362" alt="sc 15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/7e17c194-8874-16c3-81ee-74f5954a628a.png">


## 電話番号との紐付け
先ほど作成した電話番号の画面に行き、問い合わせフローを作成したものに変更します。
<img width="761" alt="sc 16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/3813c577-333c-3108-268c-f256ab850597.png">
変更後保存を押下します。
こちらも後ほどLambdaで利用するので電話番号を控えておきましょう。

これでAmazon Connect側の設定は完了です。

# AWSリソース
必要なAWSリソースを作成していきます。

## Lambda
実際にAmazon Connectを介して電話するLambda関数を作成していきます。
と言ってもConnect側は非常にシンプルです。

今回はPythonで実装したのでboto3のConnect Clientで[start_outbound_voice_contact](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/connect.html#Connect.Client.start_outbound_voice_contact)を利用します。
コードはこんな感じです。

```python
# coding=utf-8
import logging
import json
import os
import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)
headquarters = os.environ['INCIDENT_MANAGEMENT_HEADQUARTERS']


def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    message = event['Records'][0]['Sns']

    connect = boto3.client('connect')
    ssm = boto3.client('ssm')

    alertMessage = message['Subject']
    message = alertMessage + 'です。確認してください。'

    logger.info("Message: " + str(message))

    for boss in headquarters.split(","):
        destination = ssm.get_parameter(
            Name=boss,
            WithDecryption=True
        )
        phoneNumber = destination['Parameter']['Value']
        connect.start_outbound_voice_contact(
            DestinationPhoneNumber=phoneNumber,
            ContactFlowId='1234567a-zyxw-9876-12ab-098765def432',
            InstanceId='987abc12-098v-gh56-78ij-4567klmn12op',
            SourcePhoneNumber='+811234567890',
            Attributes={
                'alarm': message + message
            }
        )
```
先ほど控えておいたURLと電話番号をここで使います。
`InstanceId` / `ContactFlowId`は下記のようになっていますので、ここからとりましょう。
`SourcePhoneNumber`には取得した電話番号を入れます。

```
https://[アクセスURL].awsapps.com/connect/contact-flows/edit?id=arn:aws:connect:ap-northeast-1:12345678901:instance/[InstanceId]/contact-flow/[ContactFlowId]
```
これでOKです。

<details><summary>後ほど設定するDatadogから受け取るペイロードはこんな感じでした。</summary><div>

```json
{
  "Records": [
    {
      "EventSource": "aws:sns",
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:ap-northeast-1:123456789012:test-connect:12345678-asnc-0987-zyxw",
      "Sns": {
        "Type": "Notification",
        "MessageId": "0987654-abcd-1234-zyxw-12345abcde",
        "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:test-connect",
        "Subject": "[Recovered] [Synthetics] アプリケーションサイト正常性確認",
        "Message": "@sns-test-connect\n\nMonitor Status: https://app.datadoghq.com/monitors#0987654?group=total · Edit Monitor: https://app.datadoghq.com/monitors#0987654/edit · Event URL: https://app.datadoghq.com/event/event?id=1234567890123456789",
        "Timestamp": "2020-04-07T10:28:12.148Z",
        "SignatureVersion": "1",
        "Signature": "hogehogefugafuga==",
        "SigningCertUrl": "https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-abcdefghijklmnopqrstu.pem",
        "UnsubscribeUrl": "https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-northeast-1:123456789012:test-connect:12345678-asnc-0987-zyxw",
        "MessageAttributes": {}
      }
    }
  ]
}
```

</div></details>

## SNS
特段難しいことはないのでトピックを作成してサブスクリプションをLambdaで作成しましょう。

```bash
$ aws sns create-topic \
    --name test-connect
$ aws sns subscribe \
    --topic-arn arn:aws:sns:ap-northeast-1:123456789012:test-connect \
    --protocol lambda \
    --notification-endpoint arn:aws:lambda:ap-northeast-1:123456789012:function:test-connect
```
これでOKです。

# Dadadog Synthetics
DatadogとAWSの連携はよしなにやってください。
Datadogでの外形監視には**Synthetics**というものが利用できます。
設定は非常に簡単でぽちぽちと入力するだけで利用が可能です。無料トライアルもありますし、10,000リクエスト/月まで$5で利用可能なので既に利用されている方はおすすめです。
APIのテストやブラウザテストなど幅広く実施できますが、今回は単純にサイトのダウンを検知したかったのでシンプルに設定していきます。
<img width="409" alt="sc 17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/1956bc6e-be03-e186-7d2f-5a7e1a5038c6.png">
こんな感じでURLを指定して閾値などを設定していきます。
![sc_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/e2ed9857-2802-d235-b083-357f50623352.png)

通知設定はこんな感じです。

```markdown
{{#is_alert}} [サービスサイト異常検知](https://hogehoge.jp/) @sns-dd-2-connect  @slack-alert-test  {{/is_alert}}
{{#is_recovery}} [サービスサイト](https://hogehoge.jp/) 回復 @slack-alert-test {{/is_recovery}} 
```

あとはアラートを設定して適当にやるだけです。
Slack通知もちゃんときてます。
<img width="687" alt="sc 18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/74f5a5bf-930f-d995-ef3a-c539341d93f4.png">
電話もちゃんときてます。
![Screenshot_20200408-020946.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/ec9e9807-2a0f-5439-2a00-e654ccbda2fc.png)

ばっちりですね。

# 所感
24/7で頑張って対応する！みたいなのが少しでも減らせたら嬉しいですね。
ただほんとにクリティカルなものってユーザーからの指摘で気付くみたいなこともあると思うので、システム側で全て拾いきるにはまだまだ難しいですね。

# Appendix
コマンドでちまちま作るのめんどくさいのでTerraformでざばっとAWSリソースは作ったので一応

<details><summary>リソースざばっと作るコード</summary><div>

```go:main.tf
provider "archive" {
  version = "~> 1.3"
}

provider "aws" {
  region  = "ap-northeast-1"
  version = "~> 2.50.0"
}

locals {
  name = test-connect
}

variable "params" {
  hoge = "+811234567890"
  fuga = "+810987654321"
}

resource "aws_ssm_parameter" "params" {
  for_each = var.params
  name     = each.key
  type     = "SecureString"
  key_id   = "alias/aws/ssm"
  value    = each.value
}

resource "aws_sns_topic" "topic" {
  name = local.name
}

resource "aws_sns_topic_subscription" "subscription" {
  topic_arn = aws_sns_topic.topic.arn
  protocol  = "lambda"
  endpoint  = aws_lambda_function.function.arn
}

resource "aws_cloudwatch_log_group" "log_group" {
  name              = "/aws/lambda/${local.name}"
  retention_in_days = 14
}

data "aws_iam_policy_document" "lambda_assume" {
  
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

data "aws_iam_policy_document" "lambda_basic" {
  
  statement {
    sid     = "putLog"
    effect  = "Allow"
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
    resources = "${aws_cloudwatch_log_group.log_group.arn}:*"
  }
}

data "aws_iam_policy_document" "lambda_get_params" {
  
  statement {
    sid     = "getParams"
    effect  = "Allow"
    actions = [
      "ssm:GetParameters",
      "ssm:GetParameter"
    ]
    resources = [for params in aws_ssm_parameter.params : params.arn]
  }
}

resource "aws_iam_role" "lambda_basic" {
  name = "lambda_basic_${local.name}"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}

resource "aws_iam_policy" "lambda_basic" {
  name   = "lambda_basic_${local.name}"
  policy = data.aws_iam_policy_document.lambda_basic.json
}

resource "aws_iam_policy" "lambda_get_params" {
  name   = "lambda_${local.name}_get_params"
  policy = data.aws_iam_policy_document.lambda_get_params.json
}

resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda_basic.name
  policy_arn = aws_iam_policy.lambda_basic.arn
}

resource "aws_iam_role_policy_attachment" "lambda_get_params" {
  role       = aws_iam_role.lambda_basic.name
  policy_arn = aws_iam_policy.lambda_get_params.arn
}

resource "aws_iam_role_policy_attachment" "lambda_connect_fullaccess" {
  role       = aws_iam_role.lambda_basic.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonConnectFullAccess"
}

data "archive_file" "function" {
  type        = "zip"
  source_file = "${local.name}.py"
  output_path = "${local.name}.lambda.zip"
}

resource "aws_lambda_function" "function" {
  function_name    = local.name
  filename         = data.archive_file.function.output_path
  runtime          = "python3.7"
  handler          = "${local.name}.lambda_handler"
  source_code_hash = data.archive_file.function.output_base64sha256
  role             = aws_iam_role.lambda_basic.arn

  environment {
    INCIDENT_MANAGEMENT_HEADQUARTERS = "hoge,fuga"
  }

  lifecycle {
    ignore_changes = [
      last_modified,
      source_code_hash,
    ]
  }
}

resource "aws_lambda_permission" "sns" {
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.function.function_name
  principal     = "sns.amazonaws.com"
  source_arn    = aws_sns_topic.topic.arn
}
```

</div></details>