Amazon MWAA ServerlessのタスクからAWSサービスに到達できる範囲をVPCの有無で確認してみた
こんにちは。サービス開発部の武田です。
Amazon MWAA ServerlessでPythonOperatorとBashOperatorが使えるようになりました。ワークフローのタスクから、自前のコードでAWS APIを呼べます。
使い方やコードのスナップショットの挙動は、前回の記事で確認しました。
その検証の中で、VPCなしの構成でboto3からsts get-caller-identityを呼ぶとConnectTimeoutErrorになっていました。一方で、S3への書き込みは成功しています。MWAA ServerlessはVPCの作成が任意なのですが、ではVPCを渡さない状態でタスクから届くAWSサービスはどこまでなのか。届かないサービスを呼ぶにはVPCに何を用意すればよいのか。構成を変えながら確かめました。
VPCなしで届くもの
ドキュメントには、タスクはデフォルトでインターネットアクセスなしで動き、外部への接続がいるならインターネットアクセスのあるVPCをNetworkConfigurationで渡す、とあります。
まず、VPCなしのままで各サービスに届くかを確認しました。boto3で各サービスのAPIを、接続タイムアウト3秒・再試行なしで1回ずつ呼ぶPythonタスクと、getent hostsで名前解決の結果を出すBashOperatorのタスクを用意しています。実行ロールには対象サービスの権限を付けていないので、AccessDeniedが返れば到達はしています。
# netprobe.py(抜粋)
import boto3
from botocore.config import Config
CFG = Config(connect_timeout=3, read_timeout=5, retries={"max_attempts": 0})
def probe():
calls = {
"sts": lambda: boto3.client("sts", config=CFG).get_caller_identity(),
"s3": lambda: boto3.client("s3", config=CFG).list_buckets(),
"sqs": lambda: boto3.client("sqs", config=CFG).list_queues(),
"logs": lambda: boto3.client("logs", config=CFG).describe_log_groups(limit=1),
"dynamodb": lambda: boto3.client("dynamodb", config=CFG).list_tables(Limit=1),
}
results = {}
for name, fn in calls.items():
try:
fn()
results[name] = "ok"
except Exception as e:
results[name] = f"{e.__class__.__name__}"
print(results)
return results
結果は次のとおりです。
| 呼び先 | 結果 | 名前解決 |
|---|---|---|
STS(sts.ap-northeast-1.amazonaws.com) |
ConnectTimeoutError |
パブリックIP |
STS(グローバル sts.amazonaws.com) |
ConnectTimeoutError |
パブリックIP |
| S3 | AccessDenied(到達) |
パブリックIP |
| SQS | ConnectTimeoutError |
パブリックIP |
| CloudWatch Logs | AccessDeniedException(到達) |
プライベートIP(10.x) |
| DynamoDB | ConnectTimeoutError |
パブリックIP |
インターネット(checkip.amazonaws.com) |
タイムアウト | パブリックIP |
CloudWatch LogsだけがプライベートIPに解決され、S3はパブリックIPに解決されながら到達しています。ワーカーの/etc/resolv.confはnameserver 10.0.0.2、search ap-northeast-1.compute.internalでした。VPCのAmazon提供DNSで見られる設定と同じです。サービス管理側にS3向けのゲートウェイエンドポイントと、CloudWatch Logs向けのインタフェースエンドポイント(プライベートDNS有効)がある構成なら、この結果は説明が付きます。
必要な呼び先に到達できない場合は、NetworkConfigurationで自前VPCを指定し、NATゲートウェイまたはVPCエンドポイントで経路を用意します。
自前VPCを渡す
共通要件と経路の選択肢
ドキュメントのネットワーク要件は次のとおりです。
共通で必要なものは次の3つです。
- 別AZのプライベートサブネットを2つ以上(インターネットゲートウェイへのルートを持たないこと)
- セキュリティグループ(自己参照のインバウンド許可と、全トラフィックのアウトバウンド許可)
- ネットワークACL(ドキュメントの推奨例はインバウンド・アウトバウンドとも全許可で、デフォルトのACLがこの状態)
経路は次のどちらかを選びます。
- NATゲートウェイを置くパブリックルーティング(ドキュメントはパブリックサブネットごとに1つ)
- 使うサービスごとのVPCエンドポイントを置くプライベートルーティング
今回作った構成は、VPCが10.0.0.0/16、パブリックサブネット2つ(NAT用)とプライベートサブネット2つ(10.0.10.0/24、10.0.11.0/24)です。
セキュリティグループには自己参照のインバウンドを許可します。アウトバウンドは、作成時の全許可をそのまま残しています。
SG=$(aws ec2 create-security-group --group-name mwaa-sls-sg \
--description "mwaa serverless" --vpc-id vpc-xxxxxxxx \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol -1 --source-group "$SG"
S3のゲートウェイエンドポイントと、STSのインタフェースエンドポイントを作ります。インタフェースエンドポイントはプライベートDNSを有効にし、プライベートサブネット2つと上のセキュリティグループに関連付けます。
# S3(ゲートウェイエンドポイント)
aws ec2 create-vpc-endpoint --vpc-id vpc-xxxxxxxx \
--service-name com.amazonaws.ap-northeast-1.s3 --vpc-endpoint-type Gateway \
--route-table-ids rtb-private-a rtb-private-c
# STS(インタフェースエンドポイント、プライベートDNS有効)
aws ec2 create-vpc-endpoint --vpc-id vpc-xxxxxxxx \
--service-name com.amazonaws.ap-northeast-1.sts --vpc-endpoint-type Interface \
--subnet-ids subnet-private-a subnet-private-c \
--security-group-ids "$SG" --private-dns-enabled
エンドポイントポリシーは、今回はデフォルト(フルアクセス)のままにしています。S3のゲートウェイエンドポイントポリシーを自前バケットだけに絞る場合は注意してください。ドキュメントのポリシー例には、prod-<region>-starport-layer-bucket(Amazon ECRのイメージレイヤー取得用バケット)へのアクセスが必要と記載されています。自前バケットのみに絞ると、ワーカーのコンテナイメージを取得できなくなる可能性があります。
ワークフローにVPCを渡す
update-workflow(またはcreate-workflow)の--network-configurationに、作成したプライベートサブネットとセキュリティグループを指定します。ワークフローの他の設定(定義、コード、ロール)は毎回一緒に指定します。
aws mwaa-serverless update-workflow \
--workflow-arn arn:aws:airflow-serverless:ap-northeast-1:123456789012:workflow/net-probe-xxxxxxxxxx \
--definition-s3-location '{"Bucket":"amzn-s3-demo-bucket","ObjectKey":"definitions/net.yaml"}' \
--code '{"S3Location":{"Bucket":"amzn-s3-demo-bucket","ObjectKey":"code/netprobe.zip"}}' \
--role-arn arn:aws:iam::123456789012:role/mwaa-sls-exec \
--network-configuration '{"SecurityGroupIds":["sg-xxxxxxxx"],"SubnetIds":["subnet-private-a","subnet-private-c"]}'
ちなみに、後片付けのときにNetworkConfigurationへ空の配列を指定したupdate-workflowはValidationExceptionになりました。既存ワークフローをVPCなしへ戻す方法は確認できていません。
構成を変えて確かめる
同じワークフローを、VPC側の構成だけ変えて実行しました。
| 構成 | STS | S3 | SQS / DynamoDB | CloudWatch Logs API/タスクログ | インターネット |
|---|---|---|---|---|---|
| VPCなし | ✗ | ○ | ✗ | ○/あり | ✗ |
| 自前VPC + NAT + STSエンドポイント | ○(エンドポイント経由) | ○ | ○ | ○/あり | ○ |
| 自前VPC + NATのみ | ○(NAT経由) | ○ | ○ | ○/あり | ○ |
| 自前VPC + STSエンドポイント + S3ゲートウェイのみ(NATなし) | ○(エンドポイント経由) | ○ | ✗ | ✗/なし | ✗ |
| 上記 + CloudWatch Logsエンドポイント | ○ | ○ | ✗ | ○/あり | ✗ |
NAT + STSエンドポイント
VPCを渡した最初の実行で、ワーカーは10.0.10.243で動いていました。指定したプライベートサブネットの中です。sts.ap-northeast-1.amazonaws.comはエンドポイントのENIのIP(10.0.10.161と10.0.11.213)に解決されます。この状態でget_caller_identityが成功しました。SQSやDynamoDB、インターネットにはNAT経由で届いています。
NATのみ
STSのエンドポイントを削除して再実行すると、sts.ap-northeast-1.amazonaws.comはパブリックIPに解決され、get_caller_identityは成功しました。エンドポイントがなくても、NATがあれば届きます。
エンドポイントのみ(NATなし)
プライベートルートテーブルから0.0.0.0/0のルートを消し、STSのエンドポイントを作り直して実行しました。経路はローカルとS3ゲートウェイの2つだけです。
この構成でもワークフローはSUCCESSで完走し、STSにはエンドポイント経由で到達、XComも返ってきました。ドキュメントではプライベートルーティングの要件として、ワークフローが使うサービスのほかにApache Airflow用のVPCエンドポイントも挙げています。ただしServerless向けのエンドポイントサービス名は公開されておらず、作らないまま状態更新とXComの返却は完了しました。現状では不要と思われます。
一方で、この実行だけCloudWatch Logsにタスクログのストリームが作られませんでした。実行は成功しているのに、ログがありません。CloudWatch Logsのインタフェースエンドポイントを追加して再実行すると、logs.ap-northeast-1.amazonaws.comがエンドポイントのIPに解決されます。この場合ではログのストリームが作られました。2回の結果から、タスクログを出すには自前VPCからCloudWatch Logsへ到達する経路がいると判断できます。
今回検証したNATなしの構成で用意したのは、S3のゲートウェイエンドポイント、CloudWatch Logsのインタフェースエンドポイント、コードから呼ぶサービス(STS)のエンドポイントの3つです。ログが出ないだけでワークフローは成功するので、CloudWatch Logsのエンドポイントは忘れやすい点です。
その要件に、CloudWatch Logsは明記されていません。従来のMWAA(プロビジョニング型)のドキュメントは、必須エンドポイントの一覧にcom.amazonaws.<region>.logsを挙げています。タスクログを出すことも「ワークフローが使うサービス」に含まれる、と読んでおきます。
NATかインタフェースエンドポイントか
VPCを渡すと決めたあと、経路をNATゲートウェイにするかインタフェースエンドポイント(PrivateLink)にするかは、呼び先とコストで決まります。
判断の順序は次のとおりです。
- VPCなしで必要な呼び先に到達できるなら、VPCは渡さない(追加コストなし)
- インターネットアクセスが必要、または呼び先がVPCエンドポイントに対応していないなら、NATゲートウェイ
- 呼び先が固定され、すべてVPCエンドポイントに対応しているなら、インタフェースエンドポイント(CloudWatch Logsのエンドポイントも含める)
- 処理データ量が多いなら、時間料金とデータ処理料金の両方で比較する
料金は東京リージョンの値で比べてみます。月額は730時間とし、処理データ量に応じた料金と通常のデータ転送料金を除いた概算です。
| 経路 | 時間課金 | データ処理課金 | 月額の目安 |
|---|---|---|---|
| インタフェースエンドポイント | 0.014 USD/時/AZ | 0.01 USD/GB | 約20 USD/サービス(2AZ) |
| NATゲートウェイ | 0.062 USD/時/台 | 0.062 USD/GB | 約90 USD(2台)、約45 USD(1台) |
| S3ゲートウェイエンドポイント | 課金なし | 課金なし | 0 USD |
インタフェースエンドポイントは、STSとCloudWatch Logsの2サービスで約41 USD/月、3サービスで約61 USD/月です。ドキュメントどおりNATを2台置く構成(約90 USD/月)との損益分岐は、インタフェースエンドポイントを使うサービス数が4〜5の間にあります。NAT1台(約45 USD/月)と比べるなら2〜3サービスの間ですが、NAT1台は2AZのエンドポイントとは可用性が変わってきます。
データ処理料金はインタフェースエンドポイントの方が安い(0.01 USD/GB対0.062 USD/GB)ので、対応サービスへの通信量が多いほどインタフェースエンドポイントが有利になります。
今回はドキュメントに沿って、インタフェースエンドポイントを2つのプライベートサブネットに関連付けました。
まとめ
- 今回確認した呼び先では、VPCなしでS3とCloudWatch Logsに到達し、STS、SQS、DynamoDB、インターネットには到達を確認できなかった
- VPCを渡すと、ワーカーは指定したプライベートサブネット内で動く。NAT経由とインタフェースエンドポイント経由のどちらでもSTSに届いた
- NATなしのプライベートルーティングでは、CloudWatch Logsのインタフェースエンドポイントがないとタスクログが残らない。ワークフローは成功するので気付きにくい
- 処理データ量を除くと、2AZのインタフェースエンドポイントは4〜5サービスでNATゲートウェイ2台と同程度になる。NAT1台との損益分岐は2〜3サービスの間だが、可用性が異なる
VPCなしで届く先について今回分かったのは、試した5サービスとインターネットの範囲だけです。タスクから呼ぶサービスへの到達性を一度試してから、自前VPCと経路を用意するか決めましょう。








