AWSIaCProgram学習

AWS CDKを使ってEC2の起動と停止を自動化してみた(python)

AWS

LambdaとEventBridgeでEC2電源操作(起動と停止)を自動化し、それをAWS CDKでPythonでコード化してみました。

Lambdaで特定のタグを持つ EC2 インスタンスの自動起動・停止をする環境を構築しました。CloudWatch Events(EventBridge)のスケジュールで定期的に実行します。

テスト環境のEC2インスタンスなどの停止や起動をこまめに行ってコストカットする目的です。

図するとこんな感じですね。

1. 前提条件

  • 既にEC2インスタンスは存在する
  • オレゴンリージョンで実施する
  • CDKとLambdaのプログラム言語はPythonを利用する
  • Windows PC(Python3.9はインストール済み)で開発を実施する

2. AWS CDKのインストール

こちらは省略いたします。以下にもまとめています。

WindowでAWS CDKを使ってPythonでInfra as codeを試してみる(その1)
https://syachiku.net/windowaws-cdkpythoninfra-as-code1/

> cdk --version
1.122.0 (build ae09c16)

3. CDKプロジェクトの新規作成

それではプロジェクトのディレクトリを作成してからcdkプロジェクトを新規作成します。
今回はプロジェクト名を「aws-cdk-auto-power-ec2」とします。

> mkdir aws-cdk-auto-power-ec2
> cd aws-cdk-auto-power-ec2
> cdk init app --language=python

3.1. venv環境の作成

cdkプロジェクトを作成すると以下のようなディレクトリ構成になります。

まずは、Readmeにある通りにvenv環境を作成します。

$ python -m venv .venv
$ source .venv/bin/activate
$ .venv\Scripts\activate.bat
(.venv) C:\Project\aws-cdk-auto-power-ec2>

3.2. cdkモジュールの追加インストール

今回使うcdkのモジュールを追加します。

> pip install aws_cdk.aws_lambda 
> pip install aws_cdk.aws_iam
> pip install aws_cdk.aws_events
> pip install aws_cdk.aws_events_targets

// 確認
> pip list

4. CDK初期化(cdk bootstrap)

初回に必要な処理として、CDKで利用するリソースを置いておく S3 バケットを作成します。

AWS CLIのデフォルト設定は東京(ap-north-2)なのですが、今回はオレゴン(us-west-2)に作りたかったのでprofileで指定します。
※AWS アカウント、リージョン単位で一度だけ実行するコマンドなので過去に作成したことがあれば不要です。

> cdk --profile uw2 bootstrap

CloudFormation上に「CDKToolkit」スタックが作成されています。

WindowsのAWS CLIにprofileを追加する
https://syachiku.net/windows-aws-cli-profile/

5. 作成したCDKテンプレート

それでは実際にスタックで作成するCDKテンプレートを作成していきます。
テンプレートは最初にほぼ空の状態で「aws_cdk_auto_power_ec2」ディレクトリにある「aws_cdk_auto_power_ec2_stack.py」ファイルとして作成されてます。

このファイルを修正していきます。
ゼロからCloudFormaitonを作るよりもかなり簡単にでき、コード量もかなり少なくてすみます。
内容としてはそんなに複雑ではありません
Lambdaファンクションと実行時のRoleを作成し、イベントルールをStartとStop用で2つ作ってLambdaファンクションに紐づけています。

import aws_cdk.core

from aws_cdk import (
    aws_lambda as _lambda,
    aws_events as events,
    aws_events_targets as targets,
    aws_iam as iam,
    core
)

class AwsCdkAutoPowerEc2Stack(core.Stack):

    def __init__(self, scope: core.Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # The code that defines your stack goes here

        # create lambda
        lambda_fn = _lambda.Function(
            self, 'AutoPowerHandler',
            runtime=_lambda.Runtime.PYTHON_3_9,
            code=_lambda.Code.asset('lambda'),
            handler='auto_power_ec2.main',
            memory_size=256,
            description="auto power ec2",
            timeout=aws_cdk.core.Duration.seconds(10)
        )

        lambda_fn.add_to_role_policy(iam.PolicyStatement(
            effect=iam.Effect.ALLOW,
            actions=[
                'ec2:DescribeInstances',
                'ec2:StartInstances',
                'ec2:StopInstances',
            ],
            resources=['*']
        ))

        # create stop rule
        stop_rule = events.Rule(self, 'StopRule',
                                schedule=events.Schedule.cron(
                                    minute='0',
                                    hour='8',
                                    month='*',
                                    week_day='MON-SAT',
                                    year='*'),
                                )

        stop_rule.add_target(targets.LambdaFunction(lambda_fn,
                                                    event=events.RuleTargetInput.from_object(
                                                        {"Region": "us-west-2",
                                                         "Action": 'stop'})
                                                    ));

        # create start rule
        start_rule = events.Rule(self, 'StartRule',
                                 schedule=events.Schedule.cron(
                                     minute='0',
                                     hour='23',
                                     month='*',
                                     week_day='MON-SAT',
                                     year='*'),
                                 )

        start_rule.add_target(targets.LambdaFunction(lambda_fn,
                                                     event=events.RuleTargetInput.from_object(
                                                         {"Region": "us-west-2",
                                                          "Action": 'start'})
                                                     ));

6. 作成したLambdaファンクション

先ほどのcdkテンプレート内でも定義していますがlambdaディレクトリは/lambdaとしています。
以下のようなPythonスクリプトを配置します。

ネット上にあった記事をいくつか参考にさせて頂きました。感謝です。

import boto3
import traceback

def get_target_ec2_instances(ec2_client):
    responce = ec2_client.describe_instances(
        Filters=[{'Name': 'tag:AutoPower', "Values": ['TRUE']}])

    # get instances running and stopped
    target_instances = []
    for reservation in responce['Reservations']:
        if 'Instances' in reservation.keys() and len(reservation['Instances']) > 0:
            for instance in reservation['Instances']:
                if instance['State']['Name'] == 'running' or instance['State']['Name'] == 'stopped':
                    instance_name = ''
                    for tag in instance['Tags']:
                        if tag['Key'] == 'Name':
                            instance_name = tag['Value']
                            break

                    target_instances.append({
                        'instance_id': instance['InstanceId'],
                        'instance_name': instance_name
                    })

    return target_instances


def start_stop_instance(ec2_client, instance, action):
    # type: (boto3.EC2.Client, dict, str) -> bool

    try:
        if action == 'start':
            print('starting instance (ID: {id} Name: {name})'.format(
                id=instance['instance_id'], name=instance['instance_name']))
            res = ec2_client.start_instances(InstanceIds=[instance['instance_id']])
            print(res)
            return True

        elif action == 'stop':
            print('stopping instance (ID: {id} Name: {name})'.format(
                id=instance['instance_id'], name=instance['instance_name']))
            res = ec2_client.stop_instances(InstanceIds=[instance['instance_id']])
            print(res)
            return True
        else:
            print('Invalid action.')
            return False

    except Exception:
        print('[ERROR] failed to {action} an EC2 instance.'.format(action=action))
        print(traceback.format_exc())
        return False


def return_responce(status_code, message):
    # type: (int, str) -> dict
    print(message)
    return {
        'statusCode': status_code,
        'message': message}


def main(event, context):
    try:
        region = event['Region']
        action = event['Action']

        if action not in ['start', 'stop']:
            return_responce(400, 'Invalid action. "action" support "start" or "stop".')

        # create boto3 client
        client = boto3.client('ec2', region)
        target_instances = get_target_ec2_instances(client)

        if len(target_instances) == 0:
            return_responce(200, 'There are no instances subject to automatic {}.'.format(action))

        for instance in target_instances:
            start_stop_instance(client, instance, action)

        return {
            'statusCode': 200,
            'message': ('Finished automatic {action} EC2 instances process. '
                        '[Region: {region}, Action: {action}]').format(
                region=event['Region'], action=event['Action'])}
    except Exception:
        print(traceback.format_exc())
        return {
            'statusCode': 500,
            'message': 'An error occured at automatic start / stop EC2 instances process.'}

7. CDKデプロイ(cdk deploy)

それではDeployしてみます。
もし、デプロイ後に修正した場合にも同じコマンドで更新されます(UpdateStackされます)

> cdk diff --profile uw2
> cdk deploy --profile uw2

8. 確認

CloudFormation上に「aws-cdk-auto-power-ec2」スタックが作成されています。

また、トリガーとしてEvent2つが紐づいているLambdaFunctionが作成されています。

今回の例だと日本時間で8:00に起動して、17:00に停止するようになります、ここら辺は適宜変更してみてください。
少し修正すればタグに時間などを埋め込んで操作する事も簡単にできると思います。

9. まとめ

LambdaとEventBridgeでEC2電源操作(起動と停止)を自動化し、それをAWS CDKでコード化してみました。
やはりCDKを使うことで使いやすい言語でコード化できますし、なによりコードが簡素化できますね、かなり便利だと思います。

Pythonのオススメ勉強方法

私がオススメするPython初心者向けの最初に購入すべき書籍は「シリコンバレー一流プログラマーが教える Pythonプロフェッショナル大全です。

シリコンバレー一流プログラマーが教える Pythonプロフェッショナル大全

この書籍は実際にシリコンバレーの一流エンジニアとして活躍している酒井潤さんが書いた本です。

内容も初心者から上級者までまとめられており、各Lessonも長すぎずに分かりやすくまとめられているので、初心者の方にもおすすめです。

シリコンバレー一流プログラマーが教える Pythonプロフェッショナル大全

今回は以上となります。

コメント

タイトルとURLをコピーしました