はじめに
こちらの続きです。
CodePipelineを作成
CodePipelineを作成しましょう。
CodePipelineを作成すると、CodeDeployが実行されます。今回はエラーが起きました。
Codepipelineでデプロイエラー
CodeDeployのコンソール上でエラーログを確認する
CodeDeployのエラーログは、以下のイベント
のview events
をクリックし、Event の Error code
をクリックすると、確認できます。
CodeDeployには、以下のエラーログを表示されました。
The deployment failed because a specified file already exists at this location:/usr/share/nginx/html
AutoScalingのアクティビティ履歴でエラーログを確認する
AutoScalingのアクティビティ履歴には、以下のエラーログを表示されていました。
Launching a new EC2 instance: i-0f46554cc9d19bb2c.
Status Reason: Instance failed to complete user's Lifecycle Action:
Lifecycle Action with token 35f50a4b-4470-4396-93a2-eb71c3578280 was abandoned:
Lifecycle Action Completed with ABANDON Result
EC2内のCodeDeploy Agentのログからエラーログを確認する
エラーログは、EC2にSSH接続し、以下のコマンドでCodeDeploy Agentのlogでも確認できます。
$ tail -f -n 200 /var/log/aws/codedeploy-agent/codedeploy-agent.log
エラー原因
原因として、AutoScalingを節約の目的で、EC2の起動サイズを0にしておりました。
ただ、その状態で、サイズを1にしてもCodeDeployが強制的に起動し、上記のエラーを繰り返しました。
以下の記事が参考になりますが、対処方法としてCodeDeployをデプロイグループ一旦削除した状態で、AutoScalingのサーバーサイズを1にして、起動させました。
その後、CodeDeployをデプロイグループを再作成し、Codepipelineと連携し変更のリリースをすると成功しました。
CodePipelineのSlackへの通知設定
CodeDeployなどの成功・失敗は、Slackなどに通知することができます。
以前、投稿した記事通りに進めると、通知設定できます。
AMIを作成し、起動テンプレートにAMIを反映するLambdaを作成
下記記事を参考に作成しました。
下記の通り、適当な名前をつけ、Python3.9を選択します。
タイムアウトは、3秒から1分に修正しましょう。
LambdaのIAMロール修正
作成後のLambdaのIAMロールはこちらです。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": (略)
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
(略)
]
}
]
}
修正したIAMロールになります。
{
"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": [
"*"
]
}
]
}
Lambdaのコード
テストはしません。Pipeline作成後、CodePipelineのステージにLambdaを追加します。
import boto3
from botocore.exceptions import ClientError
import datetime as dt
import json
from decimal import Decimal
timestamp = (dt.datetime.now() + dt.timedelta(hours=9)).strftime('%Y%m%d-%H%M')
ec2 = boto3.client('ec2')
def decimal_to_int(obj):
if isinstance(obj, Decimal):
return int(obj)
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 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
def lambda_handler(event, context):
print("Received event:" + json.dumps(event, default=decimal_to_int, ensure_ascii=False))
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)
}
)
CodePipelineにLambdaを追加
-
ステージを追加する
をクリックし、ステージ名を入力します。
- アクション名、アクションプロバイダーはLambdaを選択し、関数は先程作成したLambdaを選択します。
- ユーザーパラメーターは、
{"branch":"main","instance_name":"nginx"}
を入れます。 - Auto Scaling名である
instance_name
は、EC2のタグのうちaws:autoscaling:groupName
のValue
の値と一致するはずです。
- ステージを追加後、
変更をリリースする
をクリックし、全て成功することを確認します。
Amiが作成され、起動テンプレートが反映されているか確認
Amiを確認すると、ami-05142d4df822c97fe
というIDが作成されていることが分かりました。
起動テンプレートのAMIをクリックすると、起動テンプレートのデフォルトのバージョンのAMIは、ami-05142d4df822c97fe
になっておりました。
つまり、Lambdaによって、デプロイ後の最新のソースのAMIを作成し、起動テンプレートに最新のAMIを反映させ、バージョンを更新させたことが確認できました。
GitHubのソースを修正し、サーバーが反映するか確認
GitHubのソースであるindex.htmlを以下のように修正し、サーバーに反映するか確認します。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<p>CodePipeline Finished!</p>
</body>
</html>
反映されました。サーバー負荷をあげて、スケールアウトしても反映済みのサーバーが起動されます。
参考記事