アプリ用EC2周りの構築でやったこと（２）　AutoScalingGroupの起動テンプレートVer設定

[「アプリ用EC2の構築でやったこと（１）　backup先のGolden image作成」](https://qiita.com/nsaito9628/items/bcb17830d4f547816896)の続きになります

前回同様にVPCまわり、セキュリティ、EC2、ユーザーアプリの話はありません

## 記事の内容
今回の記事では以下二つのLambda関数を説明します
* CloudformationでdeployしたAutoScalingGroupの起動テンプレートバージョンをLatestに変更する
* DLMの日次Backupで本番用AMIが更新されたら起動テンプレートのAMI Idも更新する

下図の赤枠部分に相当します

前回の続きということで図中ではBackup先リージョンでの操作を説明してますが東京にも同じ仕組みが実装されています

samでbuildとdeployをしていますが記事中では説明しません

![TP ver update.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/1048054d-f091-2c0e-c8ce-c7d14ff79f0a.png)


## 作業環境

 * Windows10 / WSL2(Ubuntu20.04)  
 * Python 3.8.10
 * aws-cli/1.22.92 Python/3.8.10 Linux/5.13.0-1021-aws botocore/1.24.37  
 * SAM CLI, version 1.46.0  

## AutoScalingGroupの起動テンプレートバージョンをLatestに変更

今回取り組み前の自分なら「何言ってるか全然わからない」キャプションタイトルです、いや普通にすればイイでしょ的な

何故このような必要があるかと言うと、CloudformationでAutoScalingGroupをdeployする時に起動テンプレートのバージョンをLatestに指定できないからです(少なくとも自分が確認した範囲で)

[AWS::AutoScaling::AutoScalingGroup LaunchTemplateSpecification](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-autoscalinggroup-launchtemplatespecification.html#cfn-autoscaling-autoscalinggroup-launchtemplatespecification-version)によると

>Version
>The version number. CloudFormation does not support specifying $Latest, or $Default for the template version number. 

数値では入れることが出来ます

>However, you can specify LatestVersionNumber or DefaultVersionNumber using the Fn::GetAtt function.

起動時はバージョン1なのでこのままだと1が入って終わりです

今回ユーザー部門からは、当面アプリを継続開発するので前日の開発imageで閲覧用の本番サーバーを立ち上げて欲しいという要求がありASGの起動テンプレートバージョンが初期値固定では要件(要望)を満たせない、というのがこのLambda関数の設置理由です

前置きが長くなりましたが、以下のLambda関数でAutoScalingGroupの起動テンプレートのバージョンをLatestに書き換えます

Lambdaの起動トリガーはCloudTrail経由でCreateAutoScalingGroupを検知するEventを使います

ですからLambda関数はEC2関連一式をdeployする前にBackup先リージョン(バージニア)に設置しておく必要があります

```python: app.py
import boto3
asg = boto3.client("autoscaling")

def lambda_handler(event, context): 
    detail = event['detail']
   
    asg_name = detail["requestParameters"]["autoScalingGroupName"]
    tp_id = detail["requestParameters"]["launchTemplate"]["launchTemplateId"]
   
    asg.update_auto_scaling_group(
        AutoScalingGroupName = asg_name,
        LaunchTemplate = {
                'LaunchTemplateId': tp_id, 
                'Version': '$Latest'
        }
    )

    return
```

Eventを設定します

```json
{
  "detail-type": ["AWS API Call via CloudTrail"],
  "source": ["aws.autoscaling"],
  "detail": {
    "eventSource": ["autoscaling.amazonaws.com"],
    "eventName": ["CreateAutoScalingGroup"]
  }
}
```
このLambdaを設置した状態でASGの入ったtemplateをsam deployするとCreateAutoScalingGroupで発火したLambdaがASGの設定を書き換えます

<img width= 700 src = https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/84b58930-bc86-9378-8293-174319291f00.png>

## DLMの日次Backupで本番用AMIが更新されたら起動テンプレートのAMI Idを更新

こちらは有難く参考記事を拝読しました  
[特定タグが設定されたAMIが作成された時に自動で起動テンプレートを更新する](https://dev.classmethod.jp/articles/use-daily-backup-of-asg-instance-for-golden-image/)
[特定のタグが設定されたAMIを作成した際にEventBridgeからSNSへ発行する](https://dev.classmethod.jp/articles/notify-create-image-filtering-tags/)

参考にしたクラメソさん記事同様にCreateImageを起動トリガーにLambdaを発火させます

ただし、EC2のstackに含まれるDLMが作成したAMIのIdが欲しいことと、stackの中でdeployされた起動テンプレートのIdはparameter storeから受けたいので2点追加してます

まずAMIの識別子として、CreateImageからのevent引数で受け取る['requestParameters']['description']を使います

詳細説明していないですがEC2用のtemplate.yamlに'EC2_BACKUP'という文字列を入れてあり

![TP ver update-2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/5ca659fe-e9bd-59fd-05e8-5339ea4a8bd1.png)

<img width= 700 src = https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/4e9cf486-b062-46b0-ed7f-38478f54fd3b.png>  

DLMがスケジュール実行されるとCreateImageで"description"の中に以下のように吐き出されるので、Event logがこの'EC2_BACKUP'を含めば対象のAMIと認識させます 

<img width= 1200 src = https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/57d18796-b999-1a8d-2d3a-97e9bea236b2.png>


'EC2_BACKUP'をLambdaでキャッチしたらAMI Idを読み取り

```python
def lambda_handler(event, context):

    detail = event['detail']
    US_backup = detail['requestParameters']['description']

    if "EC2_BACKUP" in US_backup:
        new_ami = detail['responseElements']['imageId']
```
parameter storeに起動テンプレートIdを読みに行き

```python
        response = ssm.get_parameter(
            Name = launch_template_Key,
            WithDecryption=True,
            )
        launch_template_id = response['Parameter']['Value']
```
起動テンプレートのバージョンを更新します
```python
        response = ec2.create_launch_template_version(
            LaunchTemplateId = launch_template_id,
            SourceVersion="$Latest",
            VersionDescription="Latest-AMI",
            LaunchTemplateData={
                "ImageId": new_ami
            }
        )
  
        response = ec2.modify_launch_template(
            LaunchTemplateId = launch_template_id,
            DefaultVersion = "$Latest"
        )
```
起動テンプレートは新しい順に3つ残すことにしました
```python
        try:
            previous_version = str(int(response["LaunchTemplate"]["LatestVersionNumber"]) - 3)
            response = ec2.delete_launch_template_versions(
                LaunchTemplateId = launch_template_id,
                Versions=[
                    previous_version,
                ]
            )
        except Exception as e:
            print(e)
            pass
```
Lambda関数の全体は以下のようになります
parameter storeのkeyはLambda関数の環境変数として与えます
```python:app.py
import json, boto3, os

launch_template_Key = os.environ["launch_template_Key"]

ec2 = boto3.client("ec2")
ssm = boto3.client('ssm')

def lambda_handler(event, context):

    detail = event['detail']
    US_backup = detail['requestParameters']['description']

    if "EC2_BACKUP" in US_backup:
        new_ami = detail['responseElements']['imageId']

        response = ssm.get_parameter(
            Name = launch_template_Key,
            WithDecryption=True,
            )
        launch_template_id = response['Parameter']['Value']

        response = ec2.create_launch_template_version(
            LaunchTemplateId = launch_template_id,
            SourceVersion="$Latest",
            VersionDescription="Latest-AMI",
            LaunchTemplateData={
                "ImageId": new_ami
            }
        )
  
        response = ec2.modify_launch_template(
            LaunchTemplateId = launch_template_id,
            DefaultVersion = "$Latest"
        )

        try:
            previous_version = str(int(response["LaunchTemplate"]["LatestVersionNumber"]) - 3)
            response = ec2.delete_launch_template_versions(
                LaunchTemplateId = launch_template_id,
                Versions=[
                    previous_version,
                ]
            )
        except Exception as e:
            print(e)
            pass
    
    return
```
Eventを設定します
```json
{
  "detail-type": ["AWS API Call via CloudTrail"],
  "source": ["aws.ec2"],
  "detail": {
    "eventSource": ["ec2.amazonaws.com"],
    "eventName": ["CreateImage"]
  }
}
```



このLambdaを配置しておくことでDLMがスケジュール実行されると起動テンプレートが自動更新されます

<img width= 550 src = https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/ffd2c002-9ba5-9649-f8bb-5bd93849ffb7.png>

## まとめと感想
EC2を使ったWeb ServerをIaC的に構築し[(1)](https://qiita.com/nsaito9628/items/bcb17830d4f547816896)と合わせて自動運用に必要な補助機能をLambdaで補完しました

世の中では遙かに便利な仕組みを活用していると理解していますが諸般の事情で今回このような環境を提供しました

社内外問わず同様のニーズがありそうな気もするので別テーマで扱うことがあるかもしれません

個人的には本来Lambdaはこうやって使われながら進化してきたのかな？と思う(妄想する)と今まで以上にLambdaに親近感を感じました

にわかの域を出ていないので詰めの甘い部分を修正しながらスキルアップしたいと思います

以上です

## その他参考資料

[Boto3 documentation /Docs/Available services/ec2/Client](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#client)  
[Boto3 documentation /Docs/Available services/SSM/Client](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#client)  
[AWS::Serverless::Function](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-resource-function.html)