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

# はじめに
こちらの続きです。

https://qiita.com/hirai-11/items/127e812dc7bc570c4398

# CodePipelineを作成
CodePipelineを作成しましょう。

- パイプライン名を記入し次へ
![スクリーンショット 2022-08-26 18.48.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/6abb568c-960b-834e-44ac-4672bcb718c1.png)
    - GitHub(バージョン2)を選択し、`GitHubに接続する`をクリックします。
    ![スクリーンショット 2022-08-26 18.53.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/7791a6c9-9a20-824e-ae7e-1350bbf77462.png)
    - 接続名は、適当に入力します。
    ![スクリーンショット 2022-08-26 18.55.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/a476fac0-c39e-30ad-04f9-c4e3ae11d9c8.png)
    - 新しいアプリをインストールをクリックすると、Githubに遷移し、CodePipelineと連携を許可するリポジトリを選択します。
        - 選択後、`Save`をクリックすると、前の画面に戻り、`接続`をクリックすると、連携完了です。
    ![スクリーンショット 2022-08-26 18.56.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/3e851ff9-fc03-b43c-dbaa-2f8e36c037d7.png)
    ![スクリーンショット 2022-08-26 18.58.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/f9bc11d1-2028-e117-96f3-8f4927591d4b.png)
    - あとは、リポジトリ名とブランチ名を入力し、次に進みます。
![スクリーンショット 2022-08-26 19.00.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/8758e971-5543-e565-dfe4-6af2e8b1317e.png)
- ビルドステージはスキップします。
![スクリーンショット 2022-08-26 19.01.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/143c606e-3fbf-0c67-0d03-084dca54d9c6.png)
- デプロイステージは、CodeDeployを選択します。そして、パイプラインを作成します。
![スクリーンショット 2022-08-26 19.02.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/61c2d467-5689-3839-f84e-9e23b767c9d0.png)

CodePipelineを作成すると、CodeDeployが実行されます。今回はエラーが起きました。
## Codepipelineでデプロイエラー
![スクリーンショット 2022-08-26 22.37.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/94a6b668-5f3c-9318-0b81-a4fdfcd1449b.png)

### CodeDeployのコンソール上でエラーログを確認する
CodeDeployのエラーログは、以下の`イベント`の`view events`をクリックし、`Event の Error code`をクリックすると、確認できます。
![スクリーンショット 2022-08-26 22.49.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/0744e475-99fc-20c1-27ab-38280036da76.png)
![スクリーンショット 2022-08-26 22.53.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/e72f42f0-0533-e05b-33df-3a03c0ed35fd.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/25addf9b-aec0-193b-1d1d-49849551b535.png)

### EC2内のCodeDeploy Agentのログからエラーログを確認する
エラーログは、EC2にSSH接続し、以下のコマンドでCodeDeploy Agentのlogでも確認できます。
```terminal
$ tail -f -n 200  /var/log/aws/codedeploy-agent/codedeploy-agent.log
```
### エラー原因
原因として、AutoScalingを節約の目的で、EC2の起動サイズを0にしておりました。
ただ、その状態で、サイズを1にしてもCodeDeployが強制的に起動し、上記のエラーを繰り返しました。

以下の記事が参考になりますが、対処方法としてCodeDeployをデプロイグループ一旦削除した状態で、AutoScalingのサーバーサイズを1にして、起動させました。
その後、CodeDeployをデプロイグループを再作成し、Codepipelineと連携し変更のリリースをすると成功しました。

https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/troubleshooting-auto-scaling.html#troubleshooting-auto-scaling-hooks


## CodePipelineのSlackへの通知設定
CodeDeployなどの成功・失敗は、Slackなどに通知することができます。
以前、投稿した記事通りに進めると、通知設定できます。

https://qiita.com/hirai-11/items/9bc4bdda82d8bbacc94a

# AMIを作成し、起動テンプレートにAMIを反映するLambdaを作成
下記記事を参考に作成しました。

https://oc-technote.com/aws/ec2-auto-scaling%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97-%E8%B5%B7%E5%8B%95%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%81%AE%E8%87%AA%E5%8B%95%E6%9B%B4%E6%96%B0/

下記の通り、適当な名前をつけ、Python3.9を選択します。
![スクリーンショット 2022-08-26 18.38.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/d5bb00d6-4989-557b-6234-c402844d2deb.png)

タイムアウトは、３秒から1分に修正しましょう。

## LambdaのIAMロール修正
作成後のLambdaのIAMロールはこちらです。

```json:作成時
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": （略）
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                （略）
            ]
        }
    ]
}
```

修正したIAMロールになります。
```json:修正後
{
    "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を追加します。

```python:
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/28cba59d-e2d8-6e80-5772-51db70079725.png)
- アクション名、アクションプロバイダーはLambdaを選択し、関数は先程作成したLambdaを選択します。
- ユーザーパラメーターは、`{"branch":"main","instance_name":"nginx"}`を入れます。
    - `branch`は、Githubのブランチ名、`instance_name`は、Auto Scaling名を入れます。
![スクリーンショット 2022-08-27 21.20.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/2038563e-3810-9bc8-a2cd-243333873f88.png)
- Auto Scaling名である`instance_name`は、EC2のタグのうち`aws:autoscaling:groupName`の`Value`の値と一致するはずです。
![スクリーンショット 2022-08-27 21.30.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/029a804b-ac04-ac46-11fa-04c9fd56b981.png)
- ステージを追加後、`変更をリリースする`をクリックし、全て成功することを確認します。
![スクリーンショット 2022-08-27 21.39.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/a961e248-19da-4acb-e49e-b258cbbdf243.png)

## Amiが作成され、起動テンプレートが反映されているか確認
Amiを確認すると、`ami-05142d4df822c97fe`というIDが作成されていることが分かりました。
![スクリーンショット 2022-08-27 21.41.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/df1567fa-2a7c-56ff-31be-46487db903fd.png)

起動テンプレートのAMIをクリックすると、起動テンプレートのデフォルトのバージョンのAMIは、`ami-05142d4df822c97fe`になっておりました。
![スクリーンショット 2022-08-27 21.45.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/1e06ac11-9f84-80c5-4131-1aa99ca4fb2a.png)
つまり、Lambdaによって、デプロイ後の最新のソースのAMIを作成し、起動テンプレートに最新のAMIを反映させ、バージョンを更新させたことが確認できました。

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

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/a671b73b-4803-3387-3be2-89746bf83643.png)




# 参考記事



https://docs.aws.amazon.com/ja_jp/autoscaling/ec2/userguide/ec2-auto-scaling-tagging.html

https://oc-technote.com/aws/ec2-auto-scaling%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97-%E8%B5%B7%E5%8B%95%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%81%AE%E8%87%AA%E5%8B%95%E6%9B%B4%E6%96%B0/