起動テンプレート、便利ですよね。Auto Scalingグループで参照するものを「Latest」にしておけば、デプロイのたびにAuto Scalingグループの設定を変更する必要がありません。
起動テンプレートの更新作業をデプロイ後とかに毎回やっていたのですが、
AMI取得
↓
起動テンプレートを更新(新しいバージョンを作成)
たったこれだけの作業でも、手作業でやると地味に面倒だったり、作業ミスや作業漏れが発生しがちで残念な感じです。
起動設定の更新同様、自動化してしまいます。
※レガシーですが、レガシーゆえに最近の情報が少なかったので、起動設定の自動更新についても記事を書きました。
Lambda関数による実装
起動テンプレートを使っている場合、Auto Scalingグループの更新は不要なため、AMI取得元のインスタンスのNameを指定して、そこから必要な情報を取得していきます。
(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')
def get_instance(name_tag_value):
try:
filter_key = 'tag:aws:autoscaling:groupName'
reservations = ec2.describe_instances(
Filters=[{'Name':filter_key,'Values':[name_tag_value]}])
return reservations['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: test-ec2-instance-20210614-1300
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 update_launch_template(target_launch_template_id, ami_id, description):
# Update target launch template.
try:
response = ec2.create_launch_template_version(
LaunchTemplateId=target_launch_template_id,
VersionDescription=description,
SourceVersion="$Latest",
LaunchTemplateData={
"ImageId": ami_id
}
)
print(f"New launch template created with AMI {ami_id}")
except ClientError as e:
print('#ClientError!! at make_image_name()')
raise e
def set_launch_template_default_version(target_launch_template_id):
try:
response = ec2.modify_launch_template(
LaunchTemplateId=target_launch_template_id,
DefaultVersion="$Latest"
)
print("Default launch template set to $Latest.")
except ClientError as e:
print('#ClientError!! at set_launch_template_default_version()')
raise e
def main_process(name_tag_value, description=''):
try:
# Get target instance id.
target_instance = get_instance(name_tag_value)
instance_id = target_instance['InstanceId']
print(instance_id)
# Get target launch template id.
for tag in target_instance['Tags']:
if tag['Key'] == 'aws:ec2launchtemplate:id':
target_launch_template_id = tag['Value']
break
print(target_launch_template_id)
# Make AMI name.
image_name = make_image_name(name_tag_value)
print(image_name)
# Create AMI from target instance.
if not description:
description = f'Lambda create. id:{instance_id}'
ami_id = create_ami(image_name, instance_id, description)
print(ami_id)
# Update Launch Template
update_launch_template(target_launch_template_id, ami_id, description)
# Update Launch Template default version.
set_launch_template_default_version(target_launch_template_id)
except ClientError as e:
print(e)
def lambda_handler(event, context):
target_instances = event['target_instances']
for target_instance in target_instances:
name_tag_value = target_instance['tag_name']
description = target_instance['description']
print(name_tag_value)
main_process(name_tag_value, description)
return
大まかな処理の流れは下記のとおりです。
- 指定のインスタンスNameを持つインスタンスを適当に1つ取得
- 対象のインスタンスに紐付いた起動テンプレートのIDを取得
- 対象のインスタンスからAMIを作成
- 取得したAMIを元に起動テンプレートを更新(新しいバージョンを作成)
- ソースとして$Latestを指定
- ImageIdとして今取得したAMIを指定
- 作成した起動テンプレートのバージョンを、デフォルトに指定
また、今回のコードには含めていないものの、古い起動テンプレートを削除する処理も可能です。
例えば、下記のようなdelete_previous_version()を作成し、set_launch_template_default_version()から呼び出すようにしてみると、起動テンプレートの更新後に、3つ前のバージョンが自動で削除されるようになります。
def delete_previous_version(target_launch_template_id, previous_version):
try:
response = ec2.delete_launch_template_versions(
LaunchTemplateId=target_launch_template_id,
Versions=[
previous_version,
]
)
print(f"Old launch template {previous_version} deleted.")
except ClientError as e:
print('#ClientError!! at delete_previous_version()')
print(e)
def set_launch_template_default_version(target_launch_template_id):
try:
response = ec2.modify_launch_template(
LaunchTemplateId=target_launch_template_id,
DefaultVersion="$Latest"
)
print("Default launch template set to $Latest.")
previous_version = str(
int(response["LaunchTemplate"]["LatestVersionNumber"]) - 3)
print(previous_version)
delete_previous_version(target_launch_template_id, previous_version)
except ClientError as e:
print('#ClientError!! at set_launch_template_default_version()')
raise e
Lambda関数には、通常のログ作成関連の権限の他に、下記のような権限を与えておきます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:CreateImage",
"ec2:DescribeInstances"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "ec2:DescribeLaunchTemplateVersions",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ec2:ModifyLaunchTemplate",
"ec2:DeleteLaunchTemplateVersions",
"ec2:CreateLaunchTemplateVersion"
],
"Resource": [
"*"
]
}
]
}
起動設定のときと同様、手動で実行する際には、テストイベントに下記のようなデータを入れてテストを走らせます。(いつもの)
※descriptionを空にした場合、「Lambda create. id:{instance_id}」という定型文が入るようにしています。
{
"target_instances": [
{
"tag_name": "test-ec2-instance",
"description": "hogehoge"
}
]
}
これにより、起動テンプレートの更新作業が
対象とするEC2インスタンスのNameとdescriptionを書き換える
↓
テストを走らせる
だけになりました(エラーが無ければ)。
だいぶお手軽ですね。
cronによる定期更新
cronによる定期更新の設定方法は、「EC2 Auto Scalingグループ 起動設定の自動更新」と同様です。「cronによる定期更新」の項を参照してください。
CodePipelineから呼び出す場合
CodePipelineから呼び出す場合の設定方法についても、やることは「EC2 Auto Scalingグループ 起動設定の自動更新」の「CodePipelineから呼び出す場合」と同様です。
Pipelineから与えられる引数を処理するために、下記の関数を追記します。
PipelineのLambda関数を呼び出すステージで、下記のように値を指定します。
UserParameters: {"branch":"master","instance_name":"test-ec2-instance"}
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
メインの処理では、CodePipelineにレスポンスを返すようにします。
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)
name_tag_value = params['instance_name']
github_branch = params['branch']
print(name_tag_value)
print(github_branch)
# Get target instance id.
target_instance = get_instance(name_tag_value)
instance_id = target_instance['InstanceId']
print(instance_id)
# Get target launch template id.
for tag in target_instance['Tags']:
if tag['Key'] == 'aws:ec2launchtemplate:id':
target_launch_template_id = tag['Value']
break
print(target_launch_template_id)
# Make AMI name.
image_name = make_image_name(name_tag_value)
print(image_name)
# Create AMI from target instance.
description = f'Lambda create. branch:{github_branch} id:{instance_id}'
ami_id = create_ami(image_name, instance_id, description)
print(ami_id)
# Update Launch Template
update_launch_template(target_launch_template_id, ami_id, description)
# Update Launch Template default version.
set_launch_template_default_version(target_launch_template_id)
# 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)
}
)
Lambda関数の権限は下記のとおりです。logs関連のResourceは伏せていますが、関数作成時に自動作成される箇所なので問題ないはずです。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": (略)
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
(略)
]
},
{
"Effect": "Allow",
"Action": [
"ec2:CreateImage",
"ec2:DescribeInstances"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"codepipeline:PutJobSuccessResult",
"codepipeline:PutJobFailureResult"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "ec2:DescribeLaunchTemplateVersions",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ec2:ModifyLaunchTemplate",
"ec2:DeleteLaunchTemplateVersions",
"ec2:CreateLaunchTemplateVersion"
],
"Resource": [
"*"
]
}
]
}
これで、Pipelineの進行にあわせて「デプロイ後に自動で”AMI取得→起動テンプレート更新”」ができるようになりました。
はじめまして。参考にさせてもらっています。
一番上のコードをベタでコピペして試しているのですが、
関数の引数のエラーが発生します。環境変数などで引数を指定したりするのでしょうか?
またLambdaのアクセス権はLambdaのロールにインラインで追加しても問題ないでしょうか?
Lambdaビギナー様
コメントいただきありがとうございます。
ご質問いただいた内容に回答いたします。
> 関数の引数のエラーが発生します。環境変数などで引数を指定したりするのでしょうか?
環境変数は設定しておりません。該当のLambda関数を動かすとき(Lambda関数ハンドラー)に以下のようなJSONデータが必要です。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-handler.html
サンプルJSON
> またLambdaのアクセス権はLambdaのロールにインラインで追加しても問題ないでしょうか?
こちらに関しては、Lambdaビギナー様が運用されているAWSアカウントの管理者に確認したほうが適切かと存じます。
基本的にはインラインポリシーで管理するよりも、カスタマー管理ポリシーで管理したほうが良いとされています。
詳細については以下を参照ください。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access_policies_managed-vs-inline.html#choosing-managed-or-inline
NaoiSota様
早速のご返信ありがとうございます。
“tag_name”: “EC2インスタンスに紐づけられたタグ名1”,というのは
インスタンスのNameタグの値ということでよろしいでしょうか?
JSONで存在するNameの値を入れても「インデックスの範囲外」となります。
main_process(name_tag_value, description)を print(name_tag_value,description)
に置き換えると ホスト名 TEST は表示されるので、
JSONデータの引数は取れていると思うのですが、他のAWSリソース(ASG、AMI等)のタグや設定漏れでしょうか?
以下エラーです。
Response
{
“errorMessage”: “list index out of range”,
“errorType”: “IndexError”,
“requestId”: “dummy11111”,
“stackTrace”: [
” File \”/var/task/lambda_function.py\”, line 118, in lambda_handler\n main_process(name_tag_value, description)\n”,
” File \”/var/task/lambda_function.py\”, line 79, in main_process\n target_instance = get_instance(name_tag_value)\n”,
” File \”/var/task/lambda_function.py\”, line 16, in get_instance\n return reservations[‘Reservations’][0][‘Instances’][0]\n”
]
}
Lambdaビギナー様
ご確認いただきありがとうございます。
ご質問いただいた内容に回答いたします。
> “tag_name”: “EC2インスタンスに紐づけられたタグ名1”,というのは
> インスタンスのNameタグの値ということでよろしいでしょうか?
正確には、Auto Scaling グループ名になります。
以下リンクの「EC2 インスタンスのタグ付けライフサイクル」の項目に詳細がございますので、ご確認ください。
https://docs.aws.amazon.com/ja_jp/autoscaling/ec2/userguide/autoscaling-tagging.html
また、動かない場合やご不明な点がございましたら、お手数ですが、ご連絡いただければ幸いです。