VPC Latticeのアップデート
re:Invent周辺で、VPC Lattice関連のアップデートが大きく3つ存在しました。
- VPC LatticeがECSをサポート開始
- EventBridgeとStep FunctionsがプライベートAPIとの統合を発表
- VPC Lattice に VPC リソースでの TCP サポートが追加
非常に興味を持ちつつも、そもそも自分はVPC Latticeが何なのかについてあまりキャッチアップできていません。
そこでAWSから提供されているVPC Latticeのワークショップを通して、理解を深めて行こうと思います!
ワークショップ開始準備
以下のワークショップを実施していきます。
実施にあたって、VPC Latticeで接続するために必要なアプリケーションをCloudFormationで構築します。
ワークショップ冒頭の「Self Hosted」においてテンプレートが用意されているので、それを自身の環境でデプロイしましょう。
↓
このCloudFormationテンプレートによって、「SuperApp」という架空の駐車場予約アプリで必要な、以下のリソースが作成されます。
- Payments VPCのアプリケーション ロード バランサーの背後にある EC2 インスタンスで実行されるPayments(支払い)サービス
- Rates VPC内の EC2 インスタンスで実行されるRates(料金)サービス
- Lambdaで実行されるReservation(予約)サービス
- クライアント VPCでのテストに使用するクライアントインスタンスのセット
すべてのVPCはCIDR 10.0.0.0/16
でデプロイされています。
VPC Latticeは、接続するVPCのIPアドレス範囲が重複していても柔軟に対応可能なことを体感できるワークショップとなっています。
今回のワークショップの目的は、「SuperApp」における上記サービス間の通信を簡素化し、必要なセキュリティ制御を実施し、SuperApp 内のサービス間のやり取りをエンドツーエンドで可視化することです。
したがって、実施手順はざっくり以下の3つです。
- Service-to-service connectivity(サービス間の接続)
- Application-layer security(アプリケーション層セキュリティ)
- Observability(可観測性)
1.Service-to-service connectivity
まずは、VPC LatticeとEC2・Lambdaを繋いでいきます。
ターゲットグループ・リスナーといったワードが出てくることから、最初は「ALBやELBのようなもの」として理解すると良さそうです。
ステップ1.ターゲットグループを作成する
ターゲットグループは、一言でいうとアプリケーション本体です。サービス提供部分、と置き換えてもOKです。
EC2, EKS, ALB, Lambda関数に加えて、ECSも紐づけることができます。
Reservation(Lambda)
ここで何をターゲットとして指定できるかが確認できます。
アップデートされたECSも記述されていますね。
Lambda関数をターゲットグループにする場合、イベント構造を選択できます。
JSON形式でイベントを送信し、すべてのリクエストにX-Forwarded-For
ヘッダーを追加します。
v2のイベント構造フォーマット例は以下です。
基本的にはこちらを使用することになると思います。
{
"version": "2.0",
"path": "/",
"method": "GET|POST|HEAD|...",
"headers": {
"header-key": ["header-value", ...],
...
},
"queryStringParameters": {
"key": ["value", ...]
},
"body": "request-body",
"isBase64Encoded": true|false,
"requestContext": {
"serviceNetworkArn": "arn:aws:vpc-lattice:region:123456789012:servicenetwork/sn-0bf3f2882e9cc805a",
"serviceArn": "arn:aws:vpc-lattice:region:123456789012:service/svc-0a40eebed65f8d69c",
"targetGroupArn": "arn:aws:vpc-lattice:region:123456789012:targetgroup/tg-6d0ecf831eec9f09",
"identity": {
"sourceVpcArn": "arn:aws:ec2:region:123456789012:vpc/vpc-0b8276c84697e7339",
"type": "AWS_IAM",
"principal": "arn:aws:iam::123456789012:assumed-role/my-role/my-session",
"principalOrgID": "o-50dc6c495c0c9188",
"sessionName": "i-0c7de02a688bde9f7",
"x509IssuerOu": "string",
"x509SanDns": "string",
"x509SanNameCn": "string",
"x509SanUri": "string",
"x509SubjectCn": "string"
},
"region": "region",
"timeEpoch": "1690497599177430"
}
}
v1のイベント構造フォーマット例は以下です。
{
"raw_path": "/path/to/resource",
"method": "GET|POST|HEAD|...",
"headers": {"header-key": "header-value", ... },
"query_string_parameters": {"key": "value", ...},
"body": "request-body",
"is_base64_encoded": true|false
}
rates(EC2)
続いて、料金サービスのEC2をターゲットとする設定を行います。
ここではプロトコルやVPCを選択します。サブネットは選択しません。
ここまで来たら「次へ」をクリックし、「ターゲットの登録」ページではそのまま「ターゲットグループの作成」をクリックします。
理由は、EC2インスタンスではなく、AutoScalingグループをターゲットグループに指定するからです。
先ほどスキップした、ターゲットの登録を行います。ここではまだ登録済みターゲットが存在しません。
EC2コンソールに移動し、料金用のAuto Scaling Groupを選択し、編集します。
※VPC Latticeコンソールではなく、EC2コンソールから設定します。
ここでVPC Lattice統合オプションがあるので、ここで先ほど作成したターゲットグループを選択します。
payments(EC2)
最後に支払い用サービスのターゲットグループを作成します。
ここではEC2の前段にあるALBを選択します。
プロトコルやVPCは先ほどの料金用ターゲットグループと同じように設定します。
ステップ2.サービスを作成する
続いて、VPC Latticeの本体とも言える、サービスの作成です。
ここでパスやルーティングの設定を行います。
以下2つのサービスを作成します。
- parking
→駐車場料金(/rate)と支払い(/payments)のターゲットを持つ - reservation
→予約(/reservation)ターゲットを持つ
parkingサービス
Lattice servicesからサービスを作成していきます。識別子は自由につけてOKです。
サービスアクセスに制限を設ける場合の設定も可能ですが、ここではスキップします。
そして、ルーティングを設定します。まずはリスナーの設定です。
どのプロトコル・どのポートでLatticeからアプリケーションへ接続できるかを定義します。
そしてリスナールールを作成します。
parkingサービスでは、/ratesと/paymentsという2つのターゲットグループを持つので、2つのルールを作成していきます。
デフォルトでどちらのターゲットグループに料金を送信できるか決められます。
ここでは料金用ターゲットグループを指定します。
reservation
ここでは、予約用Lambdaへのルーティングのみ行うため、ルールの作成は不要です。
代わりに、リスナーのデフォルトアクションとして先ほど作成したターゲットグループを設定しておきます。
↓
ステップ3.サービスネットワークを作成する
続いて、サービスネットワークを作成していきます。
サービスネットワーク作成
このサービスネットワークにサービスやリソースを関連付けることで、一元的なネットワークの管理が可能になります。
サービスをサービスネットワークに関連付ける
クライアントVPCをサービスネットワークに関連付ける
そしてサービスネットワークとクライアントVPCを紐づけます。
↓
ステップ4.接続テスト
これまででネットワーク周りの設定は完了しました。
続いては、きちんと接続できるかのテストを行っていきましょう。
まずはClient1のEC2インスタンスにセッションマネージャーで接続します。
以下コマンドを実行して、Lambdaで実装されている予約サービスへのアクセスを確認します。
curl https://<reservationサービスのドメイン名>
続いて、料金サービスへの接続を確認します。
// 料金サービスのデフォルトルーティングを確認する(料金サービスになっていればOK)
curl http://<parkingサービスのドメイン名>
// パスを指定し、料金サービスへのルーティングを確認する
curl http://<parkingサービスのドメイン名>/rates
最後に、支払いサービスへの接続を確認します。
curl http://<parkingサービスのドメイン名>/payments
各コマンド実行時に、上記のような表示がされていればOKです!
また、Client2でも同様の結果を得られます。
2.Application-layer security
続いて、VPC Latticeを用いてのセキュリティを実装していきます。
具体的には、以下の3層でセキュリティを確保します。
- サービスとVPCを、サービスネットワークに関連付ける
- サービスネットワークにおいて、ネットワークACLやセキュリティグループを実装する
- サービスネットワークとサービスにおいて、VPC Lattice認証ポリシーを設定する
ステップ1.認証情報をClient EC2インスタンスで設定
この後、VPC Latticeでのアクセスに認証情報を必須とします。
それにあたっての前準備として、Clientインスタンスで認証情報を登録します。
具体的には、AWS SigV4を使用してhttpリクエストに署名し、プログラムによるプロセスをシミュレートするPythonプログラムもダウンロードします。
Client EC2インスタンス1と2の両方で以下のコマンドを実行していきます。
// 必要なモジュールインストール
cd /home/ssm-user/
wget https://d3fh841oeihish.cloudfront.net/signSigV4.py
pip3 install botocore
pip3 install requests
sudo yum -y install jq
// EC2インスタンスが持つIAMロールのARNを取得し、環境変数に保存する
export AWS_ROLE_ARN=$(aws sts get-caller-identity | jq -r '.Arn')
export AWS_ROLE_NAME=$(aws sts get-caller-identity | jq -r '.Arn | split("/")[1]')
// 以下の出力を後続で使用するため、テキストエディタに保存しておく
echo $AWS_ROLE_ARN
echo $AWS_ROLE_NAME
// 資格情報を環境変数に設定する
AWS_SESSION_TOKEN=$(curl http://169.254.169.254/latest/meta-data/iam/security-credentials/${AWS_ROLE_NAME} | jq -r '.Token')
AWS_ACCESS_KEY_ID=$(curl http://169.254.169.254/latest/meta-data/iam/security-credentials/${AWS_ROLE_NAME} | jq -r '.AccessKeyId')
AWS_SECRET_ACCESS_KEY=$(curl http://169.254.169.254/latest/meta-data/iam/security-credentials/${AWS_ROLE_NAME} | jq -r '.SecretAccessKey')
先ほどインストールしたPythonプログラムですが、オレゴンリージョンがデフォルトで設定されています。
もし別のリージョンでワークショップを実施している場合は、viコマンドなどからガイルを編集し、リージョン設定を変更しておきましょう。
(私はバージニア北部リージョンで実施していました。)
(略)
def make_request(endpoint_url, method):
session = botocore.session.Session()
sigv4 = SigV4Auth(session.get_credentials(), 'vpc-lattice-svcs', 'us-east-1')
endpoint = endpoint_url
data = "some-data-here"
headers = {'Content-Type': 'application/json'}
(略)
ステップ2.サービスネットワークの保護
まずはサービスネットワークにおけるセキュリティの実装です。
先ほど作成したサービスネットワークの「アクセス」設定を編集していきます。
IAMで認証されたアクセスのみを許可するように設定します。
認証ポリシーは自動で入力されます。
ポリシーのCondition句で、「anonymousユーザーを除外する」というように設定してありますが、これが「認証されていないユーザーを除外する」という意味になっています。
"Condition": {
"StringNotEqualsIgnoreCase": {
"aws:PrincipalType": "anonymous"
}
}
その後、再びClient EC2インスタンスから接続確認をしてみます。
先程と同様のコマンドでアクセスしようとすると、拒否されるようになっています。
curl https://<reservationサービスのドメイン名>
curl http://<parkingサービスのドメイン名>/rates
curl http://<parkingサービスのドメイン名>/payments
そこで、以下のように認証付きでコマンドを実行してみます。
curl https://<reservationサービスのドメイン名> --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"
curl http://<parkingサービスのドメイン名>/rates --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"
curl http://<parkingサービスのドメイン名>/payments --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"
ステップ3.細かな認証ポリシーで個々のサービスを保護
先ほどはサービスネットワークにおいて、認証されているアクセスのみ許可するという、かなり粗めの認証ポリシーとなっていました。
ここでは各サービスにおいて、より細かな認証ポリシーを実装することでよりセキュアなネットワークにしていきましょう。
まずはparkingサービスの「アクセス」設定を編集していきます。
ここでは以下のポリシーを入力します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "<'LatticeWorkshop InstanceClient1' IAM ARN>"
},
"Action": "vpc-lattice-svcs:Invoke",
"Resource": "<ARN of Parking Service>/rates"
},
{
"Effect": "Allow",
"Principal": {
"AWS": "<'LatticeWorkshop InstanceClient2' IAM ARN>"
},
"Action": "vpc-lattice-svcs:Invoke",
"Resource": "<ARN of Parking Service>/payments",
"Condition": {
"StringEquals": {
"vpc-lattice-svcs:RequestMethod": "GET"
}
}
}
]
}
また、上記のポリシーでは、以下のような設定がされています。
クライアント1 | クライアント2 | |
---|---|---|
/rates | ○ | ✗ |
/payments (GETメソッド) | ✗ | ○ |
/payments (POSTなどその他) | ✗ | ✗ |
そして、再びEC2インスタンスからコマンドを実行します。
curl http://<parkingサービスのドメイン名>/rates --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"
curl http://<parkingサービスのドメイン名>/payments --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"
curl -d "hello=world" -X POST http://<parkingサービスのドメイン名>/payments --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"
クライアント1での実行は、以下のように/rates apiを呼び出すことができ、/payments apiへのアクセスは拒否されます。
クライアント2での実行は、以下のように/rates apiを呼び出すことができず、/payments apiのGETメソッドはコール可能で、POSTメソッドは拒否されます。
先ほどよりも細かく認証ポリシーを設定することができました!
ステップ4.AWS SigV4の使用
続いて、reservationサービスに対しても認証ポリシーを設定していきます。
ここでは、AWS SigV4を用いた認証を行います。
AWS SigV4(Signature Version 4)とは、AWS APIリクエストに認証情報を追加するためのAWS署名プロトコルです。
(細かいことを調べるとかなり深い沼だったので、詳細は別途纏めます。)
reservationサービスのアクセス設定を編集していきます。
サービスネットワークと同じように、認証されたアクセスのみを許可するように設定します。
Client EC2インスタンスに接続し、以下コマンドを実行して動作確認を行います。
cd /home/ssm-user
python3 signSigV4.py https://<reservationサービスのドメイン名> POST
python3 signSigV4.py https://<reservationサービスのドメイン名> GET
3.Observability(可観測性)
最後に、VPC Latticeからのログ出力についてです。
以下の3つがログ出力先として選択できます。
- CloudWatch Logs
- Data Firehose
- S3
まずはサービスネットワークでのログ出力設定です。
アクションからログ設定が編集できます。
この時、Service access logs
とResource access logs
の2つから選べます。
どちらもS3にログを出力してもらうように設定したのですが、結果的にはService access logs
しかログが出力されていませんでした。
こちらはService access logs
のみ設定可能でした。
CloudWatch Logsにログを出力してもらいます。
その後、以下コマンドをそれぞれのClientインスタンスで実行します。
cd /home/ssm-user
for ((i=1;i<=30;i++)); do curl http://<parkingサービスのドメイン名>/rates --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"; done
for ((i=1;i<=30;i++)); do curl http://<parkingサービスのドメイン名>/payments --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"; done
for ((i=1;i<=30;i++)); do python3 signSigV4.py https://<reservationサービスのドメイン名> POST; done
for ((i=1;i<=30;i++)); do python3 signSigV4.py https://<reservationサービスのドメイン名> GET; done
for ((i=1;i<=30;i++)); do curl https://<reservationサービスのドメイン名> --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"; done
cd /home/ssm-user
for ((i=1;i<=30;i++)); do curl http://<parkingサービスのドメイン名>/rates --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"; done
for ((i=1;i<=30;i++)); do curl <parkingサービスのドメイン名>/payments --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"; done
for ((i=1;i<=30;i++)); do curl <parkingサービスのドメイン名>/payments hello=world --aws-sigv4 "aws:amz:us-east-1:vpc-lattice-svcs" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" --header "x-amz-security-token:$AWS_SESSION_TOKEN" --header "x-amz-content-sha256:UNSIGNED-PAYLOAD"; done
上記コマンドでログが格納されたので、S3とCloudWatch Logsを確認しに行きます。
まずはS3です。
ResourceAccessLogは出力がありませんでした。
どんなログが出力されているのか確認してみましょう。
ワークショップの手順的にはS3 Selectでクエリを投げているのですが、S3 Selectは新規利用ができなくなっているので、別途Athenaで実施する必要があります。
試したい方向けに、Athena用のコマンドを掲載しておきます。
CREATE EXTERNAL TABLE vpc_lattice_logs (
serviceNetworkArn STRING,
resolvedUser STRING,
listenerType STRING,
authDeniedReason STRING,
targetGroupArn STRING,
sourceVpcArn STRING,
destinationVpcId STRING,
sourceIpPort STRING,
listenerId STRING,
targetIpPort STRING,
failureReason STRING,
serviceArn STRING,
sourceVpcId STRING,
startTime TIMESTAMP,
requestMethod STRING,
requestPath STRING,
protocol STRING,
responseCode INT,
bytesReceived BIGINT,
bytesSent BIGINT,
duration INT,
userAgent STRING,
hostHeader STRING,
serverNameIndication STRING,
requestToTargetDuration INT,
responseFromTargetDuration INT,
sslCipher STRING,
tlsVersion STRING,
callerPrincipal STRING,
callerPrincipalOrgID STRING,
callerX509IssuerOU STRING,
callerX509SubjectCN STRING,
callerX509SANNameCN STRING,
callerX509SANDNS STRING,
callerX509SANURI STRING
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://your-bucket-name/path-to-logs/';
↑location配下のS3パスは適宜書き換えてください。
続いて以下コマンドで、エンドポイントごとのアクセス数を集計します。
SELECT
requestPath,
requestMethod,
COUNT(*) as request_count,
AVG(duration) as avg_duration_ms
FROM vpc_lattice_logs
GROUP BY requestPath, requestMethod
ORDER BY request_count DESC;
以下のように、エンドポイントとメソッドごとに纏めてくれるはずです!
最後に、CloudWatch Logsも確認しておきましょう。
コンソールのロググループから確認できます。
許可されていないアクセスのログはauthDeniedReason
が記載されています。
まとめ
VPC Latticeの概念を理解するのに、取り組みやすいハンズオンとなっていました!
かなり有用なサービスだと認識しているので、引き続きキャッチアップしていこうと思います。
個人的に疑問なのは、案件や模擬試験でたまに見る以下のような構成もVPC Latticeを使えば楽につかえるようになったりするのかな?というところです。
技術的には可能そうですが、細かい要件を満たせるのかがチョット不明です。
この部分については自分で触って試してみようと思います!