2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

EC2 に Auto Scaling の設定 と Cl/CD (CodePipeline + CodeDeploy + LambdaによるAMIの自動更新) を導入 ②

Posted at

はじめに

こちらの続きです。

CodePipelineを作成

CodePipelineを作成しましょう。

  • パイプライン名を記入し次へ
    スクリーンショット 2022-08-26 18.48.43.png
    • GitHub(バージョン2)を選択し、GitHubに接続するをクリックします。
      スクリーンショット 2022-08-26 18.53.44.png
    • 接続名は、適当に入力します。
      スクリーンショット 2022-08-26 18.55.04.png
    • 新しいアプリをインストールをクリックすると、Githubに遷移し、CodePipelineと連携を許可するリポジトリを選択します。
      • 選択後、Saveをクリックすると、前の画面に戻り、接続をクリックすると、連携完了です。
        スクリーンショット 2022-08-26 18.56.30.png
        スクリーンショット 2022-08-26 18.58.04.png
    • あとは、リポジトリ名とブランチ名を入力し、次に進みます。
      スクリーンショット 2022-08-26 19.00.49.png
  • ビルドステージはスキップします。
    スクリーンショット 2022-08-26 19.01.49.png
  • デプロイステージは、CodeDeployを選択します。そして、パイプラインを作成します。
    スクリーンショット 2022-08-26 19.02.37.png

CodePipelineを作成すると、CodeDeployが実行されます。今回はエラーが起きました。

Codepipelineでデプロイエラー

スクリーンショット 2022-08-26 22.37.30.png

CodeDeployのコンソール上でエラーログを確認する

CodeDeployのエラーログは、以下のイベントview eventsをクリックし、Event の Error codeをクリックすると、確認できます。
スクリーンショット 2022-08-26 22.49.37.png
スクリーンショット 2022-08-26 22.53.28.png

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

スクリーンショット 2022-08-26 22.42.09.png

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を選択します。
スクリーンショット 2022-08-26 18.38.09.png

タイムアウトは、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を追加

  • ステージを追加するをクリックし、ステージ名を入力します。
    スクリーンショット 2022-08-27 21.16.49.png
  • アクション名、アクションプロバイダーはLambdaを選択し、関数は先程作成したLambdaを選択します。
  • ユーザーパラメーターは、{"branch":"main","instance_name":"nginx"}を入れます。
    • branchは、Githubのブランチ名、instance_nameは、Auto Scaling名を入れます。
      スクリーンショット 2022-08-27 21.20.10.png
  • Auto Scaling名であるinstance_nameは、EC2のタグのうちaws:autoscaling:groupNameValueの値と一致するはずです。
    スクリーンショット 2022-08-27 21.30.49.png
  • ステージを追加後、変更をリリースするをクリックし、全て成功することを確認します。
    スクリーンショット 2022-08-27 21.39.56.png

Amiが作成され、起動テンプレートが反映されているか確認

Amiを確認すると、ami-05142d4df822c97feというIDが作成されていることが分かりました。
スクリーンショット 2022-08-27 21.41.40.png

起動テンプレートのAMIをクリックすると、起動テンプレートのデフォルトのバージョンのAMIは、ami-05142d4df822c97feになっておりました。
スクリーンショット 2022-08-27 21.45.10.png
つまり、Lambdaによって、デプロイ後の最新のソースのAMIを作成し、起動テンプレートに最新のAMIを反映させ、バージョンを更新させたことが確認できました。

GitHubのソースを修正し、サーバーが反映するか確認

GitHubのソースであるindex.htmlを以下のように修正し、サーバーに反映するか確認します。

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <p>CodePipeline Finished!</p>
  </body>
</html>

反映されました。サーバー負荷をあげて、スケールアウトしても反映済みのサーバーが起動されます。
スクリーンショット 2022-08-27 21.57.09.png

参考記事

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?