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の効果的な学習方法は「Udemy(ユーデミー)」によるビデオ学習です。

「Udemy」は、オンライン学習の提供サイトです。学びたい人は多くある講座の中から受講したいコースを選択することができ、動画で学べるのが特徴です。

多くあるPythonのコースの中でもオススメするPythonのコースは以下となります!!

現役シリコンバレーエンジニアが教えるPython 3 入門 + 応用 +アメリカのシリコンバレー流コードスタイル

このコースでは合計で28.5時間のビデオ講座があって、それらを受講するだけで、Pythonの基礎から応用まで学ぶことができます。

私も購入して受講していますが、内容としては初心者の方から上級者まで対応する幅広い内容になっています。

下手な書籍を何冊か購入するより、この動画コースを最初からじっくりと受けることで総合的なスキルを習得することができるできます。おそらくこれ以上の教材はないと思いますので、絶対おすすめです。

ちなみに、Udemyでは頻繁にセール(1か月に2,3回程度)が開催されているので、セールのタイミングで購入すれば90%OFFになる講座もあるため、セールが開催されてからの購入をオススメします!

今回は以上となります。

コメント

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