タグベースによるEC2インスタンスの夜間停止によるコスト削減

https://github.com/koishitori/Lambda_function_EC2InsetanceChangeState
# 背景

このAWS Lambda関数は、タグ値で制御してEC2インスタンスの起動や停止を行うことができます。EventBridgeを使用した夜間の定期停止や、AWS Change Calendarを利用した不定期な長期停止など、様々なシナリオで活用できる汎用的なEC2インスタンス管理ツールです。Spotインスタンスの停止ができるようになったので、さらなるコスト削減のために作りました。

## 実行環境
### エンジン
Python3.12
### 環境
Lambda

## 関数の概要

この関数は以下の構造のJSONペイロードを受け取ります：

```json
{
  "region": string,  # 対象リージョン
  "tagName": string, # 対象EC2インスタンスのタグ名
  "action": string,  # start（起動）またはstop（停止）
  "test": bool       # テストモード（true/false）
}
```

### 機能

1. 指定された`tagName`の値が`action`または"true"であるEC2インスタンスIDを取得します。
2. `action`が"start"の場合、インスタンスIDとNameタグを表示し、インスタンスを起動します（`test`がtrueの場合を除く）。
3. `action`が"stop"の場合、インスタンスIDとNameタグを表示し、インスタンスを停止します（`test`がtrueの場合を除く）。

## セットアップ

1. AWSアカウントで新しいLambda関数を作成します。
2. ランタイムとしてPython 3.12を使用します。
3. 提供されたPythonコードをLambda関数にコピーします。
4. 適切なIAMロールを設定し、必要最小限の権限を付与します（IAMポリシーセクションを参照）。

## IAMポリシー

Lambda関数の実行ロールに以下のIAMポリシーを付与してください：

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "ec2:StartInstances",
                "ec2:StopInstances"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}
```

このポリシーは、EC2インスタンスの詳細取得、起動/停止、およびログの書き込みに必要な権限を付与します。

## 使用方法

Lambda関数をJSONペイロードで呼び出します。例：

```json
{
  "region": "ap-northeast-1",
  "tagName": "Environment",
  "action": "start",
  "test": false
}
```

これにより、ap-northeast-1リージョンで"Environment"タグの値が"start"または"true"であるすべてのEC2インスタンスが起動されます。

## テスト

ペイロードの`test`パラメータを`true`に設定すると、実際にインスタンスを起動/停止せずに関数を実行できます。これにより、どのインスタンスが影響を受けるかを確認できます。

## ログ

関数は、インスタンスIDとそのNameタグ、および実行されたアクションをCloudWatch Logsに記録します。詳細な実行情報については、Lambda関数のCloudWatchロググループを確認してください。

## 関数の中身
```python
import boto3
import json

def lambda_handler(event, context):
    region = event['region']
    tag_name = event['tagName']
    action = event['action']
    test = event['test']

    ec2 = boto3.client('ec2', region_name=region)

    # Get EC2 instances with the specified tag
    response = ec2.describe_instances(
        Filters=[
            {'Name': f'tag:{tag_name}', 'Values': [action, 'true']}
        ]
    )

    instances = []
    for reservation in response['Reservations']:
        for instance in reservation['Instances']:
            instance_id = instance['InstanceId']
            name = next((tag['Value'] for tag in instance.get('Tags', []) if tag['Key'] == 'Name'), 'N/A')
            instances.append((instance_id, name))

    if not instances:
        return {
            'statusCode': 200,
            'body': json.dumps(f'No instances found with tag {tag_name}={action} or {tag_name}=true')
        }

    for instance_id, name in instances:
        print(f"Instance ID: {instance_id} ({name})")

    instance_ids = [instance[0] for instance in instances]

    if not test:
        if action == 'start':
            ec2.start_instances(InstanceIds=instance_ids)
            message = f"Started instances: {', '.join([f'{id} ({name})' for id, name in instances])}"
        elif action == 'stop':
            ec2.stop_instances(InstanceIds=instance_ids)
            message = f"Stopped instances: {', '.join([f'{id} ({name})' for id, name in instances])}"
        else:
            message = f"Invalid action: {action}. Use 'start' or 'stop'."
    else:
        message = f"Test mode: Would have {action}ed instances: {', '.join([f'{id} ({name})' for id, name in instances])}"

    return {
        'statusCode': 200,
        'body': json.dumps(message)
    }
```