1. keys

    No comment

    keys
Changes in body
Source | HTML | Preview
@@ -1,159 +1,159 @@
# Elastic IP をケチりたい
たまにしか起動しないインスタンスに Elastic IP をアタッチするのはもったいない。といって、起動するたびにIPアドレスが変わるとホスト名でアクセスできなくて面倒くさい。
……というテーマで以前、[記事](https://qiita.com/keys/items/264a64c2841875d51cdd)を書きました。
今回はそれを推し進めて CloudWatch Event でインスタンスの起動を監視して、起動したら自動でIPアドレスを Route53 に登録する、という Lambda を書きました。これでEIPとはおさらばだ!
あと、余計かもしれないけどインスタンスを停止したときにレコードを削除するようにしてみました。意味あるかなぁ。
## 必要な権限
登録したい Route 53 のホストゾーンの一覧取得・変更の権限:
- `route53:ChangeResourceRecordSets`
- `route53:ListResourceRecordSets`
インスタンスの情報を取得するための権限:
- `ec2:DescribeInstances`
あとは Lambda 利用の基本ポリシーである `AWSLambdaBasicExecutionRole`
## 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` のドキュメントがこれだけ長いと、なにかの拍子で例外が上がるかもしれないですね……。
-エラーハンドリングをきちんとして、記事を更新したいと思います
+いずれエラーハンドリングをきちんとして、記事を更新したいと思います。いずれ……