株式会社ライブキャストロゴ 株式会社ライブキャスト

小規模なWebサイトをAWSで運営しています。突発的なアクセスに備えてインスタンスを増減できるようElastic Load Balancer(Classic Load Balancer)を利用しauto scalingするようしており、稼働中インスタンスのAMIバックアップをLambdaファンクションで夜間にとるようにしています。

今回は、このバックアップしたAMIでAuto Scaling設定を更新するLambdaファンクションを作ってみたいと思います。

こちら記事を参考にさせていただきました。
suz-lab – blog: “Auto Scaling”で利用するAMIをアップデートしてみる

Auto Scalingの設定を更新するコマンドは主にこちらの2つ。

●起動設定(Launch Configuration)を新規作成するコマンド

[root@ip-172-30-0-167 ~]# as-create-launch-config –image-id –group –instance-type <インスタンスタイプ> –region <リージョン>
OK-Created launch config

●Auto Scalingグループを更新するコマンド

[root@ip-172-30-0-167 ~]# as-update-auto-scaling-group –launch-configuration –region <リージョン>
OK-Updated AutoScalingGroup

です。

新規に作成したLaunch Configurationで、Auto Scalingグループを更新する、という流れになります。

それでは、Lambdaファンクションの作成に入ります。

boto3というPythonで書かれたライブラリを使います。
AutoScaling — Boto 3 Docs 1.4.1 documentation
上記2つのコマンド同等のboto3の関数を使っていくことになります。

まず、バックアップされたAMIのIDを取得します。
バックアップは3世代前まで世代管理をしていますので、その中から最新のものを取得しています。

def get_latests_image_id():
    ec2_client = boto3.client('ec2')
    
    sorted_images = []

    response = ec2_client.describe_images(
        Owners  = ['self'],
        Filters = [
            {
                'Name': 'tag:Name',
                'Values': ['Nameタグの値']
            }
        ]
    )

    images = response['Images']
    sorted_images = sorted(
        images, 
        key = lambda x: x['CreationDate'], 
        reverse = True
    )

    return sorted_images[0]['ImageId']

取得したAMI IDを指定して、Launch Configurationを作成します。
こちらの関数を使用します。
http://boto3.readthedocs.io/en/latest/reference/services/autoscaling.html#AutoScaling.Client.create_launch_configuration

def create_launch_config(image_id):
    as_client = boto3.client('autoscaling')

    launch_configuration_name = '起動設定名' + dt.now().strftime('%Y%m%d%H%M%S')

    response = as_client.create_launch_configuration(
        LaunchConfigurationName = launch_configuration_name,
        ImageId = image_id,
        KeyName = 'キーペア名',
        SecurityGroups = ['セキュリティーグループID'],
        InstanceType = 't2.micro',
        BlockDeviceMappings=[
            {
                'DeviceName': '/dev/sda1',
                'Ebs': {
                    'VolumeSize': 8,
                    'VolumeType': 'gp2',
                    'DeleteOnTermination': True
                }
            }
        ]
    )
    
    return launch_configuration_name

BlockDeviceMappingsのパラメータはなくてもLaunch Configurationは作成できますが、実際にauto scaleする際にInServiceにならないので、最低限の情報を指定するようにしました(内容は適宜変更してください)。

        BlockDeviceMappings=[
            {
                'DeviceName': '/dev/sda1',
                'Ebs': {
                    'VolumeSize': 8,
                    'VolumeType': 'gp2',
                    'DeleteOnTermination': True
                }
            }
        ]

古いLaunch Configurationを取得して、
※Launch Configurationの取得にはこちらの関数を使用します。

http://boto3.readthedocs.io/en/latest/reference/services/autoscaling.html?#AutoScaling.Client.describe_launch_configurations

def get_launch_config():
    as_client = boto3.client('autoscaling')

    response = as_client.describe_launch_configurations()
    
    configs = []
    
    if response is not None:
        for config in response['LaunchConfigurations']:
            launch_configuration = config['LaunchConfigurationName']
            
            if ('起動設定名' in launch_configuration):
                configs.append(launch_configuration)
            
    return configs

取得したLaunch Configurationを削除します。
※Launch Configurationの削除はこちらの関数を使用します。
http://boto3.readthedocs.io/en/latest/reference/services/autoscaling.html?#AutoScaling.Client.delete_launch_configuration

def delete_old_launc_config(configs):
    as_client = boto3.client('autoscaling')

    for config in configs:
        response = as_client.delete_launch_configuration(
            LaunchConfigurationName = config
        )
    
    return True

Auto ScalingグループのLaunch Configurationを更新します。
こちらの関数を使用します。
http://boto3.readthedocs.io/en/latest/reference/services/autoscaling.html?#AutoScaling.Client.update_auto_scaling_group

def update_auto_scaling_group(launch_configuration_name):
    as_client = boto3.client('autoscaling')

    as_client.update_auto_scaling_group(
        AutoScalingGroupName = 'オートスケーリンググループ名',
        LaunchConfigurationName = launch_configuration_name
    )
    
    return True

これらの処理をまとめて整理するとこんな感じになりました。

import json
import boto3
from boto3.session import Session

import time
from datetime import datetime as dt

def lambda_handler(event, context):
    ec2_client   = boto3.client('ec2')
    as_client   = boto3.client('autoscaling')

    # get the latest backup of AMI.
    image_id = get_latests_image_id(ec2_client)

    # get all Launch Configurations.
    configs = get_launch_configs(as_client)

    # create new Launch Configuration.
    launch_config_name = create_launch_config(as_client, image_id)

    # update the Auto Scaling group setting.
    update_auto_scaling_group(as_client, launch_config_name)
    
    if (configs is not None):
        delete_old_launc_config(as_client, configs)
        
    return 0

def get_latests_image_id(ec2_client):
    sorted_images = get_sorted_images(ec2_client)
    return sorted_images[0]['ImageId']
    
def get_sorted_images(ec2_client):
    sorted_images = []

    response = ec2_client.describe_images(
        Owners  = ['self'],
        Filters = [
            {
                'Name': 'tag:Name',
                'Values': ['example.com']
            }
        ]
    )

    images = response['Images']
    sorted_images = sorted(
        images, 
        key = lambda x: x['CreationDate'], 
        reverse = True
    )

    return sorted_images
    
def get_launch_configs(as_client):
    response = as_client.describe_launch_configurations()
    
    configs = []
    
    if response is not None:
        for config in response['LaunchConfigurations']:
            launch_configuration = config['LaunchConfigurationName']
            
            if ('example.com' in launch_configuration):
                configs.append(launch_configuration)
            
    return configs
    
def create_launch_config(as_client, image_id):
    launch_configuration_name = 'example.com_' + dt.now().strftime('%Y%m%d%H%M%S')

    response = as_client.create_launch_configuration(
        LaunchConfigurationName = launch_configuration_name,
        ImageId = image_id,
        KeyName = 'your keypair',
        SecurityGroups = ['your security group name'],
        InstanceType = 't2.micro',
        BlockDeviceMappings=[
            {
                'DeviceName': '/dev/sda1',
                'Ebs': {
                    'VolumeSize': 8,
                    'VolumeType': 'gp2',
                    'DeleteOnTermination': True
                }
            }
        ]
    )
    
    return launch_configuration_name

def delete_old_launc_config(as_client, configs):
    for config in configs:
        response = as_client.delete_launch_configuration(
            LaunchConfigurationName = config
        )
    
    return True
    
def update_auto_scaling_group(as_client, launch_configuration_name):
    as_client.update_auto_scaling_group(
        AutoScalingGroupName = 'your auto scaling group name',
        LaunchConfigurationName = launch_configuration_name
    )
    
    return True

GitHubにアップしました!
update_launch_config/update_launch_config.py at master · mojyamojya/update_launch_config

ただ、このlambdaファンクションをlambda_basic_executionのroleで実行してもエラーになってしまいます。

An error occurred (AccessDenied) when calling the DescribeLaunchConfigurations operation: User: arn:aws:sts::xxxxxxxxxxxx:assumed-role/lambda_basic_execution/update_launch_config is not authorized to perform: autoscaling:DescribeLaunchConfigurations: ClientError

以下のようなPolicyを追記し、autoscalingのリソースにアクセスできるIAM roleを作成しました。

        {
            "Effect": "Allow",
            "Action": [
                "autoscaling:*"
            ],
            "Resource": [
                "*"
            ]
        }

細かいところで雑な部分はありますが、今度は正常終了しました。

何かの参考になれば幸いです。
よければ参考にしてみてください!