AWS の IPv6化について調査は済んだけど、書き始めるのに非常にドンヨリしております。図を書いて少しでも楽しくいきましょそうしましょ。
皆様も読んだらドンヨリするかもしれませんが、できるだけ丁寧に書いてみますので、PublicIP 有料化に抗いたい人は頑張って追ってみてください:-)
目次
また長いです。でもひきかえさないほうがいいかもしれない。はじめに
先日に書いた AWSコスト削減とリソース管理 | 外道父の匠 のNAT Gateway / IPv6 の箇所を、実際にやってみた、という内容になります。キッカケとしては月額1PublicIPあたり牛丼1杯分ということで
対策方法としては基本的に公式をなぞるだけに近いのですが、
自分が思っていたよりも結構調べることが多かったのと、考察ポイントも出てきました。
細かくなるかもですが、順に説明しつつ Terraform の該当箇所のみ記載していこうと思います。ただ、これが The 正解というものではなく、環境に応じて必要な構成は変わると思いますので、それを実現するためにも丁寧に理解しておくことをオススメします。
関連記事
前提知識
IPv6 は特に新しい知識というわけではないので、そのものについての説明はあまりしません。いっぱい解説ページがあるのでググるか、私が改めて調べ直したページでも見ておいてください。それとルーティングの話も重要なので、過去記事にも目を通しておくとよいです。
目的
VPC Public Subnet に多く存在するであろう、PublicIP(IPv4) 付きリソースから、PublicIP を剥がし、IPv6 と PrivateIPAddr(IPv4) だけにすることで、来年からの有料化による費用増加を防ぎます。ついでに Global IPv4 の枯渇問題を手助けした気分に浸りつつも、それより庶民には PublicIP あたり $3.6/月、100 PublicIP で $360/月 = 5万円/月以上 の増加を防ぐのか放置するのかという話です。
会社として何個の削減可能な PublicIP を保持しているかにもよりますが、基本的には構成を変更するだけでその後ずっと安い(今回は高くならない)なら、できるだけ早く取り組むべきタスクであると判断します。
メインとなる対象リソースは ALB Target Group 配下にある EC2 Autoscaling です。昨今の WEB/APサーバーは、APIを叩くといった外部通信があったり、コンテナイメージをダウンロードしたり、といった処理のため、Private に置いて NAT G/W を通すよりは、Public に置いて PublicIP で通信することで安定/安価にしていたのではないでしょうか。
こいつから無事に PublicIP を剥奪し、IPv6 でやっていけるのか、いや、やっていけるようにしなくてはなるまい。というお題でございます。
・・・それでは、参りますか(クソでかため息;-)
IPv6 の設計
IPv6 は IPv4 のように、グローバルやプライベート・クラスA,B,C という区別がなく、グローバルで一意なため、デフォルトでパブリックアドレスとなります。使用範囲も基本は AWS からガバっと割り当ててもらうため、細かく切り詰めて使う必要はなく便利です。既存の IPv4 なプライベートアドレスをそのままに、追加で IPv6 を使えるようにし、その中でリソースをデュアルスタック (IPv4 & IPv6) として、あとは ElasticIP や PublicIP の利用都合を考えて構成することになります。
従来のプライベート IPv4
いったん復習になりますが、これまでは複数アカウントを扱う際には Public / Private Subnet に IPv4 を割り当てるために、右図のような設計をしていたはずです。
会社としてAWSというクラウドに対して設計上の CIDR を大きく取り、各アカウントに対しても設計上の CIDR を割り当てます。VPC に実際にリソースに割り当てる CIDR を登録し、Subnet はその中でイィ感じに CIDR を割り振っています。
アカウントの中にどのくらい VPC を作るのか、VPC の中に何個 Subnet を作って、何IPを実際に使うのか、で範囲の大きさを調整する必要があり、最初の設計が重要で、これからもお世話になり続けます。
IPv6 の割り当て
Subnet までの構成はそのままに、IPv6 を追加で割り当てます。VPC で IPv6 を有効にした時点で、/56 の範囲が割り当てられます。そのため、アカウント内での仮CIDR 範囲で考え込む必要はありません。
Subnet は /64 固定にする必要があり、広すぎるので迷うかもですが、各Subnet に前から順番に割り振ればいいと思います。
AZ ごとの Public / Private のセットで (0, 1) (2, 3) としてもいいし、(11, 12) (21, 22) でも、そんなに Subnet を作るはずもないので、ぶっちゃけ変わりません。
Gateway と Routing
IPv4 と IPv6 は互換性がありませんので、ではどのように外部と通信をするのかというところで、Gateway と Routing が肝になります。まずは新参者の Egress-Only G/W を仲間に入れて、Gateway が3種類あるので、それぞれについて整理していきます。
ちゃんと Egress-Only G/W を知りたい人はこの辺をどうぞ。
- エグレス専用インターネットゲートウェイを使用してアウトバウンド IPv6 トラフィックを有効にする – Amazon Virtual Private Cloud
- 新機能 – Virtual Private Cloud での EC2 インスタンスの IPv6 サポート | Amazon Web Services ブログ
Gateway名 | 略称 | 解説 |
Internet Gateway | IGW | PublicIPやIPv6を送信元として、外部通信を代わりに行ってくれる二刀流 |
Egress-Only Internet gateway | EIGW | IPv6の外部通信のみ代わりに行ってくれる専門家 |
NAT Gateway | NAT G/W, NAT64 | 通常のIPv4と、Well-known Prefix な 64:ff9b::/96 のIPv6をIPv4として外部へNATしつつ送信する二刀流 |
NAT G/W は IPv6 を転送できないため、代わりに生まれたっぽいのが EIGW で、Private subnet から IPv6 だけ転送してくれて、しかも存在は無料でさらに水平スケールするのねフムフムって思ってたら……
NAT64 という新機能が NAT G/W に装備されています。IPv6 しか持たないリソースは、IPv4 しか持たない宛先に対して互換性がないので送信できません。大きなプラットフォームや SaaS は、名前解決すると IPv6 を返してくれる所が多いですが、IPv4 しかなかったら接続できないじゃないか!という問題を解決してくれます。
- AWS が IPv6 サービスと IPv4 サービス間の通信を可能にする NAT64 および DNS64 機能の提供を開始
- NAT ゲートウェイ – Amazon Virtual Private Cloud
- DNS64 と NAT64 – Amazon Virtual Private Cloud
頑張って説明すると、IPv6 を持つリソースがDNSレコードを検索した時に、IPv6 アドレスを含まない場合、IPv4 と判断して 64:ff9b::/96 と合成することで、無理やり IPv4 を含む IPv6 を返すようにできます(これは Subnet で DNS64 を設定)。それを Routing して NAT64 に送れば、IPv4 に戻して、いつもの NAT 転送をしてくれる、というわけです。
さて、よくわからんかもしれませんが、なんとなく知ったところで実際にリソースをどう設定してこれらを実現していくのかを見て、理解を深めていくことにしましょう。
VPC
まずは VPC で IPv6 を有効にします。既存のまま変更可能です。今回は AWS 提供の IPv6 CIDR ブロックを選択しており、通常はそうすると思いますが、自己所有の CIDR も使えるようです。
1 2 |
resource "aws_vpc" "default" { assign_generated_ipv6_cidr_block = true |
変更後は、管理画面で /56 の CIDR を確認できますし、terraform.tfstate.d を見るとこんな感じになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "mode": "managed", "type": "aws_vpc", "name": "default", "instances": [ { "attributes": { "assign_generated_ipv6_cidr_block": true, "ipv6_association_id": "vpc-cidr-assoc-0123456789abcdef", "ipv6_cidr_block": "1234:5678:abcd:1000::/56", "ipv6_cidr_block_network_border_group": "ap-northeast-1", "ipv6_ipam_pool_id": "", "ipv6_netmask_length": 0, |
SecurityGroup
VPC の次は Subnet に行きたいところですが、VPC を変更した時点で既存の SecurityGroup に自動的に IPv6 egress が追加されます。もし1つ1つ変更していく場合は、この時点で terraform plan すると IPv6 egress を消そうとするはずなので、ここで辻褄を合わせておいてあげます。
1 2 3 4 5 6 7 8 |
resource "aws_security_group" "default" { egress { from_port = 0 to_port = 0 protocol = "-1" ipv6_cidr_blocks = ["::/0"] } |
全ての SecurityGroup に追加して No Changes になったところで、ついでにグローバルからの接続を開けてある場合、IPv6 も開けておくとよいでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
resource "aws_security_group" "web_global" { ingress { from_port = 80 to_port = 80 protocol = "tcp" ipv6_cidr_blocks = ["::/0"] } ingress { from_port = 443 to_port = 443 protocol = "tcp" ipv6_cidr_blocks = ["::/0"] } |
もし、VPC IPv6化と同時に実行されると、the specified rule “peer: ::/0, ALL, ALLOW” already exists こんなエラーが出る可能性が高いですが、次に plan したら No Changes になると思います。
Network ACL
同じく IPv6 を有効にすると、Network ACL が自動更新されます。編集状態によって更新内容が変わるかもですが、私の場合は egress に ::/0 は自動で入りましたが、ingress には入らなかったので、疎通確認で引っかかりました。ingress をメインの VPC と、ついでに SecurityHub 対策的なデフォルト VPC にも入れておきます。
1 2 3 4 5 6 7 8 9 10 |
resource "aws_network_acl_rule" "default_ipv6_allow" { network_acl_id = aws_vpc.default.default_network_acl_id rule_number = 101 egress = false protocol = -1 rule_action = "allow" ipv6_cidr_block = "::/0" from_port = 0 to_port = 0 } |
1 2 3 4 5 6 7 8 9 10 11 12 |
resource "aws_default_network_acl" "advanced" { default_network_acl_id = aws_default_vpc.advanced.default_network_acl_id subnet_ids = data.aws_subnets.advanced.ids ingress { protocol = -1 rule_no = 101 action = "allow" ipv6_cidr_block = "::/0" from_port = 0 to_port = 0 } |
Subnet
ようやくメインディッシュで、以下の設定をします。- Subnet に IPv6 CIDR を /64 で追加
- PublicIP の自動割り当てを無効化
- IPv6 アドレスの自動割り当てを有効化
- DNS64 を有効化
下記 Terraform コードではわかりやすく固定値を書きましたが、実際には locals 変数を使っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
data "aws_availability_zones" "available" { } resource "aws_subnet" "public" { count = 3 vpc_id = aws_vpc.default.id availability_zone = data.aws_availability_zones.available.names[count.index] # 既存の IPv4 cidr_block = cidrsubnet( "10.0.0.0/18", 6, 2 * count.index, ) # 新規の IPv6 ipv6_cidr_block = cidrsubnet( aws_vpc.default.ipv6_cidr_block, 8, 2 * count.index, ) map_public_ip_on_launch = false assign_ipv6_address_on_creation = true enable_dns64 = true |
これは public 用になっていますが、Private 用も同じ内容です。CIDR をズラすために 2 * count.index + 1 にするくらいです。
PublicIP はここではいったん OFF にしましたが、Public/Private の配置構成をキチンとできていれば、Public は PublicIP を自動付与した方が運用しやすいリソースもあるかもなので、必要に応じて ON にしてください。
DNS64 は Public では無効化した方が良いだろうことを、別記事に書きました。
Egress Only G/W
Private Subnet から IPv6 での外部通信が必要な場合、作成します。通信が無料というわけではなく EC2 と同じルールだと思いますが(公式に書いていない)、存在自体は無料なので、作成した方が良いとは思います。
1 2 3 |
resource "aws_egress_only_internet_gateway" "default" { vpc_id = aws_vpc.default.id } |
NAT64
Private Subnet から従来どおりに IPv4 で外部へ NAT したり、Public なアドレスとしては IPv6 しか持たないリソースが外部の IPv4 しか持たない宛先へ接続したい場合は、作成します。既に NAT G/W がある場合はそのまま利用します。もしかしたら外部接続の内容を精査したら、無しにしてコスト削減に繋がるかもですが、後述する考察ポイントを踏まえると、結局は必要になる可能性の方が高いです。
1 2 3 4 5 6 7 8 |
resource "aws_eip" "nat" { domain = "vpc" } resource "aws_nat_gateway" "default" { allocation_id = aws_eip.nat.id subnet_id = aws_subnet.public.id } |
RouteTable
Routing の図で説明した通りに、Public と Private で RouteTable を設定します(右図・再掲)。もし EIGW や NAT64 を省いた場合、その分は無くしてください。
また、Public の 64:ff9b::/96 → NAT64 はいったん入れてありますが、Subnet へのリソース配置ルールを正せば、不要になるものです。
Public
IPv4, IPv6 ともに IGW を通します。Public Subnet だけど PublicIP を持たない場合、NAT64 へ任せることで IPv4 へ接続可能になります。ただし、そういうリソースは基本 Private にいるべきなので、そのような環境に整備できれば、この Routing は不要になります(後述)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
resource "aws_route_table" "public" { vpc_id = aws_vpc.default.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.default.id } route { ipv6_cidr_block = "::/0" gateway_id = aws_internet_gateway.default.id } route { ipv6_cidr_block = "64:ff9b::/96" nat_gateway_id = aws_nat_gateway.default.id } |
Private
IPv6 は EIGW へ、通常の IPv4 は NAT G/W へ、Well-known Prefix な IPv4 が埋め込まれた IPv6 は NAT64 へ送ります。このあたりで、64:ff9b::/96 という文字列が可愛く見えてきたかもしれませんね:-)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
resource "aws_route_table" "private" { vpc_id = aws_vpc.default.id route { ipv6_cidr_block = "::/0" egress_only_gateway_id = aws_egress_only_internet_gateway.default.id } route { cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.default.id } route { ipv6_cidr_block = "64:ff9b::/96" nat_gateway_id = aws_nat_gateway.default.id } |
RouteTable と Subnet の関連付けは従来どおり変わりません。
インスタンスで動作確認
以上で設定は完了なので、実際にインスタンスを起動して状態を確認します。IPアドレス割り当ての各デフォルト設定が変わっているのを確認しつつ、Public と Private に1台ずつ起動してみるとよいでしょう。IPv6 アドレスの確認
管理画面でも PublicIP がなくなり、IPv6 が付与されたことを確認できますが、OS内でも見ておきます。無事に IPv6 にご対面できることでしょう。
1 2 3 4 5 6 7 |
$ ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 ~snip~ 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000 ~snip~ inet6 1234:5678:abcd:1000:e062:3bf3:5418:5490/128 scope global dynamic valid_lft 438sec preferred_lft 128sec |
Routing は IPv4 は route -n , IPv6 は route -n6 で確認することができますが、クラウドの Routing はOS内に直接影響しないので、気になったら見ておく程度で良いです。
IPv6用の設定
公式ドキュメントには、ディストリビューション毎のIPv6設定が記載されています。私は Amazon Linux 2 で動作確認しましたが、Amazon Linux の設定として書かれていた以下の内容は、特に設定しなくとも IPv6 の通信は行えました。
1 2 3 |
IPV6INIT=yes DHCPV6C=yes DHCPV6C_OPTIONS=-n |
ここは各環境に応じて、設定を試しながら疎通確認することになると思います。
疎通確認
基本的なコマンドを使って、Public (PublicIP 無し) と Private で疎通確認をしておきます。返ってきた内容がなんであろうと、接続確認がとれれば◯としています。接続先は、IPv4,IPv6 両方持ちを www.amazon.com 、IPv4 のみを blog.father.gedow.net として使用しています。
Command | Public | Private |
ping www.amazon.com | x | o |
ping6 www.amazon.com | o | o |
curl -v https://www.amazon.com | o | o |
ping blog.father.gedow.net | x | o |
ping6 blog.father.gedow.net | o | o |
curl -v https://blog.father.gedow.net | o | o |
wget https://blog.father.gedow.net | o | o |
telnet blog.father.gedow.net 443 | o | o |
全ての G/W があるので、どちらの条件でも全て接続に成功しました。自分のブログに対しての宛先が 64:ff9b::3444:25c5 としっかり変換されたのでホッコリです。
このコマンドの内、ping だけは IPv4 でしか接続しようとしないので、Public Subnet で PublicIP なしには IGW から出ていくことができず失敗します。当然、PublicIP をつけたら、IGW から出ていくことはできます。
それ以外のコマンドは、IPv6 として問題なく動いてくれたので、よっぽどバージョンが古くなければ作業に困ることはないでしょう。
IPv6リソースの配置
一通り落ち着いたところで、細かいところの考察を詰めていきます。Subnet は Public / Private で2種類あり、今回で PublicIP の扱いを変えるのと、他に ElasticIP(EIP) もあります。それらを整理すると構成は以下の4種類に分類されると思われます。
Subnet | ElasticIP | PublicIP | PrivateIPv4 | IPv6 |
Public(1) | o | x | o | o |
Public(2) | x | o | o | o |
Public(3) | x | x | o | o |
Private | x | x | o | o |
元々 Private にいたリソースは、そのままで問題ありません。また、Public(1) で ElasticIP を割り当て、グローバルから直にアクセスを受けていたリソースも、そこに留まるしかありません。
問題は今まで Public(2) にて PublicIP を持っていたけど、PublicIP を剥ぎ取るリソースです。Public(3) として留まるのか、Private に移るのかという選択になります。
全く外部アクセスを必要としないアプリケーションもあるかもですが、昨今は外部APIを叩いたり、ログを送ったりするので、たいていは何かしらあるものです。それでは、そういうサーバーが IPv6 構成においてどうするべきかというと……
結論(仮) としては、Public から Private に変更すべきですが、Public のままでも多くは問題なく動く。になると思われます。
それはさきほどの接続確認で分かる通り、接続元となるクライアントが IPv4 / IPv6 に両対応していれば、どちらも接続可能であり、IPv6 へは IGW or EIGW を、IPv4 なら NAT64 を出ていくので、コストにも影響しないはずです。
ただし、IPv6 未対応な処理だと、相手が IPv4 しか持たない時、IGW から出ていけないため、その辺の判断が難しいなら Private に移したほうが無難でしょう。
これらをまとめると、
- ElasticIP を持つ場合は Public に配置
- それ以外は Private に配置
- 宛先がIPv4しか持たず、かつINPUTが多い場合はNATを通すと高いので、PublicでPublicIPを持ってIGW経由にすると転送料金が安くなるかも
- EIPを割り当てるほどでもないけど、一時的にPublicIPで外部からアクセスしたい場合
※環境によって異なるかもなので、ちゃんと自分で考えてちょ:-)
既存リソースの入れ替え
今回の IPv6 構成に変更するには、EC2 Autoscaling だと Subnet を変更する必要があるため、単純にリソースを循環させて入れ替えるだけでは済みません(Public のままでいいならそれでイケると思われ)。サービス・メンテインしてゼロ台にしてから Subnet を変更するか、もしオンラインでの変更を試みるなら、並列した2つ目の AutoscalingGroup を用意して、同台数を起動後に、旧Groupを削除する。といった方法が考えられます。
しかし、それなりに大きな変更であることと、アプリケーションの行う接続処理がどのようなものかによって、どうなるかの保証は全くないので、エイヤでやるようなものでもないでしょう。
アプリケーションの動作確認
例えばこんなことを考えてから変更する計画にしましょうという話です。接続先種類と接続方法
サーバーから外部への接続が有るのか無いのか、で全くなければ NAT G/W を無くせるのでコスト削減になります。接続が有って、接続先が全て IPv6 持ちなら、それもまた NAT G/W を無くせることになります。接続先に1つでも IPv4 のみしかない所があれば、NAT64 が必須になります。また、接続クライアントが IPv6 対応していなければ Public にいると IGW から出ていけないので、クライアントをIPv6対応するか、おとなしく Private に移ることになります。
(※Public の 0.0.0.0/0 → NAT に Routing する手段は可能かもですが、それだと EIP / PublicIP 持ちが IGW へ行かなくなり不格好になります)
時間をかけずに対応するなら、IPv6化+Private移動 をしてしまえばよいですが、改めてどのような外部のプラットフォームやサービスに接続しているのかを整理してみて、IPv6 対応がされているかを確認してみるのも、意味のある現状把握になると思います。
Lambdaの動作確認
少し例外的な話になりますが、Lambda 大好きな私は VPC所属 + NAT G/W で、boto3 で AWS API を叩きつつ、VPC 内のリソースにアクセスするような処理を仕込んでいます。VPC にいる Lambda は、NAT G/W がないと外に出ていけないという仕様は変わっていません。仮にアプリケーションの接続内容的に NAT64 がいらないとなっても、Lambda もそうでなければ NAT G/W は無くせないので、キッチリ調べてみました。ちなみに検証しなくても公式的に唯一 Lambda だけがデュアルスタックをサポートしていないことがわかるので、一縷の望みにかけただけです;-(
素の Lambda リソースは ip コマンドのような余計なモノは何も置かれていないので、レイヤーにネットワーク系ライブラリを持っていき、
1 2 3 4 |
cd ~/tmp pip3 install netifaces -t ./python zip -r layer.zip python/ aws lambda publish-layer-version --layer-name test --zip-file fileb://layer.zip --compatible-runtimes python3.9 |
割り当てられたIPアドレスを全て表示しつつ、NAT64 なしでも AWS API が叩ければ大勝利や!という感じで実行してみると
1 2 3 4 5 6 7 8 9 10 11 12 |
import boto3 import netifaces def lambda_handler(event, context): for i in netifaces.interfaces(): print(f"{i}:") ifaddresses = netifaces.ifaddresses(i) print(ifaddresses) ec2 = boto3.client('ec2') vpcs = ec2.describe_vpcs() print(vpcs) |
やっぱり無いんか~い で繋がらなくて終わり。ていうかお前、リンクローカルアドレスしか持っとらんかったんか……
1 2 3 4 5 6 |
lo: {17: [{'addr': '00:00:00:00:00:00', 'peer': '00:00:00:00:00:00'}], 2: [{'addr': '127.0.0.1', 'netmask': '255.0.0.0', 'peer': '127.0.0.1'}]} vinternal_1: {17: [{'addr': '02:72:b4:7e:c8:70', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], 2: [{'addr': '169.254.76.1', 'netmask': '255.255.254.0'}]} telemetry1_sb: {17: [{'addr': '4a:8b:36:da:18:8c', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], 2: [{'addr': '169.254.79.130', 'netmask': '255.255.255.252'}]} |
※普通のIPv6インスタンスだとこんな感じ。
1 2 3 4 5 6 |
lo: {17: [{'addr': '00:00:00:00:00:00', 'peer': '00:00:00:00:00:00'}], 2: [{'addr': '127.0.0.1', 'netmask': '255.0.0.0', 'peer': '127.0.0.1'}], 10: [{'addr': '::1', 'netmask': 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128'}]} eth0: {17: [{'addr': '06:db:68:86:ae:0d', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], 2: [{'addr': '10.0.0.77', 'netmask': '255.255.255.0', 'broadcast': '10.0.0.255'}], 10: [{'addr': '1234:5678:abcd:1001:6569:b145:da42:efa2', 'netmask': 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128'}, {'addr': 'fe80::4db:68ff:fe86:ae0d%eth0', 'netmask': 'ffff:ffff:ffff:ffff::/64'}]} |
おわりに
今回の目的の度合い的にはそこまで必須感はないんだけど、知識として蓄えたり少しでも扱ってみると、ワタシハ IPv6 チョットデキル 感が、技術力の心のスキマをお埋めしてくれるかもしれません。環境によっては、必要な部分や構成が結構違ったぜ!とか、躱しの一手でインスタンスクラスを1つ上げて台数を半減させることで PublicIP 数を減らして済ますとか、色々あると思います。
最低限、必要なパーツは揃えられたと思うので、これを元に唸りながら各々頑張ってみてください:-)