AWS LambdaでEC2, RDS, ECS(Fargate) を自動停止・自動起動

# やりたいこと
 * 運用コスト削減
 * 開発環境の平日の夜間、休日は停止、平日の朝起動

# v2

 * tag
例) RunFrom=9, RunTo=23: 9時に起動、23時に停止
   * Key=RunFrom, Value=起動時間 [0-23]
   * Key=RunTo, Value=停止時間 [0-23]
 * EventBridge: 時ごと
   * cron(0 * ? * * *)

## EC2

```python:lambda_handler Python3.9
import boto3
import json
from datetime import datetime, timedelta
import pprint


def touch_instance(cur_hour, tag_name, run_values):
    log_msg = []
    run_from_ids = []
    run_to_ids = []
    cnt = 0

    client = boto3.client('ec2')
    reservations = client.describe_instances(
        Filters=[{
            'Name': f'tag:{tag_name}',
            'Values': run_values
        }]
    )

    for reservation in reservations['Reservations']:
        for instance in reservation['Instances']:
            cnt = cnt + 1
            inst_id = instance['InstanceId']
            state_name = instance['State']['Name']

            for tag in instance['Tags']:
                key_name = tag['Key']
                
                if key_name in 'RunFrom, RunTo' and tag['Value'] == str(cur_hour):
                    if key_name == 'RunFrom':
                        if state_name != 'running':
                            run_from_ids.append(inst_id)
                        break
                    if key_name == 'RunTo':
                        if state_name != 'stopped':
                            run_to_ids.append(inst_id)
                        break

    try:
        response = []
        if len(run_from_ids) > 0:
            response = client.start_instances(InstanceIds=run_from_ids)
            log_msg.append(f"start: {run_from_ids}")

        if len(run_to_ids) > 0:
            response = client.stop_instances(InstanceIds=run_to_ids)
            log_msg.append(f"stop: {run_to_ids}")

        # if response:
        #     log_msg.append(response)
    except Exception as e:
        log_msg.append(e)
    
    return cnt, log_msg

def lambda_handler(event, context):
    cnt = 0
    log_msg = []
    run_values = [str(n) for n in list(range(24))]
    cur_hour = (datetime.now() + timedelta(hours=9)).hour

    log_msg.append(f'cur_hour: {cur_hour}')

    # 起動
    r_cnt, r_log_msg = touch_instance(cur_hour, 'RunFrom', run_values)
    cnt = cnt + r_cnt
    log_msg.extend(r_log_msg)

    # 停止
    r_cnt, r_log_msg = touch_instance(cur_hour, 'RunTo', run_values)
    cnt = cnt + r_cnt
    log_msg.extend(r_log_msg)
    
    log_msg.append(f'instance count: {cnt}')
    pprint.pprint(log_msg)

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

```

## ECS

```python:lambda_handler Python3.9
import boto3
import json
import pprint
from datetime import datetime, timedelta

def lambda_handler(event, context):
    log_msg = []
    log_err = []
    cnt = 0
    cur_hour = (datetime.now() + timedelta(hours=9)).hour
    log_msg.append(f"cur_hour:{cur_hour}")

    client = boto3.client('ecs')
    pagenator = client.get_paginator('list_clusters')
    page_iterator = pagenator.paginate()
    
    for page in page_iterator:
        for clst in page['clusterArns']:
            svc_pagenator = client.get_paginator('list_services')
            parms = {
                'cluster': clst
            }
            svc_page_iterator = svc_pagenator.paginate(**parms)

            for svc_page in svc_page_iterator:
                if len(svc_page['serviceArns']) < 1:
                    continue

                res_services = client.describe_services(
                    cluster = clst,
                    services = svc_page['serviceArns'],
                    include = [
                        'TAGS'
                    ]
                )

                for service in res_services['services']:
                    cnt = cnt + 1
                    service_arn = service['serviceArn']
                    service_name = service['serviceName']
                    desired_count = service['desiredCount']
                    new_desired_count = -1

                    if 'tags' not in service:
                        print(f"tag not found: {service_name}")
                        continue

                    for tag in service['tags']:
                        key_name = tag['key']

                        if key_name in 'RunFrom, RunTo' and tag['value'] == str(cur_hour):
                            if key_name == 'RunFrom':
                                if desired_count < 1:
                                    new_desired_count = 1
                                break
                            if key_name == 'RunTo':
                                if desired_count > 0:
                                    new_desired_count = 0
                                break

                    # 時間指定外はスルー
                    if new_desired_count == -1:
                        continue

                    response = client.update_service(
                        cluster = clst,
                        service = service_arn,
                        desiredCount = new_desired_count
                    )
                    log_msg.append(f"{service_name}, desired_count:{desired_count}->{new_desired_count}")

    if len(log_err) > 0:
        log_err.append(boto3.client("sts").get_caller_identity()["Arn"])
        log_msg.extend(log_err)

        # fixme
        # 通知

    log_msg.append(f"service count: {cnt}")
    pprint.pprint(log_msg)

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

```

## RDS

```python:lambda_handler Python3.10
import boto3
import json
import pprint
from datetime import datetime, timedelta

def touch_clst(cur_hour, tag_name, run_values):
    log_msg = []
    cnt = 0
    client = boto3.client('rds')
    rdss = client.describe_db_clusters()

    for rds in rdss['DBClusters']:
        dbid = rds['DBClusterIdentifier']
        inst = client.describe_db_clusters(DBClusterIdentifier=dbid)
        inst_status = inst["DBClusters"][0]["Status"]

        tags = client.list_tags_for_resource(
            ResourceName="arn:aws:rds:ap-northeast-1:{account id}:cluster:" + dbid
        )

        for tag in tags['TagList']:
            key_name = tag['Key']
            
            if key_name in 'RunFrom, RunTo' and tag['Value'] == str(cur_hour):
                if key_name == 'RunFrom':
                    if inst_status == 'stopped':
                        response = client.start_db_cluster(DBClusterIdentifier=dbid)
                        log_msg.append(f"start: {dbid}")
                        cnt = cnt + 1
                    break
                if key_name == 'RunTo':
                    if inst_status == 'available':
                        response = client.stop_db_cluster(DBClusterIdentifier=dbid)
                        log_msg.append(f"stop: {dbid}")
                        cnt = cnt + 1
                    break

    return cnt, log_msg

def touch_instance(cur_hour, tag_name, run_values):
    log_msg = []
    cnt = 0
    client = boto3.client('rds')
    rdss = client.describe_db_instances()

    for rds in rdss['DBInstances']:
        dbid = rds['DBInstanceIdentifier']
        inst = client.describe_db_instances(DBInstanceIdentifier=dbid)
        inst_status = inst["DBInstances"][0]["DBInstanceStatus"]

        tags = client.list_tags_for_resource(
            ResourceName="arn:aws:rds:ap-northeast-1:{account id}:db:" + dbid
        )

        for tag in tags['TagList']:
            key_name = tag['Key']
            
            if key_name in 'RunFrom, RunTo' and tag['Value'] == str(cur_hour):
                if key_name == 'RunFrom':
                    if inst_status == 'stopped':
                        response = client.start_db_instance(DBInstanceIdentifier=dbid)
                        log_msg.append(f"start: {dbid}")
                        cnt = cnt + 1
                    break
                if key_name == 'RunTo':
                    if inst_status == 'available':
                        response = client.stop_db_instance(DBInstanceIdentifier=dbid)
                        log_msg.append(f"stop: {dbid}")
                        cnt = cnt + 1
                    break

    return cnt, log_msg

def lambda_handler(event, context):
    cnt = 0
    log_msg = []
    run_values = [str(n) for n in list(range(24))]
    cur_hour = (datetime.now() + timedelta(hours=9)).hour
    log_msg.append(f'cur_hour: {cur_hour}')

    # クラスタ起動
    r_cnt, r_log_msg = touch_clst(cur_hour, 'RunFrom', run_values)
    cnt = cnt + r_cnt
    log_msg.extend(r_log_msg)

    # クラスタ停止
    r_cnt, r_log_msg = touch_clst(cur_hour, 'RunTo', run_values)
    cnt = cnt + r_cnt
    log_msg.extend(r_log_msg)

    # インスタンス起動
    r_cnt, r_log_msg = touch_instance(cur_hour, 'RunFrom', run_values)
    cnt = cnt + r_cnt
    log_msg.extend(r_log_msg)

    # インスタンス停止
    r_cnt, r_log_msg = touch_instance(cur_hour, 'RunTo', run_values)
    cnt = cnt + r_cnt
    log_msg.extend(r_log_msg)
    
    log_msg.append(f'instance count: {cnt}')
    pprint.pprint(log_msg)

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

# v1
  * tag 対象リソースにタグ追加
    * key = lambda:stop, value = on
    * key = lambda:start, value = on
    * valueをon以外は、offのこと

  * CloudWatch Events
    * auto-start cron(30 23 ? * 1-5 *)
    * auto-stop cron(0 15 * * ? *)

## EC2

```python:lambda_handler Python3.9
import boto3
from datetime import datetime, timedelta

def lambda_handler(event, context):
    tag = ''
    cnt = 0
    curr_hour = (datetime.now() + timedelta(hours=9)).hour
    instance_ids = []

    print(curr_hour)
    
    if(curr_hour >= 6 and curr_hour <= 12):
        tag = 'start'
    elif(curr_hour >= 21 and curr_hour <= 24):
        tag = 'stop'
    else:
        return 'NONE'

    ec2 = boto3.client('ec2')
    reservations = ec2.describe_instances(
        Filters=[{
            'Name': 'tag:lambda:' + tag,
            'Values': [
                'on',
            ]},
        ])['Reservations']

    for reservation in reservations:
        instances = reservation['Instances']
        for instance in instances:
            instance_ids.append(instance['InstanceId'])

        #print("Stopping instances: {}".format(','.join(instance_ids)))

    if len(instance_ids) > 0:
        if tag == 'start':
            response = ec2.start_instances(InstanceIds=instance_ids)
        elif tag == 'stop':
            response = ec2.stop_instances(InstanceIds=instance_ids)
        else:
            response = 'NONE'
        
        print(response)
        print('instances: ' + str(instance_ids))

    return "done."
```

## ECS

```python:lambda_handler Python3.9
import boto3
import json
from datetime import datetime, timedelta

def lambda_handler(event, context):
    tagname = None
    desiredCount = 1
    cnt = 0
    curr_hour = (datetime.now() + timedelta(hours=9)).hour

    print('curr_hour', curr_hour)

    if(curr_hour >= 6 and curr_hour <= 12):
        tagname = 'start'
        desiredCount = 1
    elif(curr_hour >= 21 and curr_hour <= 24):
        tagname = 'stop'
        desiredCount = 0

    if tagname is not None:
        clst_seek = ':cluster/'
        service_seek = ':service/'
        
        client = boto3.client('ecs')
        pagenator = client.get_paginator('list_clusters')
        page_iterator = pagenator.paginate()
        
        for page in page_iterator:
            for clst in page['clusterArns']:
                clst_name = clst[clst.find(clst_seek) + len(clst_seek):]

                svc_pagenator = client.get_paginator('list_services')
                parms = {
                    'cluster': clst
                }
                svc_page_iterator = svc_pagenator.paginate(**parms)

                for svc_page in svc_page_iterator:
                    for service in svc_page['serviceArns']:
                        cnt = cnt + 1
                        service_arn = service
                        
                        if not clst_name in service_arn:
                            service_name = service[service.find(service_seek) + len(service_seek):]
                            service_arn = service[:service.find(service_seek) + len(service_seek)] + clst_name + '/' + service_name
                            
                        try:
                            tags = client.list_tags_for_resource(
                                resourceArn = service_arn
                            )
                        except Exception as e:
                            print(e)
                            continue
        
                        for tag in tags['tags']:
                            if tag['key'] == f'lambda:{tagname}' and tag['value'] == 'on':
        
                                response = client.update_service(
                                    cluster = clst,
                                    service = service_arn,
                                    desiredCount = desiredCount
                                )
        
                                print(clst, service_arn, tag['key'], tag['value'])
                                break

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

# v0

## EC2
 * EventBridge (CloudWatch Events)使用 インスタンス単体
 * CloudWatch ルール
   * イベントスケジュール: cron(30 1 6 3 ? 2019)
   * 入力　定数: {"Action": "start", "Region": "ap-northeast-1", "Instances": ["i-xxxxx"]}

```python:lambda_handler
import json
import boto3
 
def lambda_handler(event, context):
    msg = ""
    region = event['Region']
    instances = event['Instances']
    ec2 = boto3.client('ec2', region_name=region)
    
    if event['Action'] == 'start':
        ec2.start_instances(InstanceIds=instances)
        msg = 'started: '
    elif event['Action'] == 'stop':
        ec2.stop_instances(InstanceIds=instances)
        msg = 'stopped: '
        
    msg = msg + ", ".join(instances)
        
    return {
        'statusCode': 200,
        'body': json.dumps(msg)
    }
```
 

# 参考
 * https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html