zaki work log

作業ログやら生活ログやらなんやら

[AWS / Terraform] EC2作成時のユーザーデータ内で動的に割り当てられるIPアドレスやパブリックDNSの参照とIMDSv2の設定

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を使用する」で対処。

docs.aws.amazon.com

[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-hostnameIPアドレス(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)

コメントありがとうございます!

IMDSv2

上記はIMDS(Instance Metadata Service)はv1の例で、現在はv2が推奨。

docs.aws.amazon.com

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名を使うんだろうけど、使い捨ての検証環境とかならこんな感じで十分かな。

discuss.hashicorp.com