インスタンスのスケール時にLambdaでレコードを更新する

## 概要と課題
参画中の案件で、AutoScalingから起動しているインスタンスのプライベートIPを宛先に、プライベートホストゾーンにレコードを登録しています。
現状ではインスタンスのスケール時にプライベートIPが変わってしまうため、スケールイベントが発生するたびに手動でレコードの更新が必要になってしまいます。それはあまり現実的ではないのですが、
要件的にENIやEIPを使用して固定IPを割り当てることができないため、別途スケールイベント発生時にレコードの更新をする仕組みが必要になり、その際に検証したことを記載していきます。

## 解決策
いくつか解決策はあったのですが今回は、
ライフサイクルフック + EventBridge + Lambda でインスタンスのスケールイベントに合わせてレコードの更新を行っていきます！
他の解決策としては、下記も検討していました。

1. ライフサイクルフック + SQS + Lambda
1. CloudInit

全て一長一短ある方法だとは思うのですが、CloudInitはエラーハンドリングのしにくさもあり選択肢から消してしまいました。
SQSはEventBridgeよりもより詳細に設定が出来るのですが、今回のケースではそこまで複雑な処理をしないためよりシンプルに実装できるEventBridgeを選択しました。

## 構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/b5b64afd-c65e-4de5-b16e-966b2464c368.png)

スケールイン、アウトのイベントをEventBridgeで検知してLambdaで該当インスタンスのプライベートIPを持つレコードを削除 or 作成します。
一応EventBridgeではなくSQSでもLambdaと連携してレコードの更新を行うことは可能でした。
ただAWS的にはEventBridgeを使用した構成の方が推奨とのことだったのと、SQSを使用するような複雑な要件が今回はなかったので今回は見送っています。

## 実践

下記インスタンスをAutoScalingGroupから起動。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/ca70967c-dea2-45f7-a824-28e09ce9e4f3.png)

インスタンスのプライベートIPを宛先にしたレコードを作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/7cb76f24-1d4d-405c-b0f5-54d138a8d06b.png)

インスタンスの起動と終了時のライフサイクルフックを作成。これにより、Lambdaからの完了通知がくるまでスケールイベントを一時中断できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/ada71e74-7ec8-4e78-9464-bda76c7b90da.png)


ここからインスタンスを削除すると、下記の流れで処理が走ります。
1. 終了時のライフサイクルフックによりスケールイン処理が一時中断
1. EventBridgeのルールがスケールインイベントを検知しLambdaに連携
1. Lambdaの関数で該当インスタンスのプライベートIPを宛先に持つレコードを削除
1. ライフサイクルフックに完了通知
1. スケールインイベント再開
1. インスタンスの数が減ったのでスケールアウトイベント発生
1. 起動時のライフサイクルフックによりスケールアウト処理が一時中断
1. EventBridgeのルールがスケールアウトイベントを検知しLambdaに連携
1. Lambdaの関数で該当インスタンスのプライベートIPを宛先に持つレコードを作成
1. ライフサイクルフックに完了通知
1. スケールアウトイベント再開

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/d9187bb3-4ac0-43c7-be03-2c7b46ee8dc2.png)

下記コマンドでAutoScalingから起動しているインスタンスを削除します。
```bash
aws autoscaling terminate-instance-in-auto-scaling-group --instance-id i-042566f52ff29874b --no-should-decrement-desired-capacity --profile your-profile
```

そうするとEventBridge経由でLambdaの処理が走ります。

***終了時のLambdaの実行ログ***
```
2025-05-25T09:03:20.854Z
b6e63d1a-2b2a-4e3c-bbb0-c79ecc429ca9
2025/05/25/[$LATEST]024ae7ad4e644ccfae7d1119fa579225
1759.16
1760.0
128.0
98.0
@billedDuration
1760.0
@duration
1759.16
@entity.Attributes.Lambda.Function
EventBridgeDeleteRecord
@entity.Attributes.PlatformType
AWS::Lambda
@entity.KeyAttributes.Environment
lambda:default
@entity.KeyAttributes.Name
EventBridgeDeleteRecord
```

***起動時のLambdaの実行ログ***
```
2025-05-25T09:04:00.395Z
f93f8cca-204f-41fd-8c8a-3c0c7960988d
2025/05/25/[$LATEST]a28ba77e85944083a69260c8d515472a
1791.78
1792.0
128.0
97.0
@billedDuration
1792.0
@duration
1791.78
@entity.Attributes.Lambda.Function
EventBridgeCreateRecord
@entity.Attributes.PlatformType
AWS::Lambda
@entity.KeyAttributes.Environment
lambda:default
@entity.KeyAttributes.Name
EventBridgeCreateRecord
```

タイムスタンプを見ると、終了時の関数が動いてから20秒後には起動時の関数が動いているのがわかります。

インスタンスとレコードを確認します。
新たに起動したインスタンスのプライベートIPは、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/dbc7d971-7b4b-4c4e-8d1d-ba24edecb882.png)

そしてレコードの一覧を確認してみると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/fcbd5aa1-4627-41c3-8968-2b58a26e4c66.png)

新しく起動したインスタンスのプライベートIP宛にレコードが更新されていました！！

また今回使用したLambdaのコードは下記です。

<details>
<summary>インスタンス起動時のLambda関数</summary>

```python

import os
import boto3

ec2 = boto3.client('ec2')
route53 = boto3.client('route53')
autoscaling = boto3.client('autoscaling')

def lambda_handler(event, context):
    # 1. イベントからインスタンスIDとライフサイクル情報取得
    instance_id = event['detail']['EC2InstanceId']
    asg_name = event['detail']['AutoScalingGroupName']
    lifecycle_hook_name = event['detail']['LifecycleHookName']
    lifecycle_action_token = event['detail']['LifecycleActionToken']

    # 2. インスタンスのプライベートIP取得
    reservations = ec2.describe_instances(InstanceIds=[instance_id])['Reservations']
    private_ip = reservations[0]['Instances'][0]['PrivateIpAddress']

    # 3. ホストゾーンIDとゾーン名取得
    hosted_zone_id = os.environ['HOSTED_ZONE_ID']
    zone = route53.get_hosted_zone(Id=hosted_zone_id)
    hosted_zone_name = zone['HostedZone']['Name'].rstrip('.')  # 末尾の.を除去

    # 4. レコード名生成
    record_name = f'choshu2.{hosted_zone_name}'

    # 5. Route53にAレコード登録
    route53.change_resource_record_sets(
        HostedZoneId=hosted_zone_id,
        ChangeBatch={
            'Changes': [{
                'Action': 'UPSERT',
                'ResourceRecordSet': {
                    'Name': record_name,
                    'Type': 'A',
                    'TTL': 60,
                    'ResourceRecords': [{'Value': private_ip}]
                }
            }]
        }
    )

    # 6. ライフサイクルフック完了通知
    autoscaling.complete_lifecycle_action(
        LifecycleHookName=lifecycle_hook_name,
        AutoScalingGroupName=asg_name,
        LifecycleActionToken=lifecycle_action_token,
        LifecycleActionResult='CONTINUE'
    )

    return {'status': 'success'}
```

</details>


<details>

<summary>インスタンス終了時のLambda関数</summary>

```python
import os
import boto3

ec2 = boto3.client('ec2')
route53 = boto3.client('route53')
autoscaling = boto3.client('autoscaling')

def lambda_handler(event, context):
    # 1. イベントからインスタンスIDとライフサイクル情報取得
    instance_id = event['detail']['EC2InstanceId']
    asg_name = event['detail']['AutoScalingGroupName']
    lifecycle_hook_name = event['detail']['LifecycleHookName']
    lifecycle_action_token = event['detail']['LifecycleActionToken']

    # 2. インスタンスのプライベートIP取得
    reservations = ec2.describe_instances(InstanceIds=[instance_id])['Reservations']
    private_ip = reservations[0]['Instances'][0]['PrivateIpAddress']

    # 3. ホストゾーンIDとゾーン名取得
    hosted_zone_id = os.environ['HOSTED_ZONE_ID']
    zone = route53.get_hosted_zone(Id=hosted_zone_id)
    hosted_zone_name = zone['HostedZone']['Name'].rstrip('.')

    # 4. レコード名生成
    record_name = f'choshu2.{hosted_zone_name}'

    # 5. Route53からAレコード削除
    route53.change_resource_record_sets(
        HostedZoneId=hosted_zone_id,
        ChangeBatch={
            'Changes': [{
                'Action': 'DELETE',
                'ResourceRecordSet': {
                    'Name': record_name,
                    'Type': 'A',
                    'TTL': 60,
                    'ResourceRecords': [{'Value': private_ip}]
                }
            }]
        }
    )

    # 6. ライフサイクルフック完了通知
    autoscaling.complete_lifecycle_action(
        LifecycleHookName=lifecycle_hook_name,
        AutoScalingGroupName=asg_name,
        LifecycleActionToken=lifecycle_action_token,
        LifecycleActionResult='CONTINUE'
    )

    return {'status': 'success'}

```

</details>

両Lambda関数とも環境変数に、`HOSTED_ZONE_ID`を作成すれば動くはずなのでよければ使用してみてください。

## 終わりに
今回は閉域網に閉じたインスタンスのスケールイベントに合わせてレコードの更新を行うアーキテクチャについて解説しました。
固定ＩＰを使用できないという特殊な要件下ではありますが、ＡＷＳのサービスはかなり多岐にわたりますので、工夫次第で何かしらの解決策を見出すことができることを痛感しました。
この記事が誰かの参考になりましたら幸いです。