Help us understand the problem. What is going on with this article?

[AWS]インスタンス起動時にIPアドレスを自動でRoute53に登録する

Elastic IP をケチりたい

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

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

今回はそれを推し進めて CloudWatch Event でインスタンスの起動を監視して、起動したら自動でIPアドレスを Route53 のAレコードに登録する、という 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)

ざっくり説明

インスタンスに Hostname タグがあったら、それを利用して登録します。

Hostname がない場合、Name タグを使います。ここでは正規表現で server- というプレフィクスがあるものだけを拾い上げていますが、なにかしらフィルタしないと全インスタンスを対象にするので意図しないレコード変更が起こりえます。
面倒が起こらないよう削ってしまうのもありかもしれないです。

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 のドキュメントがこれだけ長いと、なにかの拍子で例外が上がるかもしれないですね……。

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away