sorta kinda...

主にAWS関連ですが、これに限らずいろいろ勉強したことや思ったことを書いていきます。

Datadog で障害検知したら Slack と Backlog に通知しよう

API Gateway はじめました、那須です。

以前、Backlog の課題追加があったら Slack に投げる記事を書きました。
nasrinjp1.hatenablog.com

プロジェクトでお客さんとのやりとりを Backlog にしておけば、プロジェクト管理する上で手作業で WBS ひいたり返信対象のメールを探して状況を確認したり課題管理表を作る手間も省け、通知を逃したとしても Slack に投げ込まれるのでどちらかで拾うことができていいですね。

運用も同じように自動で連携して通知する仕組みがほしいなーと思ったので、勉強がてらがんばって作ってみました。

 

仕組みの図

こんな感じです。監視システムは Datadog を使いました。前職で使ってたのでとっつきやすかったので。Datadog で障害検知したら Backlog に課題追加しつつ Slack にも通知されるようにしました。グレーの矢印の部分は、飛ばないようにしました。直接 Slack に投げているので二重に通知されてしまうためです。アラートだけは Slack 連携しない、ってことは Integromat なら簡単にできますよ。
f:id:nasrinjp1:20180905163815p:plain

 

Datadog から Slack へ

Datadog の Integration を使えば終わりです。めちゃ簡単です。説明するまでもなくドキュメントもそろってますし、他のブログでも書かれている&楽をしたいので書きませんw

 

Datadog から Backlog へ

この仕組みを作るのに 16 時間ほどつぶしてしまったので、自分のためにも書いていきます。

Backlog に投げるための情報収集

Backlog API を使って課題登録します。課題登録するにあたって、スペース ID、プロジェクト ID、イシュータイプ ID、apiKey が必要です。apiKey 以外は下記画面の URL のところで見れます。
f:id:nasrinjp1:20180905165537p:plain

apiKey は個人を特定して認証するためのキーです。Backlog の↓の画面で登録します。
f:id:nasrinjp1:20180905170246p:plain

あと、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 の作成」を押します。
f:id:nasrinjp1:20180905172636p:plain

API 名とエンドポイントタイプを決めます。
f:id:nasrinjp1:20180905172650p:plain

続いてリソースを作成しましょう。
f:id:nasrinjp1:20180905172704p:plain

今回は test というリソースを作成します。
f:id:nasrinjp1:20180905172716p:plain

次にメソッドを作りましょう。
f:id:nasrinjp1:20180905172728p:plain

POST を選んで、Lambda function を指定します。
f:id:nasrinjp1:20180905172740p:plain

OK を押すと、Lambda のトリガーとして API Gateway が登録されます。
f:id:nasrinjp1:20180905203950p:plain

こんな図が出来上がりましたね。ではテストを押します。
f:id:nasrinjp1:20180905172805p:plain

一番下のリクエスト本文のところに POST する JSON を書いて「テスト」を押します。
f:id:nasrinjp1:20180905172846p:plain

ステータスが 200 になってることを確認しましょう。
f:id:nasrinjp1:20180905172858p:plain

テスト結果に問題がなければデプロイしましょう。
f:id:nasrinjp1:20180905172910p:plain

デプロイするステージを指定します。今回は test ステージを作りました。
f:id:nasrinjp1:20180905172920p:plain

画面上部の「URL の呼び出し」に、API の URL が書かれています。この URL に POST することで Backlog に課題登録されるようになります。
f:id:nasrinjp1:20180905172930p:plain

 

Datadog から API Gateway へ投げる

では Datadog の設定をしていきましょう。

Integrations から webhook を探してクリックします。
f:id:nasrinjp1:20180905174400p:plain

API Gateway で作成した URL の後ろにリソース名を追加した形で URL を指定します。今回だとちょっとぶさいくですが、https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/test/test って形になります。最初の test がステージ名、後ろの test がリソースです。わかりやすいように名前変えようと一瞬だけ思いましたが、気にせず進めます。
f:id:nasrinjp1:20180905175228p:plain

@webhook で始まる名前をアラートテキストのところで指定すれば OK です。
f:id:nasrinjp1:20180905174433p:plain

テストで監視対象の EC2 インスタンスを停止してみました。しばらくすると、Backlog に課題が登録されました!
f:id:nasrinjp1:20180905175629p:plain

Slack にも通知されました!これで一応仕組みができあがりました!
f:id:nasrinjp1:20180905175811p:plain

 

ここに至るまでにつまづいたところ

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 で確認したい

今日も JSON ネタです、那須です。

先日 Windows Server でボリューム ID とドライブレターを関連付ける記事を書きました。 nasrinjp1.hatenablog.com

これ単純に PowerShell 実行してるだけなので、Systems Manager で簡単にできるんじゃないの?って思ったのでやってみました。 簡単は簡単なんですが、ちょっとだけてこずりました。

 

とりあえず PowerShell スクリプトを実行するドキュメントを作成

Systems Manager の既存の RunCommand ドキュメントから適当に aws:runPowerShellScript を実行してるものを探して、そのコンテンツを参考にスクリプトを埋め込みます。 前回記事で書いたスクリプトを実行するように書いた JSON が↓です。 JSON の中に PowerShell を書く場合は、下記に注意しましょう。

  • 1 行ごとに "(ダブルクォーテーション) で囲む
  • スクリプト内に出てくる "(ダブルクォーテーション) は \(バックスラッシュ) でエスケープする
{
    "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」を押します。
f:id:nasrinjp1:20180821103234p:plain

名前は適当につけてください。ドキュメントのタイプは、コマンドのドキュメントを選びましょう。今回はコンテンツは JSON を選んで、↑で書いた JSON をコピペします。これだけです、かんたんですね。
f:id:nasrinjp1:20180821103258p:plain

 

実行してみよう

RunCommand でさきほど作成したドキュメントを実行しましょう。簡単に流れを書きます。 先ほど作成したコマンドのドキュメントを選びます。
f:id:nasrinjp1:20180821103406p:plain

実行対象のインスタンスを選択します。
f:id:nasrinjp1:20180821103612p:plain

出力オプションを適当に指定します。S3 に出力しておくと日本語のログも文字化けすることなく見れるので、最低限 S3 には出力するようにしましょう。これで実行します。
f:id:nasrinjp1:20180821103734p:plain

 

エラー出た

あれ? なんか Could not access the AWS API, therefore, VolumeId is not available. ってエラー出てるな… これ Catch で指定したエラー文なので、本当のエラーを確認するために Catch の中を下記に書き換えます。

Write-Host $_.Exception.Message

ちなみに、schemaVersion を 1.2 で作ってしまったので、同じドキュメントを更新してバージョン 2 を作ることができません。

docs.aws.amazon.com

とりあえず、さっき作ったドキュメントを削除して再作成しましょう。 そして 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 で書きましょう!