1. keys

    Posted

    keys
Changes in title
+[AWS]インスタンス起動時にIPアドレスをRoute53に登録する
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,146 @@
+# Elastic IP をケチりたい
+
+たまにしか起動しないインスタンスに Elastic IP をアタッチするのはもったいない。といって、起動するたびにIPアドレスが変わるとホスト名でアクセスできなくて面倒くさい。
+
+……というテーマで以前、[記事](https://qiita.com/keys/items/264a64c2841875d51cdd)を書きました。
+
+今回はそれを推し進めて CloudWatch Event でインスタンスの起動を監視して、起動したら自動でIPアドレスを Route53 に登録する、という Lambda を書きました。これでEIPとはおさらばだ!
+
+あと、余計かもしれないけどインスタンスを停止したときにレコードを削除するようにしてみました。意味あるかなぁ。
+
+# Lambda のコード
+
+```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223758/d4746ac1-73f3-0f9d-bec4-fb36785a0729.png)
+
+## Lambda に渡される event の例
+
+こんな感じで、`InstanceId` と `State` が取れます。
+
+```json
+{
+ "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` のドキュメントがこれだけ長いと、なにかの拍子で例外が上がるかもしれないですね……。
+
+エラーハンドリングをきちんとして、記事を更新したいと思いますー。