TerraformでEC2プロビジョニング時に、ユーザーデータ内でパブリックDNS名を参照しようとしたらすんなり実装できなかったのでメモ。
AWSでEC2をプロビジョニングする際にホスト上の初期処理を投入したい場合は、実行したいコマンドをシェルスクリプトとして記述できるユーザーデータが便利。 ただし、Terraformなどから利用する場合にテンプレートを使ってパラメタも指定できてさらに便利に使えるけど、EC2をデプロイした結果決まる値を渡すことはできない。たとえばIPアドレスとか。
TerraformのNG実装例
よくある(?)例として、EC2上にデプロイするアプリケーションに自信のホスト名やアドレスを設定したいケースで、テンプレートにpublic_dnsを指定してもエラーになる。
resource "aws_instance" "server" { ami = var.server_ami_id instance_type = var.server_instance_type subnet_id = aws_subnet.subnet.id associate_public_ip_address = true [...] user_data = templatefile("${path.module}/user_data.sh", { public_dns = aws_instance.server.public_dns })
上記の定義だと以下のように自身を参照できずにエラーになる。
╷ │ Error: Self-referential block │ │ on online/main.tf line 115, in resource "aws_instance" "server": │ 115: public_dns = aws_instance.server.public_dns │ │ Configuration for aws_instance.server may not refer to itself. ╵
メタデータの参照
検索すると「ネットワークインタフェースを先に単体で作成・アドレスを決定し、EC2を作成してアタッチ」という方法もあったけど、インフラ構成が変わるのが個人的には微妙だったので、「ユーザーデータ内でメタデータを返すAPIを使用する」で対処。
[ec2-user@ip-10-1-1-227 ~]$ curl http://169.254.169.254/latest/meta-data/ ami-id ami-launch-index ami-manifest-path block-device-mapping/ events/ hibernation/ hostname identity-credentials/ instance-action instance-id instance-life-cycle instance-type local-hostname local-ipv4 mac metrics/ network/ placement/ profile public-hostname public-ipv4 public-keys/ reservation-id security-groups services/ system
このように、様々な情報を参照できるエンドポイントが提供されている。
DNS名であればpublic-hostname
でIPアドレス(GIP)ならpublic-ipv4
という具合。
[ec2-user@ip-10-1-1-227 ~]$ curl http://169.254.169.254/latest/meta-data/public-hostname ec2-**-**-**-**.ap-northeast-1.compute.amazonaws.com
プライベートIPアドレスも取れる。
[ec2-user@ip-10-1-1-227 ~]$ ip a s eth0 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000 link/ether 06:c9:30:bd:85:55 brd ff:ff:ff:ff:ff:ff inet 10.1.1.227/24 brd 10.1.1.255 scope global dynamic noprefixroute eth0 valid_lft 1931sec preferred_lft 1931sec inet6 fe80::4c9:30ff:febd:8555/64 scope link valid_lft forever preferred_lft forever [ec2-user@ip-10-1-1-227 ~]$ curl http://169.254.169.254/latest/meta-data/local-ipv4 10.1.1.227
このAPIを使えば、テンプレートの引数も使わず、例えばIPアドレスをセットしたい箇所で $(curl http://169.254.169.254/latest/meta-data/public-ipv4)
と記述すれば、EC2プロビジョニング時にはEC2のグローバルIPに変換される、という寸法。
と、ここまで書いてシェアしたところ、Xで「IMDSはv2を使うように」とコメントをいただいたので追加調査。 (認証不要のv1の方で動いたのでこれでいいやと思ってブログにしたのが本音w)
IMDS は v2 をつかってくだされ〜
— すぎむら (@sugitk) 2024年4月13日
コメントありがとうございます!
IMDSv2
上記はIMDS(Instance Metadata Service)はv1の例で、現在はv2が推奨。
IMDSv2を使うにはまず認証トークンを取得する必要がある。
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 600")
このトークンをヘッダに指定して実行すればOK
[ec2-user@ip-10-1-1-229 ~]$ curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-ipv4 52.*.*.*
ここでトークンの期限を600秒に指定しているので、時間が過ぎれば使用不可になる。
[ec2-user@ip-10-1-1-229 ~]$ curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-ipv4 <?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>401 - Unauthorized</title> </head> <body> <h1>401 - Unauthorized</h1> </body> </html>
IMDSv2を必須にするにはEC2の設定で変更でき、v2を必須にするとトークン不要のv1のアクセスが使用不能になる。
[ec2-user@ip-10-1-1-229 ~]$ curl http://169.254.169.254/latest/meta-data/public-ipv4 <?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>401 - Unauthorized</title> </head> <body> <h1>401 - Unauthorized</h1> </body> </html>
IMDSv2必須設定
設定変更はUIであれば以下の通り。
Terraformの場合はmetadata_options
で指定可能。
resource "aws_instance" "server" { [...] metadata_options { http_tokens = "required" }
まぁ商用だったらEC2デプロイ時に決定するIPアドレスやDNS名を使うことはなくて別途レコード作成した自前のDNS名を使うんだろうけど、使い捨ての検証環境とかならこんな感じで十分かな。