前提: 起動設定よりも起動テンプレートを使った方が良いです。
一部の古いシステム等、起動設定を使っているまま起動テンプレートに移行ができていないものが、あったりなかったりすると思います。
Auto Scalingグループに紐付いた起動設定の更新作業って、デプロイ後とかに毎回やっていたのですが、手作業でやると地味に面倒です。
AMI取得
↓
起動設定を作成(既存のものからコピーしてName等変更)
↓
Auto Scalingグループの設定更新
この3ステップめんどくないですか?めんどいですよね?自動化しませんか?します(しました)。
※AWSの既存サービスを組み合わせて、本記事の内容が全て実装できるかもしれません。無知を晒しているな~と思ったらコメント等お願いいたします。救われます。
※同様の流れで起動テンプレートを自動更新する記事も書きました。
Lambda関数による実装
今回はAuto Scalingグループについて起動設定の更新を行いたいので、AMIの取得元はAuto ScalingグループのNameで指定することにしています。
import boto3
from botocore.exceptions import ClientError
import datetime as dt
timestamp = (dt.datetime.now() + dt.timedelta(hours=9)).strftime('%Y%m%d-%H%M')
ec2 = boto3.client('ec2')
autoscaling = boto3.client('autoscaling')
def get_instance(name_tag_value):
try:
filter_key = 'tag:aws:autoscaling:groupName'
response = ec2.describe_instances(
Filters=[{'Name':filter_key,'Values':[name_tag_value]}])
return response['Reservations'][0]['Instances'][0]
except ClientError as e:
print('#ClientError!! at get_instance()')
raise e
def make_image_name(instance_name):
# Make image name like: hogehoge-20210611-1530
try:
return instance_name + '-' + timestamp
except ClientError as e:
print('#ClientError!! at make_image_name()')
raise e
def create_ami(image_name, instance_id, description):
# Create image NO-reboot.
try:
image = ec2.create_image(
InstanceId=instance_id,
# DryRun=True, # For test.
Name=image_name,
Description=description,
NoReboot=True,
)
return image['ImageId']
except ClientError as e:
print('#ClientError!! at create_ami()')
raise e
def get_launch_conf_name(auto_scaling_group_name):
# Get Launch Configuration Name of Auto Scaling Group.
try:
response = autoscaling.describe_auto_scaling_groups(
AutoScalingGroupNames=[auto_scaling_group_name])
return response['AutoScalingGroups'][0]['LaunchConfigurationName']
except ClientError as e:
print('#ClientError!! at get_launch_conf_name()')
raise e
def get_launch_conf(launch_conf_name):
# Get Launch Configuration of Auto Scaling Group.
try:
response = autoscaling.describe_launch_configurations(
LaunchConfigurationNames=[launch_conf_name])
return response['LaunchConfigurations'][0]
except ClientError as e:
print('#ClientError!! at get_launch_conf()')
raise e
def create_launch_conf(old_launch_conf, launch_conf_name, ami_id):
# Create NEW Launch Configuration.
# Get target snapshots.
try:
response = ec2.describe_snapshots(
Filters=[
{
'Name': 'description',
'Values': ['Created by CreateImage(*) for ' + ami_id + '*',]
}
]
)
except ClientError as e:
print(e.response['Error']['Code'])
print(e.response['Error']['Message'])
raise e
snapshot_id = response['Snapshots'][0]['SnapshotId']
print(snapshot_id)
# Create Launch Configuration.
old_launch_conf['BlockDeviceMappings'][0]['Ebs']['SnapshotId'] = snapshot_id
try:
autoscaling.create_launch_configuration(
LaunchConfigurationName=launch_conf_name,
ImageId=ami_id,
KeyName=old_launch_conf['KeyName'],
SecurityGroups=old_launch_conf['SecurityGroups'],
UserData="#!/bin/bash\nrm -f /var/tmp/aws-mon/instance-id",
InstanceType=old_launch_conf['InstanceType'],
BlockDeviceMappings=old_launch_conf['BlockDeviceMappings'],
InstanceMonitoring=old_launch_conf['InstanceMonitoring'],
IamInstanceProfile=old_launch_conf['IamInstanceProfile'],
EbsOptimized=old_launch_conf['EbsOptimized'])
except ClientError as e:
print('#ClientError!! at create_launch_conf()')
raise e
return
def update_auto_scaling_group(auto_scaling_group_name, new_launch_conf_name):
# Update Launch Configuration of Auto Scaling Group.
autoscaling.update_auto_scaling_group(
AutoScalingGroupName=auto_scaling_group_name,
LaunchConfigurationName=new_launch_conf_name)
return
def main_process(auto_scaling_group_name, description=''):
try:
# Get Original(Old) Launcn Configuration.
old_launch_conf_name = get_launch_conf_name(auto_scaling_group_name)
print(old_launch_conf_name)
old_launch_conf = get_launch_conf(old_launch_conf_name)
# print(old_launch_conf)
# Make AMI name and Launcn Configuration name.
image_name = make_image_name(auto_scaling_group_name)
launch_conf_name = image_name
print(image_name)
# Create AMI from target instance.
target_instance = get_instance(auto_scaling_group_name)
instance_id = target_instance['InstanceId']
print(instance_id)
if not description:
description = f'Lambda create. id:{instance_id}'
ami_id = create_ami(image_name, instance_id, description)
print(ami_id)
# Create Launch Configuration.
create_launch_conf(old_launch_conf, launch_conf_name, ami_id)
# Update Auto Scaling Group.
update_auto_scaling_group(auto_scaling_group_name, launch_conf_name)
except ClientError as e:
print(e)
def lambda_handler(event, context):
targets = event['targets']
for target in targets:
auto_scaling_group_name = target['auto_scaling_group_name']
description = target['description']
print(auto_scaling_group_name)
main_process(auto_scaling_group_name, description)
return
大まかな処理の流れは下記のとおりです。
- 現在の(古い)起動設定を取得(現在のものから設定を引き継ぐため)
- Auto Scalingグループから現在の(古い)起動設定のNameを取得
- 現在の(古い)起動設定を取得
- 新しく作成するAMIのNameを決定
- 新しい起動設定のNameを決定(便宜上、AMIのNameと同名とする)
- 新しい起動設定で使うAMIを作成
- Auto Scalingグループから適当なインスタンス1台を取得してインスタンスIDを取得
- 対象のインスタンスから、AMIを作成
- 新しい起動設定を作成
- 新しい起動設定をAuto Scalingグループに割り当て(設定の更新)
Lambda関数には、下記のような権限を付与しておきます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:PassRole",
"ec2:DescribeSnapshots",
"ec2:DescribeInstances",
"ec2:CreateImage",
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:UpdateAutoScalingGroup",
"autoscaling:DescribeLaunchConfigurations",
"autoscaling:CreateLaunchConfiguration"
],
"Resource": "*"
}
]
}
手動で実行する際には、テストイベントに下記のようなデータを入れてテストを走らせます。(本来の用途とは異なるけどお手軽なのでやりがち)
※descriptionを空にした場合、「Lambda create. id:{instance_id}」という定型文が入るようにしています。
{
"targets": [
{
"auto_scaling_group_name": "example-group-name",
"description": ""
},
{
"auto_scaling_group_name": "awesome-group-name",
"description": "The bug was gone with a great fix."
}
]
}
これにより、デプロイ後に行う作業が
Auto Scalingグループの名前と説明文を書き換える
↓
テストを走らせる
だけになりました(エラーが無ければ)。
手動で3ステップの作業を行う手間が省け、作業ミスが入り込む余地が削減でき、詳細なノウハウが無くても作業が実施可能になってしまいました。すごい。
cronによる定期更新
デプロイ後に手動実行するのではなくて、定期的にバックアップを取りつつ、起動設定の更新をしてAuto Scalingに対応したいというニーズもあるかもしれません。WordPressのサイトをEC2で動かしているとかね。
Lambdaにはcronで定期実行する仕組みが組み込まれています。天才。
Lambda > 関数 > トリガーを追加 > トリガーの設定 でEventBridgeを使います。
プログラムの方は現在テストイベントから引数(対象のAuto Scalingグループ名)を受け取っていますが、こちらをコード内で決め打ちにしてしまえばOKです。非常に簡単。
targets = event['targets']
↓
targets = [
{
"auto_scaling_group_name": "example-group-name",
"description": "Daily buckup."
}
]
CodePipelineから呼び出す場合
デプロイ後にPipelineから自動で呼び出してやって欲しいことの方が多そうですね。こちらも簡単ですが、コードの作りが多少変わります。
だいぶ大雑把ですが……下記のような変更があります。
- main_process()をtry-exceptでくくり、Pipelineに成功/失敗を返すような処理にします。
- 下記の書き方の場合、main_process()内のtry-exceptでは例外発生時にエラーをraiseし、lambda_handler()で受け取れるようにする必要があります。
- Pipelineにレスポンスを返すため、PipelineのJob IDなどの情報もあわせて受け取っています。
- descriptionには、例えばデプロイしたGitHubのブランチ名などを含めると良いかもしれません。
def lambda_handler(event, context):
codepipeline = boto3.client('codepipeline')
try:
# Get CodePipeline user params.
job_data = event['CodePipeline.job']['data']
params = get_user_params(job_data)
auto_scaling_group_name = params['auto_scaling_group_name']
github_branch = params['branch']
print(auto_scaling_group_name)
print(github_branch)
description = f'Lambda create. branch:{github_branch}'
# update Launch Configuration and Auto Scaling Group.
main_process(auto_scaling_group_name, description)
# Return Success to CodePipeline.
codepipeline.put_job_success_result(
jobId=event['CodePipeline.job']['id'])
except ClientError as e:
print(e)
# Return Failure to CodePipeline.
codepipeline.put_job_failure_result(
jobId=event['CodePipeline.job']['id'],
failureDetails={
'type': 'JobFailed',
'message': str(e)
}
)
Pipelineから受け取るパラメータを処理する get_user_params() は下記のような実装です。
PipelineのLambda関数を呼び出すステージで、下記のように値を指定します。
UserParameters: {"branch":"master","auto_scaling_group_name":"example-group-name"}
import json
def get_user_params(job_data):
try:
user_parameters = job_data['actionConfiguration']['configuration']['UserParameters']
decoded_parameters = json.loads(user_parameters)
except Exception as e:
raise Exception('UserParameters could not be decoded as JSON')
if 'branch' not in decoded_parameters:
raise Exception('UserParameters JSON must include the "branch"')
if 'instance_name' not in decoded_parameters:
raise Exception('UserParameters JSON must include the "instance_name"')
return decoded_parameters
Lambda関数には、CodePipelineに結果を返す権限も付与する必要があります。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"codepipeline:PutJobSuccessResult",
"codepipeline:PutJobFailureResult"
],
"Resource": "*"
}
]
}
これで、Pipelineの進行にあわせて「デプロイ後に自動で”AMI取得→起動設定作成→Auto Scalingグループ更新”」ができるようになりました。