アプリ用EC2周りの構築でやったこと（１）　Backup先のGolden image作成

最近仕事でEC2を使ってユーザー部門向けWeb Serverを準備する機会がありました

直接運用に関わる立場ではないので、不具合が起きた場合にlog見てもらいつつも基本はstackのdeleteとdeploy(あとはaws lambda invoke)くらいで対応できるようにIaC化前提で構築しました

運用を考えたServer構築経験がないため調べることが多々あり印象に残った点を数回に分けて記録に残したいと思います

尚、VPCまわり、セキュリティ、EC2、ユーザーアプリの話はありません

[(お試しで初投稿した前回も本件の関連記事となります)](https://qiita.com/nsaito9628/items/62d82e534a34be2d5d37?utm_campaign=post_article&utm_medium=twitter&utm_source=twitter_share)


## 全体構成  

今回詳細説明はしませんが投稿の背景となる全体構成について最初に説明します

通常は東京リージョンのEC2上でユーザー部門のアプリが動いています  
EC2は開発用と本番用に分けていて、DLMによる開発用AMIの日次copyを本番用起動imageとしてLaunchTemplateに登録しています

DLMの日次copyではCrossRegionCopyをバージニアに作成します

障害等で東京のEC2上でアプリが動かせなくなった場合には  
　①バージニアにある最新コピーからGolden imageを作成  
　②東京と同じstackをバージニアにdeployして起動  
を運用の担当者がcliで実行することにしました  

![backup architecture.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/8d752621-5fd9-42b4-6583-0398109cb31f.png)


## 記事の内容  

今回の記事では  
* Backup先でGolden imageを作成する  
* AMI Idをparameter storeに出力する  

Lambda関数を説明します  

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

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

![copy_ami_export_amiid.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/71f8ee92-7838-a936-03fe-2716ec800901.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  

## Golden imageの作成

一つ目のLambda関数では、東京から日次CrossRegionCopyされているAMIのLatestを選びGolden imageとしてコピーします


#### 過去のGolden imageの削除


まず、過去にバージニアにBackupを立ち上げてGolden imageが残っていたらダブるので消去します

バージニアのAMIを参照して

```python
def delete_old_ami():
    image_id = ""

    response = ec2.describe_images(
        Owners = [Owner_id]
    )
```

最後のlambda_handlerで出てきますがバージニアGolden imageは"DevAMIBackup"と名前を付けますので、このNameに紐づいたAMIが既にあれば削除します  

AMIが見つかったらIdとSnapshot idを取得します

```python
    for image in response['Images']:
        if "DevAMIBackup" in image['Name']:
            image_id = image['ImageId']
            ebs = image['BlockDeviceMappings'][0]
            snapshot_id = ebs['Ebs']['SnapshotId']
```

AMI登録を解除してSnapshotを削除します

```python
            ec2.deregister_image(
                ImageId = image_id,
            )
            ec2.delete_snapshot(
                SnapshotId = snapshot_id,
            )

    return
```

#### CrossRegionCopyのNameとIdの取得


次に東京リージョンのDLM Policyで作成されたCrossRegionCopyを探してAMI名とAMI Idのリストを作ります

```python
def get_list_ami_name():

    response = ec2.describe_images(
        Owners = [Owner_id]
    )

    for image in response['Images']:
        if "DLM_policy" in image['Name']:
            list_ami_name.append(image['Name'])
            list_image_id.append(image['ImageId'])

    return
```

#### Latest AMI Idの取得


取得したAMI名からLatestのタイムスタンプを持つAMI Idを探します  
CrossRegionCopy作成時のタイムスタンプは直接Key:Valueで拾えなかったのでAMI名内のdatetime情報から拾いました  

東京のDLMは02:00(JST)に起動する設定にしてあり、昼間の勤務時間中に最新のAMIを取得しようとするとGMTでは昨日のAMIを探せば良いことになります  

まず、AMI名内のdatetime情報がMM.ddなのに合わせて昨日の月日をyesterday = MM.ddとして準備します  

```python 
def set_information(): 
    dt = datetime.datetime.now()

    if len(str(dt.day)) == 1:
        yesterday = "%s.%s" %(str(dt.month), "0" + str(dt.day-1))
    else:
        yesterday = "%s.%s" %(str(dt.month), str(dt.day-1))
```

yesterdayの文字列を含むAMIを探して更にHH:MMを取得します  
listにして複数HH:MMを取得してるのは開発側で複数CrossRegionCopyを作った場合を想定した対策です  

```python
    IDs = []
    HHs = []
    mms = []

    IDs_max = []
    HHs_max = []
    mms_max = []

    ID = ""
    
    for ami_name, amiid in zip(list_ami_name, list_image_id):
        if yesterday in ami_name:
            IDs.append(amiid)
            HHs.append(int(ami_name.split(".")[2][-2:]))
            mms.append(int(ami_name.split(".")[3]))
```

LatestのAMI Idを特定して返します  

```python
    hh_max = max(HHs)
    mm_max = max(mms)

    for amiid, hh, mm in zip(IDs, HHs, mms):
        if hh == hh_max:
            IDs_max.append(amiid)
            HHs_max.append(hh)
            mms_max.append(mm)

    for amiid, mm in zip(IDs_max, mms_max):
        if len(IDs_max) == 1:
            ID = amiid
        elif mm == mm_max:
            ID = amiid          
    
    return ID
```

#### Golden imageの作成

LatestのCrossRegionCopyに'DevAMIBackup'と名前を付けてGolden imageを作成します

```python
def lambda_handler(event, context):
    delete_old_ami()
    get_list_ami_name()
    ami_ID = set_information()

    ec2.copy_image(
        Name ='DevAMIBackup',
        SourceImageId = ami_ID,
        SourceRegion = Source_Region,
    )
    
    return
```

Golden imageをcopyするLambda関数の全体は以下のようになります

```python:app.py (Copy Image)   
import datetime, boto3, os

ec2 = boto3.client("ec2")

list_ami_name = []
list_image_id = []
list_TP_version = []
Owner_id = os.environ['Owner_id']
Source_Region = os.environ['Source_Region']


def delete_old_ami():
    image_id = ""

    response = ec2.describe_images(
        Owners = [Owner_id]
    )

    for image in response['Images']:
        if "DevAMIBackup" in image['Name']:
            image_id = image['ImageId']
            ebs = image['BlockDeviceMappings'][0]

            snapshot_id = ebs['Ebs']['SnapshotId']

            ec2.deregister_image(
                ImageId = image_id,
            )
            ec2.delete_snapshot(
                SnapshotId = snapshot_id,
            )

    return


def get_list_ami_name():

    response = ec2.describe_images(
        Owners = [Owner_id]
    )

    for image in response['Images']:
        if "DLM_policy" in image['Name']:
            list_ami_name.append(image['Name'])
            list_image_id.append(image['ImageId'])

    return


def set_information(): 
    t_delta = datetime.timedelta(hours=9) 
    JST = datetime.timezone(t_delta, 'JST') 
    dt = datetime.datetime.now(JST)

    if len(str(dt.day)) == 1:
        yesterday = "%s.%s" %(str(dt.month), "0" + str(dt.day-1))
    else:
        yesterday = "%s.%s" %(str(dt.month), str(dt.day-1))

    IDs = []
    HHs = []
    mms = []

    IDs_max = []
    HHs_max = []
    mms_max = []

    ID = ""
    
    for ami_name, amiid in zip(list_ami_name, list_image_id):
        if yesterday in ami_name:
            IDs.append(amiid)
            HHs.append(int(ami_name.split(".")[2][-2:]))
            mms.append(int(ami_name.split(".")[3]))
            
    hh_max = max(HHs)
    mm_max = max(mms)

    for amiid, hh, mm in zip(IDs, HHs, mms):
        if hh == hh_max:
            IDs_max.append(amiid)
            HHs_max.append(hh)
            mms_max.append(mm)

    for amiid, mm in zip(IDs_max, mms_max):
        if len(IDs_max) == 1:
            ID = amiid
        elif mm == mm_max:
            ID = amiid          
    
    return ID


def lambda_handler(event, context):
    delete_old_ami()
    get_list_ami_name()
    ami_ID = set_information()

    ec2.copy_image(
        Name ='DevAMIBackup',
        SourceImageId = ami_ID,
        SourceRegion = Source_Region,
    )
    
    return
```   


## AMI Idをparameter storeに出力


バージニアに東京と同じEC2をdeployする為のtemplate.yamlではAMI Idをparameter store経由で参照する仕様にしました  

その為に前項でcopyしたGolden imageのAMI Idをparameter storeに出力します  

AMI Idを探す操作は前項と同じです  

parameterのkeyは'Latest_AMI_Id_us-east-1'としてstringで出力しました
```python:app.py (Export AMI Id)
import boto3, os

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

Owner_id = os.environ['Owner_id']


def get_ami_id():
    image_id = ""

    response = ec2.describe_images(
    Owners = [Owner_id]
    )

    for image in response['Images']:
        if "DevAMIBackup" in image['Name']:
            image_id = image['ImageId']
            print(image_id)

    return image_id


def lambda_handler(event, context):

    image_id = get_ami_id()

    try:
        ssm.delete_parameter(
            Name='Latest_AMI_Id_us-east-1'
            )
    except Exception as e:
        pass

    ssm.put_parameter(
        Name='Latest_AMI_Id_us-east-1',
        Description='Latest backup My_EC2 AMI Id of us-east-1',
        Value=image_id,
        Type='String',
    )

```

## Lambda関数の実行


東京リージョンのEC2と通信できなくなった場合は、バージニアにVPC、NW、EC2をdeployする前に今回説明した二つのLambda関数を実行します  

#### AMIのCopy


最初にLatestのBackupをcopyします
```bash
$ aws lambda invoke --function-name Copy-Latest-Image-Function --region us-east-1 responce.json
```
```bash
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
```
<img width= 700 src = https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/e6056997-9f03-324e-65d4-570fb8bdb675.png>

正常に実行してAMIが作成できました

#### AMI Idの出力

続いてAMI Idをparameter storeに格納します
```bash
$ aws lambda invoke --function-name Export-Latest-AMIId-Function --region us-east-1 responce.json
```
```bash
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
```

<img width= 700 src = https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/e150890a-3b2c-9b82-a21d-69c559d0ec0e.png>

パラメータストアに出力ができました

## まとめ

EC2を使ったWeb Serverを自動運用するためにGolden imageをcopyする補助機能を作りました  

先人の記事を参照してもっと今っぽく自動化できたかもしれません、確認しきれなかった反省点として留めておきます  

[EC2 Auto Scaling ゴールデンイメージの更新と Auto Scaling グループのインスタンス更新を自動化しました](https://dev.classmethod.jp/articles/aws-autoscaling-amiupdate-usgin-ssmautomation/)

こちらの方法も手順習得しておきたいと思います  

[アプリ用EC2周りの構築でやったこと（２）　AutoScalingGroupの起動テンプレートVer設定に続きます](https://qiita.com/nsaito9628/items/1ed45fb2ac5ce4f6f54f)


## その他の参考資料

[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)