【Serverless Framework】EC2インスタンス起動時に動的パブリックIPをLambda Rubyでroute53に登録
CloudWatch EventsのEC2インスタンスの起動をトリガーにLambda関数を実行し、起動したEC2インスタンスの動的パブリックIPをroute53のレコードに登録します。また、EC2の停止をトリガーに対象のレコードを削除します。 VPNが接続できない、したくない環境、Elastic IPを節約したいといような個人ユーザにはちょっぴりうれしいかもしれません。興味本位で以下を触りたかったというのが本音で、お題はおまけです。
- Lambdaでrubyを使ってみたい
- Serverless Frameworkを使ってみたい
前提条件
前提条件
- CentOS 7系利用
- credentialが設定済みであること
- Serverless Frameworkインストール済み
- ruby2.5(Lambdaのサポートバージョン)インストール済み
構成図
機能詳細
EC2インスタンスの起動時に以下の2つのタグを利用して、route53のホストゾーンにAレコードを登録します。SetRoute53タグの値がONの時、[Nameタグの値].[ドメイン名]で、動的パブリックIPをroute53のAレコードに登録します。また、EC2インスタンスの停止時に登録されているAレコードの削除を行います。
利用するタグ
- Nameタグ
- SetRoute53タグ(値にONを記載)
設定例
項目 | 設定値 |
Nameタグ | demo-machine |
SetRoute53タグ | ON ※その他の値やタグ未設定の場合は登録しない |
ドメイン名 | tekunote.com |
登録されるAレコードのFQDN | demo-machine.tekunote.com |
やってみよう
Serverless FrameworkでLambda関数の作成とデプロイ
Lambda関数の雛型作成
サービス名、作成先のパス共にdynamic-dns
でAWS Lambda のRuby環境の雛型を作成し、対象のパスへ移動します。
$ sls create --template aws-ruby --path dynamic-dns --name dynamic-dns $ cd dynamic-dns
dynamic-dns
ディレクトリ内に作成されたserverless.yml
、handler.rb
の2ファイル利用します。
$ ls -la total 16 drwxrwxr-x. 3 demo-user demo-user 83 May 9 17:04 . drwx------. 15 demo-user demo-user 4096 May 9 17:04 .. -rw-r--r--. 1 demo-user demo-user 1124 May 6 23:23 .gitignore -rw-rw-r--. 1 demo-user demo-user 1552 May 9 09:50 handler.rb drwxrwxr-x. 2 demo-user demo-user 157 May 9 16:00 .serverless -rw-r--r--. 1 demo-user demo-user 1011 May 9 15:56 serverless.yml
環境変数の設定
次の手順で、serverless.yml
を設定しますが、以下のパラメータはべた書きせず、環境変数から読み込むようにしていますので設定しておきます。
$ export AWS_ACCOUNT_ID=111122223333 # AWSアカウントID $ export HOSTED_ZONE_ID=ABCDEFH012345 # 設定対象のホストゾーンID $ export AWS_REGION=ap-northeast-1 # リージョン $ export DOMAIN=tekunote.com # Aレコードを設定する対象のドメイン名
serverless.yamlの設定
以下の通り設定します。環境変数HOSTED_ZONE_ID
、DOMAIN
についてはLambda関数の環境変数に登録されます。serverless.yml
のリファレンスはこちらです。
service: dynamic-dns provider: name: aws stage: nonstage runtime: ruby2.5 region: ${env:AWS_REGION} memorySize: 256 timeout: 60 iamRoleStatements: - Effect: Allow Action: - ec2:DescribeInstances Resource: "*" - Effect: Allow Action: - route53:ChangeResourceRecordSets - route53:ListResourceRecordSets Resource: "arn:aws:route53:::hostedzone/${self:functions.change_record.environment.HOSTED_ZONE_ID}" functions: change_record: handler: handler.change_record environment: HOSTED_ZONE_ID: ${env:HOSTED_ZONE_ID} DOMAIN: ${env:DOMAIN} events: - cloudwatchEvent: event: source: - "aws.ec2" detail-type: - "EC2 Instance State-change Notification" detail: state: - running - stopped
serverless.yaml
の設定内容について補足します。 Lambda関数に割り当てるIAMロールの設定はiamRoleStatements:
内で設定しています。Lambda関数にec2:DescribeInstances
、route53:ChangeResourceRecordSets
、route53:ListResourceRecordSets
の権限を設定しています。CloudWatch Logsの操作関連の権限も必要ですが、Serverless Frameworkではデフォルトで付与されるためserverless.yaml
内に記載する必要はありません。
iamRoleStatements: - Effect: Allow Action: - ec2:DescribeInstances Resource: "*" - Effect: Allow Action: - route53:ChangeResourceRecordSets - route53:ListResourceRecordSets Resource: "arn:aws:route53:::hostedzone/${self:functions.change_record.environment.HOSTED_ZONE_ID}"
CloudWatch Eventのトリガ(EC2起動時に実行)はevents:
内で設定しています。
events: - cloudwatchEvent: event: source: - "aws.ec2" detail-type: - "EC2 Instance State-change Notification" detail: state: - running - stopped
handler.rbの作成
今回作成してみたLambda関数のソースは以下です。handler.rb
に上書きします。
require 'aws-sdk' def change_record(event:, context:) ttl = '60' record_type = 'A' filter_tag_key = 'SetRoute53' filter_tag_value = 'ON' ec2_client = Aws::EC2::Client.new route53_client = Aws::Route53::Client.new # タグとCloudWatchのイベントから取得したインスタンスIDでフィルタ ec2_resp = ec2_client.describe_instances( filters: [ { name: "tag:#{filter_tag_key}", values: [filter_tag_value.to_s] } ], instance_ids: [(event['detail']['instance-id']).to_s] ) instance = ec2_resp.reservations[0].instances[0] instance.tags.each do |tag| # レコードの更新にはNameタグの値を利用するので、Nameタグと一致するときのみ処理 next unless tag[:key] == 'Name' route53_resp = route53_client.list_resource_record_sets( hosted_zone_id: (ENV['HOSTED_ZONE_ID']).to_s, start_record_type: record_type, start_record_name: "#{tag[:value]}.#{ENV['DOMAIN']}.", max_items: 1 ) record_name = route53_resp.resource_record_sets[0].name record_value = route53_resp.resource_record_sets[0].resource_records[0].value # 起動時は、EC2インスタンスに割り当てられたパブリックIPを利用して更新処理 if instance.state.name == 'running' record_set_action = 'UPSERT' ip_address = instance.public_ip_address # 停止時は、既にAレコードに登録済みのIPを取得し削除処理 elsif instance.state.name == 'stopped' record_set_action = 'DELETE' ip_address = record_name == "#{tag[:value]}.#{ENV['DOMAIN']}." ? record_value : nil end next if ip_address.nil? # Aレコードの更新、削除処理 route53_client.change_resource_record_sets( change_batch: { changes: [ action: record_set_action, resource_record_set: { name: "#{tag[:value]}.#{ENV['DOMAIN']}.", type: record_type, ttl: ttl.to_s, resource_records: [ { value: ip_address } ] } ] }, hosted_zone_id: ENV['HOSTED_ZONE_ID'] ) # CloudWatch Logに出力 puts "#{record_set_action} Record Infomation: type=#{record_type} fqdn=#{tag[:value]}.#{ENV['DOMAIN']}. ip=#{ip_address}" end end
デプロイ
CloudFormationスタックdynamic-dns-nonstage
が作成され、CloudFormationから各リソースが作成されます。deploy時のオプションの詳細表示(-v
)で、CloudFormationからどのリソースが作成されるか確認できますが、今回は指定せずに伏せています。
[demo-user@demo-machine dynamic-dns]$ sls deploy Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Creating Stack... Serverless: Checking Stack create progress... ..... Serverless: Stack create finished... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service dynamic-dns.zip file to S3 (1.92 KB)... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... ..................... Serverless: Stack update finished... Service Information service: dynamic-dns stage: nonstage region: ap-northeast-1 stack: dynamic-dns-nonstage resources: 7 api keys: None endpoints: None functions: change_record: dynamic-dns-nonstage-change_record layers: None
動作確認
EC2インスタンスにタグを設定し起動
以下のようにEC2インスタンスのタグを設定し、起動してみます。
Route53のレコード確認
IPはマスクしていますが、このような形でレコードが登録されます。
CloudWatch Log確認
こちらもIPはマスクしていますが、このような形で表示されます。 EC2を停止した場合は、登録したAレコードが削除されます。
最後に
Serverless Frameworkが、CloudFormationでデプロイされることさえ、使ってみるまで知らなかったです。。。CloudFormationの変更セット適用するとき、差分チェックやら置換の有無やら結構たいへんだったなぁという思い出に浸りました。読んでいただきありがとうございました。