CloudWatch アラームを Redmine のチケットに起票する

# やりたいこと

CloudWatch アラームが ALARM 状態になったら Redmine のチケットに起票したい。

なお、Redmine チケットは次のようにしたい。

- プロジェクト： **test_project**
- トラッカー： **Alarm**
- チケットの優先度
  - CloudWatch アラーム名に Critical が含まれる場合は、優先度 **Urgent** とする
  - CloudWatch アラーム名に Error が含まれる場合は、優先度 **High** とする


# どうやるか？

Redmine API が提供されているので、これを参考に行う。

https://www.redmine.org/projects/redmine/wiki/Rest_api


# 構成図

実際は、CloudWatch metrics のアクションとして、Lambda を指定しますが、今回は Lamda からテストイベントを作成し実行させるので、CloudWatch metrics の作成は手順から省いています。
![redmine.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/93046c72-25d6-a488-1232-fc34440f9a72.png)


# 手順

## Lightsail で Redmine を作成する

本題は CloudWatch アラームが ALARM 状態になったら Redmine のチケットに起票するという点ですので、Redmine の作成において今回は簡易的に Lightsail で作成しておきます。

![スクリーンショット 2024-05-09 13.11.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/3861a9c5-ee31-034a-1ef8-bc39867f8d76.png)

## ドキュメントを参考に「ステップ 1 〜 4」を行う

繰り返しになりますが、本題は CloudWatch アラームが ALARM 状態になったら Redmine のチケットに起票するという点ですので、「ステップ 5 〜 8」は今回は飛ばします。

https://docs.aws.amazon.com/ja_jp/lightsail/latest/userguide/amazon-lightsail-quick-start-guide-redmine.html

## プロジェクト,トラッカーの作成、チケットの優先度の確認

プロジェクト： **test_project** を追加します。
![スクリーンショット 2024-05-09 13.25.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/8cbcd599-9715-dd19-d96c-8fa6f9fb38e4.png)

トラッカー： **Alarm** を追加します。
![スクリーンショット 2024-05-09 13.25.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/789dc938-9733-b292-4636-aa5864cc3531.png)

チケットの優先度: **Urgent** と **High** は既に存在することだけを確認します。
![スクリーンショット 2024-05-09 13.26.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/3ad2c0f6-e9ac-cb24-37bc-2c0b9cf446b0.png)

## ユーザーと API キーの作成

Redmine の REST API を有効にするため、「RESTによるWebサービスを有効にする」にチェックを入れ、「保存」します。
![スクリーンショット 2024-05-09 13.49.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/58ece3ec-49bf-57a8-f139-5b671e3bef5d.png)

ユーザーを作成します。ここでは、**aws** としています。
![スクリーンショット 2024-05-09 13.46.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/6fb048b1-c670-1c71-2150-8b4fdcf46250.png)

新しく作成した aws のアカウントでログインし、「個人設定」から API アクセスキーを確認し控えます。
![スクリーンショット 2024-05-08 13.52.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/914d1260-f504-21b7-addf-783b32d6fdac.png)

## プロジェクト,トラッカー,チケットの優先度の id の確認

プロジェクトの test_project の id: 1 を確認。

```xml:projects.xml
<projects total_count="1" offset="0" limit="25" type="array">
  <project>
    <id>1</id>
    <name>test_project</name>
    <identifier>test_project</identifier>
    <description/>
    <homepage/>
    <status>1</status>
    <is_public>true</is_public>
    <inherit_members>false</inherit_members>
    <created_on>2024-05-08T02:40:49Z</created_on>
    <updated_on>2024-05-08T02:40:49Z</updated_on>
  </project>
</projects>
```

トラッカーの Alarm の id: 6 を確認。

```xml:trackers.xml
<trackers type="array">
  <tracker>
    <id>1</id>
    <name>Bug</name>
    <default_status id="1" name="New"/>
    <description/>
    <enabled_standard_fields type="array">
      <field>assigned_to_id</field>
      <field>category_id</field>
      <field>fixed_version_id</field>
      <field>parent_issue_id</field>
      <field>start_date</field>
      <field>due_date</field>
      <field>estimated_hours</field>
      <field>done_ratio</field>
      <field>description</field>
      <field>priority_id</field>
    </enabled_standard_fields>
  </tracker>
  <tracker>
    <id>2</id>
    <name>Feature</name>
    <default_status id="1" name="New"/>
    <description/>
    <enabled_standard_fields type="array">
      <field>assigned_to_id</field>
      <field>category_id</field>
      <field>fixed_version_id</field>
      <field>parent_issue_id</field>
      <field>start_date</field>
      <field>due_date</field>
      <field>estimated_hours</field>
      <field>done_ratio</field>
      <field>description</field>
      <field>priority_id</field>
    </enabled_standard_fields>
  </tracker>
  <tracker>
    <id>3</id>
    <name>Support</name>
    <default_status id="1" name="New"/>
    <description/>
    <enabled_standard_fields type="array">
      <field>assigned_to_id</field>
      <field>category_id</field>
      <field>fixed_version_id</field>
      <field>parent_issue_id</field>
      <field>start_date</field>
      <field>due_date</field>
      <field>estimated_hours</field>
      <field>done_ratio</field>
      <field>description</field>
      <field>priority_id</field>
    </enabled_standard_fields>
  </tracker>
  <tracker>
    <id>6</id>
    <name>Alarm</name>
    <default_status id="1" name="New"/>
    <description/>
    <enabled_standard_fields type="array">
      <field>assigned_to_id</field>
      <field>category_id</field>
      <field>fixed_version_id</field>
      <field>parent_issue_id</field>
      <field>start_date</field>
      <field>due_date</field>
      <field>estimated_hours</field>
      <field>done_ratio</field>
      <field>description</field>
      <field>priority_id</field>
    </enabled_standard_fields>
  </tracker>
</trackers>
```

チケットの優先度の 
 Urgent の id: 4
 High の id: 3 
 Normal の id: 2
を確認。

```xml:enumerations/issue_priorities.xml
<issue_priorities type="array">
  <issue_priority>
    <id>5</id>
    <name>Immediate</name>
    <is_default>false</is_default>
    <active>true</active>
  </issue_priority>
  <issue_priority>
    <id>4</id>
    <name>Urgent</name>
    <is_default>false</is_default>
    <active>true</active>
  </issue_priority>
  <issue_priority>
    <id>3</id>
    <name>High</name>
    <is_default>false</is_default>
    <active>true</active>
  </issue_priority>
  <issue_priority>
    <id>1</id>
    <name>Low</name>
    <is_default>false</is_default>
    <active>true</active>
  </issue_priority>
  <issue_priority>
    <id>2</id>
    <name>Normal</name>
    <is_default>false</is_default>
    <active>true</active>
  </issue_priority>
</issue_priorities>
```

## Lambda の実装

**##** になっている箇所を自身の環境に合わせて修正し、Lambda 関数を作成します。

```python:redmine.py
import json
import urllib.request
import ssl

def lambda_handler(event, context):
    # CloudWatchからのイベント情報を取得
    alarm_name = event['detail']['alarmName']
    alarm_description = event['detail']['alarmDescription']
    new_state = event['detail']['newState']['stateValue']
    
    # new_stateがALARMの場合のみRedmineのチケットに登録
    if new_state != 'ALARM':
        print('CloudWatchアラームはALARM状態ではありません。Redmineチケットへの登録をスキップします。')
        return
    
    # 優先度のデフォルト設定
    priority_id = 2  ## enumerations/issue_priorities.xml で確認した Normal id
    
    # alarm_nameに応じて優先度を設定
    if 'Error' in alarm_name:
        priority_id = 3  ## enumerations/issue_priorities.xml で確認した High id
    elif 'Critical' in alarm_name:
        priority_id = 4  ## enumerations/issue_priorities.xml で確認した Urgent id
    # RedmineのAPIエンドポイントと認証情報
    redmine_url = 'https://X.X.X.X/issues.json'  ## redmine のip
    redmine_api_key = 'abcdefghi1234567' ## redmine aws ユーザーの API アクセスキー

    # SSL証明書の検証を無効にする
    ssl_context = ssl.create_default_context()
    ssl_context.check_hostname = False
    ssl_context.verify_mode = ssl.CERT_NONE
    
    # CloudWatchアラームの情報をRedmineチケットに記録するためのデータを準備
    ticket_data = {
        'issue': {
            'project_id': 1, ## projects.xml で確認した test_project id
            'tracker_id': 6, ## trackers.xml で確認した Alarm id
            'priority_id': priority_id,
            'subject': f'Alarm: {alarm_name} is now {new_state}',
            'description': f'{alarm_description}\n\nCloudWatch Alarm: {alarm_name} is now {new_state}'
        }
    }
    
    # RedmineにPOSTリクエストを送信してチケットを作成
    req = urllib.request.Request(
        redmine_url, 
        data=json.dumps(ticket_data).encode('utf-8'), 
        headers={'Content-Type': 'application/json', 'X-Redmine-API-Key': redmine_api_key})
    try:
        with urllib.request.urlopen(req, context=ssl_context) as response:
            response_data = response.read().decode('utf-8')
            print(response_data)
            print('Redmineチケットが正常に登録されました。')
    except urllib.error.HTTPError as e:
        print('Redmineチケットの作成中にエラーが発生しました。')
        print(e)
```

# 動作確認

## Critical_test

CloudWatch アラーム名に Critical が含まれる場合は、優先度 Urgent とするを行いたいと思います。
次のようなテストイベントを作成し、Lambda を実行させます。

```json:Critical_test.json
{
  "version": "0",
  "id": "b80d68ad-1c0b-4b9e-8c1e-092fe533b4a5",
  "detail-type": "CloudWatch Alarm State Change",
  "source": "aws.cloudwatch",
  "account": "123456789012",
  "time": "2024-05-08T12:34:56Z",
  "region": "ap-northeast-1.",
  "resources": [
    "arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:Critical_test"
  ],
  "detail": {
    "alarmName": "Critical_test",
    "alarmDescription": "これは緊急テストです。",
    "oldState": {
      "stateValue": "OK",
      "stateReason": "Threshold Crossed: 1 out of the last 1 datapoints [1.0 (08/05/24 12:32:00)] was greater than the threshold (0.0)."
    },
    "newState": {
      "stateValue": "ALARM",
      "stateReason": "Threshold Crossed: 1 out of the last 1 datapoints [1.0 (08/05/24 12:32:00)] was greater than the threshold (0.0).",
      "stateReasonData": {
        "version": "1.0",
        "queryDate": "2024-05-08T12:34:56.005Z",
        "startDate": "2024-05-08T12:32:00.000Z",
        "statistic": "Sum",
        "period": 60,
        "recentDatapoints": [
          1
        ]
      }
    }
  }
}
```

実行結果は以下の通りです。

```json:Critical_test_logs.json
{
    "issue": {
        "id": 32,
        "project": {
            "id": 1,
            "name": "test_project"
        },
        "tracker": {
            "id": 6,
            "name": "Alarm"
        },
        "status": {
            "id": 1,
            "name": "New",
            "is_closed": false
        },
        "priority": {
            "id": 4,
            "name": "Urgent"
        },
        "author": {
            "id": 6,
            "name": "aws aws"
        },
        "subject": "Alarm: Critical_test is now ALARM",
        "description": "これは緊急テストです。\r\n\r\nCloudWatch Alarm: Critical_test is now ALARM",
        "start_date": "2024-05-09",
        "due_date": null,
        "done_ratio": 0,
        "is_private": false,
        "estimated_hours": null,
        "total_estimated_hours": null,
        "created_on": "2024-05-09T05:39:31Z",
        "updated_on": "2024-05-09T05:39:31Z",
        "closed_on": null
    }
}
```

## Error_test

CloudWatch アラーム名に Error が含まれる場合は、優先度 High とするを行いたいと思います。
次のようなテストイベントを作成し、Lambda を実行させます。

```json:Error_test.json
{
  "version": "0",
  "id": "b80d68ad-1c0b-4b9e-8c1e-092fe533b4a5",
  "detail-type": "CloudWatch Alarm State Change",
  "source": "aws.cloudwatch",
  "account": "123456789012",
  "time": "2024-05-08T12:34:56Z",
  "region": "ap-northeast-1.",
  "resources": [
    "arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:Error_test"
  ],
  "detail": {
    "alarmName": "Error_test",
    "alarmDescription": "これはエラーテストです。",
    "oldState": {
      "stateValue": "OK",
      "stateReason": "Threshold Crossed: 1 out of the last 1 datapoints [1.0 (08/05/24 12:32:00)] was greater than the threshold (0.0)."
    },
    "newState": {
      "stateValue": "ALARM",
      "stateReason": "Threshold Crossed: 1 out of the last 1 datapoints [1.0 (08/05/24 12:32:00)] was greater than the threshold (0.0).",
      "stateReasonData": {
        "version": "1.0",
        "queryDate": "2024-05-08T12:34:56.005Z",
        "startDate": "2024-05-08T12:32:00.000Z",
        "statistic": "Sum",
        "period": 60,
        "recentDatapoints": [
          1
        ]
      }
    }
  }
}
```

実行結果は以下の通りです。

```json:Error_test_logs.json
{
    "issue": {
        "id": 33,
        "project": {
            "id": 1,
            "name": "test_project"
        },
        "tracker": {
            "id": 6,
            "name": "Alarm"
        },
        "status": {
            "id": 1,
            "name": "New",
            "is_closed": false
        },
        "priority": {
            "id": 3,
            "name": "High"
        },
        "author": {
            "id": 6,
            "name": "aws aws"
        },
        "subject": "Alarm: Error_test is now ALARM",
        "description": "これはエラーテストです。\r\n\r\nCloudWatch Alarm: Error_test is now ALARM",
        "start_date": "2024-05-09",
        "due_date": null,
        "done_ratio": 0,
        "is_private": false,
        "estimated_hours": null,
        "total_estimated_hours": null,
        "created_on": "2024-05-09T05:40:02Z",
        "updated_on": "2024-05-09T05:40:02Z",
        "closed_on": null
    }
}
```

## redmine で確認

redmine で想定通り登録されたことを確認できれば完了です。
![スクリーンショット 2024-05-09 14.44.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/c0c52bf0-a0ef-6691-ca77-aee7a7d9d865.png)