Datadog で障害検知したら Slack と Backlog に通知しよう
以前、Backlog の課題追加があったら Slack に投げる記事を書きました。
nasrinjp1.hatenablog.com
プロジェクトでお客さんとのやりとりを Backlog にしておけば、プロジェクト管理する上で手作業で WBS ひいたり返信対象のメールを探して状況を確認したり課題管理表を作る手間も省け、通知を逃したとしても Slack に投げ込まれるのでどちらかで拾うことができていいですね。
運用も同じように自動で連携して通知する仕組みがほしいなーと思ったので、勉強がてらがんばって作ってみました。
仕組みの図
こんな感じです。監視システムは Datadog を使いました。前職で使ってたのでとっつきやすかったので。Datadog で障害検知したら Backlog に課題追加しつつ Slack にも通知されるようにしました。グレーの矢印の部分は、飛ばないようにしました。直接 Slack に投げているので二重に通知されてしまうためです。アラートだけは Slack 連携しない、ってことは Integromat なら簡単にできますよ。
Datadog から Slack へ
Datadog の Integration を使えば終わりです。めちゃ簡単です。説明するまでもなくドキュメントもそろってますし、他のブログでも書かれている&楽をしたいので書きませんw
Datadog から Backlog へ
この仕組みを作るのに 16 時間ほどつぶしてしまったので、自分のためにも書いていきます。
Backlog に投げるための情報収集
Backlog API を使って課題登録します。課題登録するにあたって、スペース ID、プロジェクト ID、イシュータイプ ID、apiKey が必要です。apiKey 以外は下記画面の URL のところで見れます。
apiKey は個人を特定して認証するためのキーです。Backlog の↓の画面で登録します。
あと、Datadog からは JSON を POST してくるみたいです。
docs.datadoghq.com
これで情報はそろいました。
Lambda から Backlog に投げるスクリプト
Python3 で requests モジュールを使うので、ローカル環境でモジュールを準備します。
cd <project_directory> python3 -m pip install requests -t .
エラー処理してないとか秘密の情報をべた書きしてるとか、ツッコミどころはありますが、まずは最低限動くところまで持っていきたいので後日修正するとして、↓のようなスクリプトを書きます。
# coding: utf-8 import json import requests def lambda_handler(event, context): title = event['title'] description = event['body'] if event['alert_type'] == 'Warning': severity = 3 else: severity = 2 return createissue(title, description, severity) def createissue(title,description,severity): url = "https://<spaceid>.backlog.com/api/v2/issues" payload = { 'projectId' : '<プロジェクト ID>', 'issueTypeId' : '<イシュータイプ ID>', 'priorityId' : severity, 'summary' : title, 'description' : description, 'apiKey' : '<apiKey>' } response = requests.post(url, params=payload) return response.json()
スクリプトと requests モジュールを一緒に zip で固めて、Lambda にアップロードします。Lambda function の IAM ロールは Cloudwatch ログにログ出力できればいいので、AWSLambdaBasicExecutionRole ポリシーをつけたロールを作成してそれを指定します。あとの設定は好きにしてください。
Function を保存したら、準備のところで書いた Datadog のページにある JSON の例を使ってテストしましょう。エラーなく Backlogに課題登録されましたね。
API Gateway から Lambda への連携
Datadog からの通知を受け付ける API を作成します。
「API の作成」を押します。
API 名とエンドポイントタイプを決めます。
続いてリソースを作成しましょう。
今回は test というリソースを作成します。
次にメソッドを作りましょう。
POST を選んで、Lambda function を指定します。
OK を押すと、Lambda のトリガーとして API Gateway が登録されます。
こんな図が出来上がりましたね。ではテストを押します。
一番下のリクエスト本文のところに POST する JSON を書いて「テスト」を押します。
ステータスが 200 になってることを確認しましょう。
テスト結果に問題がなければデプロイしましょう。
デプロイするステージを指定します。今回は test ステージを作りました。
画面上部の「URL の呼び出し」に、API の URL が書かれています。この URL に POST することで Backlog に課題登録されるようになります。
Datadog から API Gateway へ投げる
では Datadog の設定をしていきましょう。
Integrations から webhook を探してクリックします。
API Gateway で作成した URL の後ろにリソース名を追加した形で URL を指定します。今回だとちょっとぶさいくですが、https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/test/test って形になります。最初の test がステージ名、後ろの test がリソースです。わかりやすいように名前変えようと一瞬だけ思いましたが、気にせず進めます。
@webhook で始まる名前をアラートテキストのところで指定すれば OK です。
テストで監視対象の EC2 インスタンスを停止してみました。しばらくすると、Backlog に課題が登録されました!
Slack にも通知されました!これで一応仕組みができあがりました!
ここに至るまでにつまづいたところ
API Gateway からテストした時は問題なかったのに、Datadog からテストした時は↓このエラーばっかりでて最初悩みまくってました。
{"message":"Missing Authentication Token"}
application/x-www-form-urlencoded をリクエストに含めるとか、リクエストの認証がなしだったらうまくいかないとか、いろんな情報が出てきましたがどれも違ってて、結局 Datadog で指定する Webhook URL の最後の /test がなかっただけ、というオチでした。でもおかげでステージとリソースの関係が理解できたのでよかったです。
あとは、リソースの設定を変更したらそのたびにデプロイしないと反映されないとかも体験できたので、勇気を出して時間かけて API Gateway 使ってみてよかったと思います。
また時間作って Lambda Function の秘密の情報のところをべた書きじゃなくて、Secrets Manager とか Parameter Store とかつかって置き換えていきます!
2018/09/07追記
続編書きました!秘密の情報を Secrets Manager を使って置き換えました!
nasrinjp1.hatenablog.com
EBS ボリューム ID とデバイス名とドライブレターの関連を Systems Manager で確認したい
先日 Windows Server でボリューム ID とドライブレターを関連付ける記事を書きました。 nasrinjp1.hatenablog.com
これ単純に PowerShell 実行してるだけなので、Systems Manager で簡単にできるんじゃないの?って思ったのでやってみました。 簡単は簡単なんですが、ちょっとだけてこずりました。
とりあえず PowerShell スクリプトを実行するドキュメントを作成
Systems Manager の既存の RunCommand ドキュメントから適当に aws:runPowerShellScript を実行してるものを探して、そのコンテンツを参考にスクリプトを埋め込みます。 前回記事で書いたスクリプトを実行するように書いた JSON が↓です。 JSON の中に PowerShell を書く場合は、下記に注意しましょう。
{ "schemaVersion": "1.2", "description": "Mapping Disks to Volumes on Your Windows Instance.", "runtimeConfig": { "aws:runPowerShellScript": { "properties": [ { "id": "0.aws:runPowerShellScript", "timeoutSeconds": 7200, "runCommand": [ "function Get-EC2InstanceMetadata", "{", " param([string]$Path)", " (Invoke-WebRequest -Uri \"http://169.254.169.254/latest/$Path\").Content ", "}", "", "function Convert-SCSITargetIdToDeviceName", "{", " param([int]$SCSITargetId)", " If ($SCSITargetId -eq 0) {", " return \"/dev/sda1\"", " }", " $deviceName = \"xvd\"", " If ($SCSITargetId -gt 25) {", " $deviceName += [char](0x60 + [int]($SCSITargetId / 26))", " }", " $deviceName += [char](0x61 + $SCSITargetId % 26)", " return $deviceName", "}", "", "Try {", " $InstanceId = Get-EC2InstanceMetadata \"meta-data/instance-id\"", " $AZ = Get-EC2InstanceMetadata \"meta-data/placement/availability-zone\"", " $Region = $AZ.Remove($AZ.Length - 1)", " $BlockDeviceMappings = (Get-EC2Instance -Region $Region -Instance $InstanceId).Instances.BlockDeviceMappings", " $VirtualDeviceMap = @{}", " (Get-EC2InstanceMetadata \"meta-data/block-device-mapping\").Split(\"`n\") | ForEach-Object {", " $VirtualDevice = $_", " $BlockDeviceName = Get-EC2InstanceMetadata \"meta-data\/block-device-mapping\/$VirtualDevice\"", " $VirtualDeviceMap[$BlockDeviceName] = $VirtualDevice", " $VirtualDeviceMap[$VirtualDevice] = $BlockDeviceName", " }", "}", "Catch {", " Write-Host \"Could not access the AWS API, therefore, VolumeId is not available. ", " Verify that you provided your access keys.\" -ForegroundColor Yellow", "}", "", "Get-WmiObject -Class Win32_DiskDrive | ForEach-Object {", " $DiskDrive = $_", " $Volumes = Get-WmiObject -Query \"ASSOCIATORS OF {Win32_DiskDrive.DeviceID='$($DiskDrive.DeviceID)'} WHERE AssocClass=Win32_DiskDriveToDiskPartition\" | ForEach-Object {", " $DiskPartition = $_", " Get-WmiObject -Query \"ASSOCIATORS OF {Win32_DiskPartition.DeviceID='$($DiskPartition.DeviceID)'} WHERE AssocClass=Win32_LogicalDiskToPartition\"", " }", " If ($DiskDrive.PNPDeviceID -like \"*PROD_PVDISK*\") {", " $BlockDeviceName = Convert-SCSITargetIdToDeviceName($DiskDrive.SCSITargetId)", " $BlockDevice = $BlockDeviceMappings | Where-Object { $_.DeviceName -eq $BlockDeviceName }", " $VirtualDevice = If ($VirtualDeviceMap.ContainsKey($BlockDeviceName)) { $VirtualDeviceMap[$BlockDeviceName] } Else { $null }", " } ElseIf ($DiskDrive.PNPDeviceID -like \"*PROD_AMAZON_EC2_NVME*\") {", " $BlockDeviceName = Get-EC2InstanceMetadata \"meta-data/block-device-mapping/ephemeral$($DiskDrive.SCSIPort - 2)\"", " $BlockDevice = $null", " $VirtualDevice = If ($VirtualDeviceMap.ContainsKey($BlockDeviceName)) { $VirtualDeviceMap[$BlockDeviceName] } Else { $null }", " } Else {", " $BlockDeviceName = $null", " $BlockDevice = $null", " $VirtualDevice = $null", " }", " New-Object PSObject -Property @{", " Disk = $DiskDrive.Index;", " Partitions = $DiskDrive.Partitions;", " DriveLetter = If ($Volumes -eq $null) { \"N/A\" } Else { $Volumes.DeviceID };", " EbsVolumeId = If ($BlockDevice -eq $null) { \"N/A\" } Else { $BlockDevice.Ebs.VolumeId };", " Device = If ($BlockDeviceName -eq $null) { \"N/A\" } Else { $BlockDeviceName };", " VirtualDevice = If ($VirtualDevice -eq $null) { \"N/A\" } Else { $VirtualDevice };", " VolumeName = If ($Volumes -eq $null) { \"N/A\" } Else { $Volumes.VolumeName };", " }", "} | Sort-Object Disk | Format-Table -AutoSize -Property Disk, Partitions, DriveLetter, EbsVolumeId, Device, VirtualDevice, VolumeName" ] } ] } } }
ざっとドキュメントの作り方を書きます。「Create Document」を押します。
名前は適当につけてください。ドキュメントのタイプは、コマンドのドキュメントを選びましょう。今回はコンテンツは JSON を選んで、↑で書いた JSON をコピペします。これだけです、かんたんですね。
実行してみよう
RunCommand でさきほど作成したドキュメントを実行しましょう。簡単に流れを書きます。
先ほど作成したコマンドのドキュメントを選びます。
実行対象のインスタンスを選択します。
出力オプションを適当に指定します。S3 に出力しておくと日本語のログも文字化けすることなく見れるので、最低限 S3 には出力するようにしましょう。これで実行します。
エラー出た
あれ? なんか Could not access the AWS API, therefore, VolumeId is not available. ってエラー出てるな… これ Catch で指定したエラー文なので、本当のエラーを確認するために Catch の中を下記に書き換えます。
Write-Host $_.Exception.Message
ちなみに、schemaVersion を 1.2 で作ってしまったので、同じドキュメントを更新してバージョン 2 を作ることができません。
とりあえず、さっき作ったドキュメントを削除して再作成しましょう。 そして RunCommand で再実行します。
エラー内容出ました。Windows Server でスクリプト実行した時はすんなり結果出たのに不思議だ。。。
Internet Explorer エンジンを使用できないか、Internet Explorer の初回起動構成が完了していないため、応答のコンテンツを解析できません。UseBasicParsing パラメーターを指定して再試行してください。
なんか Windows Server を起動して一度もログインしてないか IE 開いてないからこのエラーが出てるように見えますね。
UseBasicParsing オプションとは?
このオプションをつけると IE エンジンを使わずにパースするみたいです。逆にこのオプションをつけなければ IE エンジンを使おうとします。 今回の場合は、AMI から Windows Server のインスタンス起動した直後の状態で IE は開いてもないし設定もしていないのでこうなったみたいです。 前回、OS にログインしてやった時は無意識に IE を開いていたんだろうか…?
TechNet フォーラムにかいてありました。
https://social.technet.microsoft.com/Forums/ja-JP/12e5ada3-5fc6-46c5-a504-e8d51719cad1/invokewebrequest203512999226178123981230012475124611251712522124861?forum=powershellja
PowerShell 6.0.0 以降だとこのオプションがついた状態での実行となるようです。 docs.microsoft.com
UseBasicParsing オプションをつけてみる
PowerShell の 4 行目の Invoke-WebRequest に UseBasicParsing オプションを追加します。 大事なことなので 2 回書きますが、schemaVersion を 1.2 で作ってしまったので同じドキュメントを更新してバージョン 2 を作ることができません。 さっき作ったドキュメントを削除して再作成します。 次からは schemaVersion は 2.2 で作ろうと心の底から思いました。
ここまでの変更点を反映したコンテンツの JSON は↓になります。
{ "schemaVersion": "1.2", "description": "Mapping Disks to Volumes on Your Windows Instance.", "runtimeConfig": { "aws:runPowerShellScript": { "properties": [ { "id": "0.aws:runPowerShellScript", "timeoutSeconds": 7200, "runCommand": [ "function Get-EC2InstanceMetadata", "{", " param([string]$Path)", " (Invoke-WebRequest -Uri \"http://169.254.169.254/latest/$Path\" -UseBasicParsing).Content ", "}", "", "function Convert-SCSITargetIdToDeviceName", "{", " param([int]$SCSITargetId)", " If ($SCSITargetId -eq 0) {", " return \"/dev/sda1\"", " }", " $deviceName = \"xvd\"", " If ($SCSITargetId -gt 25) {", " $deviceName += [char](0x60 + [int]($SCSITargetId / 26))", " }", " $deviceName += [char](0x61 + $SCSITargetId % 26)", " return $deviceName", "}", "", "Try {", " $InstanceId = Get-EC2InstanceMetadata \"meta-data/instance-id\"", " $AZ = Get-EC2InstanceMetadata \"meta-data/placement/availability-zone\"", " $Region = $AZ.Remove($AZ.Length - 1)", " $BlockDeviceMappings = (Get-EC2Instance -Region $Region -Instance $InstanceId).Instances.BlockDeviceMappings", " $VirtualDeviceMap = @{}", " (Get-EC2InstanceMetadata \"meta-data/block-device-mapping\").Split(\"`n\") | ForEach-Object {", " $VirtualDevice = $_", " $BlockDeviceName = Get-EC2InstanceMetadata \"meta-data\/block-device-mapping\/$VirtualDevice\"", " $VirtualDeviceMap[$BlockDeviceName] = $VirtualDevice", " $VirtualDeviceMap[$VirtualDevice] = $BlockDeviceName", " }", "}", "Catch {", " Write-Host $_.Exception.Message", "}", "", "Get-WmiObject -Class Win32_DiskDrive | ForEach-Object {", " $DiskDrive = $_", " $Volumes = Get-WmiObject -Query \"ASSOCIATORS OF {Win32_DiskDrive.DeviceID='$($DiskDrive.DeviceID)'} WHERE AssocClass=Win32_DiskDriveToDiskPartition\" | ForEach-Object {", " $DiskPartition = $_", " Get-WmiObject -Query \"ASSOCIATORS OF {Win32_DiskPartition.DeviceID='$($DiskPartition.DeviceID)'} WHERE AssocClass=Win32_LogicalDiskToPartition\"", " }", " If ($DiskDrive.PNPDeviceID -like \"*PROD_PVDISK*\") {", " $BlockDeviceName = Convert-SCSITargetIdToDeviceName($DiskDrive.SCSITargetId)", " $BlockDevice = $BlockDeviceMappings | Where-Object { $_.DeviceName -eq $BlockDeviceName }", " $VirtualDevice = If ($VirtualDeviceMap.ContainsKey($BlockDeviceName)) { $VirtualDeviceMap[$BlockDeviceName] } Else { $null }", " } ElseIf ($DiskDrive.PNPDeviceID -like \"*PROD_AMAZON_EC2_NVME*\") {", " $BlockDeviceName = Get-EC2InstanceMetadata \"meta-data/block-device-mapping/ephemeral$($DiskDrive.SCSIPort - 2)\"", " $BlockDevice = $null", " $VirtualDevice = If ($VirtualDeviceMap.ContainsKey($BlockDeviceName)) { $VirtualDeviceMap[$BlockDeviceName] } Else { $null }", " } Else {", " $BlockDeviceName = $null", " $BlockDevice = $null", " $VirtualDevice = $null", " }", " New-Object PSObject -Property @{", " Disk = $DiskDrive.Index;", " Partitions = $DiskDrive.Partitions;", " DriveLetter = If ($Volumes -eq $null) { \"N/A\" } Else { $Volumes.DeviceID };", " EbsVolumeId = If ($BlockDevice -eq $null) { \"N/A\" } Else { $BlockDevice.Ebs.VolumeId };", " Device = If ($BlockDeviceName -eq $null) { \"N/A\" } Else { $BlockDeviceName };", " VirtualDevice = If ($VirtualDevice -eq $null) { \"N/A\" } Else { $VirtualDevice };", " VolumeName = If ($Volumes -eq $null) { \"N/A\" } Else { $Volumes.VolumeName };", " }", "} | Sort-Object Disk | Format-Table -AutoSize -Property Disk, Partitions, DriveLetter, EbsVolumeId, Device, VirtualDevice, VolumeName" ] } ] } } }
さあ今度こそ!
無事成功しました!
Disk Partitions DriveLetter EbsVolumeId Device VirtualDevice VolumeName ---- ---------- ----------- ----------- ------ ------------- ----- 0 1 C: vol-0cec1838471c7db24 /dev/sda1 root 1 0 G: vol-06bce859498e0fa05 xvdb ebs2 G drive 2 0 H: vol-0ed34a2e652141916 xvdc ebs3 H drive 3 0 I: vol-06a9e298faeec098b xvdd ebs4 I drive
これで OS にログインしなくても確認できますね
OS にログインしてしまうと権限によってはなんでもできてしまうので、オペミス等で変な操作してしまって業務に影響及ぼしてしまうとかありますが、Systems Manager を使えばそれもなくなりますね。 ログも簡単に残せるので OS 上でのコマンド作業は積極的に Systems Manager を使うようにしていきましょう。
あと、RunCommand ドキュメントの schemaVersion は 2.2 で書きましょう!