1. keys

    No comment

    keys
Changes in body
Source | HTML | Preview

Elastic IP をケチりたい

たまにしか起動しないインスタンスに Elastic IP をアタッチするのはもったいない。といって、起動するたびにIPアドレスが変わるとホスト名でアクセスできなくて面倒くさい。

……というテーマで以前、記事を書きました。

今回はそれを推し進めて CloudWatch Event でインスタンスの起動を監視して、起動したら自動でIPアドレスを Route53 に登録する、という Lambda を書きました。これでEIPとはおさらばだ!

あと、余計かもしれないけどインスタンスを停止したときにレコードを削除するようにしてみました。意味あるかなぁ。

必要な権限

登録したい Route 53 のホストゾーンの一覧取得・変更の権限:

  • route53:ChangeResourceRecordSets
  • route53:ListResourceRecordSets

インスタンスの情報を取得するための権限:

  • ec2:DescribeInstances

あとは Lambda 利用の基本ポリシーである AWSLambdaBasicExecutionRole

Lambda のコード

set_route53_record
import json
import os
import re
from datetime import datetime

import boto3


def json_dt(o):
    if isinstance(o, datetime):
        return o.isoformat()


def change_record(action, host_name, host_addr):
    domain_name = os.environ.get('DomainName')
    client = boto3.client('route53')
    change_batch = {
        "Comment": "optional comment about the changes in this change batch request",
        "Changes": [
            {
                "Action": F"{action}",
                "ResourceRecordSet": {
                    "Name": F"{host_name}.{domain_name}.",
                    "Type": "A",
                    "TTL": 300,
                    "ResourceRecords": [
                        {
                          "Value": F"{host_addr}"
                        }
                    ]
                }
            }
        ]
    }
    print("change_batch: " + json.dumps(change_batch, default=json_dt))
    response = client.change_resource_record_sets(
        HostedZoneId=os.environ.get('HostedZoneId'),
        ChangeBatch=change_batch
    )
    print("result: " + json.dumps(response, default=json_dt))
    return response


def get_hostname_from_tags(tags):
    host_name = ''
    for tag in tags:
        if tag['Key'] == 'Hostname':
            host_name = tag['Value']
    if not host_name:
        if tag['Key'] == 'Name':
            match = re.match(r"^server-(.*)$", tag['Value'])
            if match:
                host_name = match.group(1)
    return host_name


def check_action(state):
    if state == 'running':
        action = 'UPSERT'
    elif state == 'stopping':
        action = 'DELETE'
    else:
        action = ''
    return action


def lambda_handler(event, context):
    result = dict()
    print('event: ' + json.dumps(event, default=json_dt))
    action = check_action(event['detail']['state'])
    if action:
        ec2 = boto3.resource('ec2')
        instance = ec2.Instance(event['detail']['instance-id'])
        host_name = get_hostname_from_tags(instance.tags)
        if host_name:
            host_addr = instance.public_ip_address
            result = change_record(action, host_name, host_addr)
    return json.dumps(result, default=json_dt)

Lambda の設定

環境変数

今この Lambda を使っている環境では、ドメイン名と Route53 のホストゾーンIDはひとつだけなので Lambda の環境変数で設定するようにしました。

複数のホストゾーンにまたがって動くようにすると、結構めんどうくさそう。

DomainName=mikan.example.com
HostedZoneId=ZABCDEFGHIJKL

CloudWatch Event の設定

スクリーンショットを参考に、なんとか……。

make_cloudwatch_event.png

Lambda に渡される event の例

こんな感じで、InstanceIdState が取れます。

{
    "version": "0",
    "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "detail-type": "EC2 Instance State-change Notification",
    "source": "aws.ec2",
    "account": "123456789012",
    "time": "2019-11-20T08:52:00Z",
    "region": "ap-northeast-1",
    "resources": [
        "arn:aws:ec2:ap-northeast-1:123456789012:instance/i-0123456789abcdefg"
    ],
    "detail": {
        "instance-id": "i-0123456789abcdefg",
        "state": "stopping"
    }
}

注意点

レコードを DELETE するときは、レコードの Value が現在のものでないと失敗するんだそうで。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/route53.html#Route53.Client.change_resource_record_sets

In the case of a DELETE action, if the current value does not match the actual value, an error is returned.

エラーハンドリングなにもしていないので、あとで考えます。
change_resource_record_sets のドキュメントがこれだけ長いと、なにかの拍子で例外が上がるかもしれないですね……。

エラーハンドリングをきちんとして、記事を更新したいと思いますー。