ALBのスティッキーセッションを検証する
目的
ECS on Fargateで稼働しているWEBアプリケーションにALBからトラフィックを通しています。
1つのセッションで1つのタスクストレージを使い続ける必要があり、ELBがECS on Fargateのタスクに向けてApplication-based cookieでセッションを固定しようと考えています。
今回、スティッキーセッションの検証方法や挙動で躓いたのでまとめを作成しました。
スティッキーセッションの仕組みは下記の記事を参考にさせていただきました。
前提
- アプリケーション側にはセッションCookie(APP_SESSION_ID)を設定しています。
- ALBのターゲットグループにスティッキーセッションを設定しています。
- Stickiness: On
- Stickiness type: Application-based cookie
- App cookie name: APP_SESSION_ID
スティッキーセッションの確認方法
確認方法は次の通りです。
普段は検証環境のタスクを1つしか立ち上げていないのでタスクを増やしてターゲットグループに3つのIPアドレスを分散させて検証します。
- ECSサービスにタスクを3つ立ち上げた状態にする
- 何度かアプリケーションにアクセスする
- ELBアクセスログをAthenaでクエリしてターゲットとクライアントが同一のIPであることを確認する
タスクを増やしてターゲットを分散する
各タスクのIPアドレス
task ID | localIP |
---|---|
46093b7b7... | 10.24.10.201 |
6a4b3f6eeb... | 10.24.13.208 |
8b96488516... | 10.24.12.41 |
この状態でアプリケーションに数回アクセスします。
アクセスログの調査
ELBログのクエリ方法
まず、Athenaのテーブル作成です。
クエリ料金を節約するためパーティションで本日のデータだけのテーブルを作成します。
テーブル作成のクエリ
CREATE EXTERNAL TABLE IF NOT EXISTS my_alb_logs_20240625 (
type string,
time string,
elb string,
client_ip string,
client_port int,
target_ip string,
target_port int,
request_processing_time double,
target_processing_time double,
response_processing_time double,
elb_status_code int,
target_status_code string,
received_bytes bigint,
sent_bytes bigint,
request_verb string,
request_url string,
request_proto string,
user_agent string,
ssl_cipher string,
ssl_protocol string,
target_group_arn string,
trace_id string,
domain_name string,
chosen_cert_arn string,
matched_rule_priority string,
request_creation_time string,
actions_executed string,
redirect_url string,
lambda_error_reason string,
target_port_list string,
target_status_code_list string,
classification string,
classification_reason string,
traceability_id string
)
PARTITIONED BY
(
day STRING
)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe'
WITH SERDEPROPERTIES (
'serialization.format' = '1',
'input.regex' =
'([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) (.*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-_]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^\s]+?)\" \"([^\s]+)\" \"([^ ]*)\" \"([^ ]*)\" ?([^ ]*)?( .*)?')
LOCATION 's3://cloudwatch-logs-123412341234/my-alb-ap-dev/AWSLogs/123412341234/elasticloadbalancing/ap-northeast-1/'
TBLPROPERTIES
(
"projection.enabled" = "true",
"projection.day.type" = "date",
"projection.day.range" = "2024/06/25,NOW",
"projection.day.format" = "yyyy/MM/dd",
"projection.day.interval" = "1",
"projection.day.interval.unit" = "DAYS",
"storage.location.template" = "s3://cloudwatch-logs-123412341234/my-alb-ap-dev/AWSLogs/123412341234/elasticloadbalancing/ap-northeast-1/${day}"
)
Athenaのクエリ
最低限の列だけクエリします。
今回はリクエストURLとターゲットコンテナのIP、クライアントIP、タイムスタンプだけでよいです。
SELECT request_url,target_port_list,client_ip,time FROM "default"."my_alb_logs_20240625";
結果
request_url | target_port_list | client_ip | time |
---|---|---|---|
https://my-app.example.com:443/api/BBBB.php | 10.24.10.201:8080 | 130.176.189.48 | 2024-06-25T08:57:29.117812Z |
https://my-app.example.com:443/api/AAAA.php | 10.24.13.208:8080 | 130.176.189.48 | 2024-06-25T08:57:29.024817Z |
https://my-app.example.com:443/CCCC.php | 10.24.12.41:8080 | 130.176.189.46 | 2024-06-25T08:57:28.718711Z |
https://my-app.example.com:443/api/BBBB.php | 10.24.12.41:8080 | 130.176.189.54 | 2024-06-25T08:57:27.359199Z |
https://my-app.example.com:443/api/EEEE.php | 10.24.12.41:8080 | 130.176.189.18 | 2024-06-25T08:57:27.275572Z |
https://my-app.example.com:443/DDDD.php | 10.24.13.208:8080 | 130.176.189.14 | 2024-06-25T08:57:26.938586Z |
https://my-app.example.com:443/api/AAAA.php | 10.24.10.201:8080 | 130.176.189.19 | 2024-06-25T08:57:25.517015Z |
https://my-app.example.com:443/api/BBBB.php | 10.24.10.201:8080 | 130.176.189.12 | 2024-06-25T08:57:25.482284Z |
IPアドレスの照合
アクセスした時間帯を調べてみたところ、会社のIP(1xx.xxx.xxx.xx2)の記録がないようです。
対象のIPを逆引きすると、CloudFrontのIPアドレスのようでした。
ALBはクライアントのIPではなくCloudFront側のIPをclient_ipとして受け取っているようです。
IPの逆引き結果
$ dig -x 130.176.189.14
; <<>> DiG 9.18.18-0ubuntu0.22.04.2-Ubuntu <<>> -x 130.176.189.14
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57896
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 5, ADDITIONAL: 7
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;14.189.176.130.in-addr.arpa. IN PTR
;; ANSWER SECTION:
14.189.176.130.in-addr.arpa. 82726 IN PTR server-130-176-189-14.nrt57.r.cloudfront.net.
Cloudfrontのログを確認
CloudFrontでクライアント側のIPからアクセスされたことを確認するため、AthenaでCloudFrontログのテーブルを作成します。
CloudFrontログテーブルの作成
CREATE EXTERNAL TABLE IF NOT EXISTS cf_logs_myapp (
`date` DATE,
time STRING,
x_edge_location STRING,
sc_bytes BIGINT,
c_ip STRING,
cs_method STRING,
cs_host STRING,
cs_uri_stem STRING,
sc_status INT,
cs_referrer STRING,
cs_user_agent STRING,
cs_uri_query STRING,
cs_cookie STRING,
x_edge_result_type STRING,
x_edge_request_id STRING,
x_host_header STRING,
cs_protocol STRING,
cs_bytes BIGINT,
time_taken FLOAT,
x_forwarded_for STRING,
ssl_protocol STRING,
ssl_cipher STRING,
x_edge_response_result_type STRING,
cs_protocol_version STRING,
fle_status STRING,
fle_encrypted_fields INT,
c_port INT,
time_to_first_byte FLOAT,
x_edge_detailed_result_type STRING,
sc_content_type STRING,
sc_content_len BIGINT,
sc_range_start BIGINT,
sc_range_end BIGINT
)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\t'
LOCATION 's3://cloudfront-logs-123412341234/cf-logs-myapp.com/'
TBLPROPERTIES ( 'skip.header.line.count'='2' )
クエリ
SELECT date, time, c_ip, cs_uri_stem, cs_referrer FROM "default"."cf_logs_myapp" WHERE "date" BETWEEN DATE '2024-06-24' AND DATE '2024-06-25';
結果
date | time | c_ip | cs_uri_stem |
---|---|---|---|
2024/6/25 | 8:57:29 | 1xx.xxx.xxx.xx2 | /api/AAAA.php |
2024/6/25 | 8:57:29 | 1xx.xxx.xxx.xx2 | /api/BBBB.php |
2024/6/25 | 8:57:28 | 1xx.xxx.xxx.xx2 | /CCCC.php |
2024/6/25 | 8:57:27 | 1xx.xxx.xxx.xx2 | /api/EEEE.php |
2024/6/25 | 8:57:27 | 1xx.xxx.xxx.xx2 | /api/BBBB.php |
2024/6/25 | 8:57:26 | 1xx.xxx.xxx.xx2 | /DDDD.php |
2024/6/25 | 8:57:25 | 1xx.xxx.xxx.xx2 | /CCCC.php |
2024/6/25 | 8:57:25 | 1xx.xxx.xxx.xx2 | /api/BBBB.php |
2024/6/25 | 8:57:25 | 1xx.xxx.xxx.xx2 | /api/AAAA.php |
CloudFrontとALBのログを結合
request_url | target_port_list | client_ip | time | request_url | target_port_list | client_ip | time |
---|---|---|---|---|---|---|---|
https://my-app.example.com:443/api/BBBB.php | 10.24.10.201:8080 | 130.176.189.48 | 2024-06-25T08:57:29.117812Z | https://my-app.example.com:443/api/BBBB.php | 10.24.10.201:8080 | 130.176.189.48 | 2024-06-25T08:57:29.117812Z |
https://my-app.example.com:443/api/AAAA.php | 10.24.13.208:8080 | 130.176.189.48 | 2024-06-25T08:57:29.024817Z | https://my-app.example.com:443/api/AAAA.php | 10.24.13.208:8080 | 130.176.189.48 | 2024-06-25T08:57:29.024817Z |
https://my-app.example.com:443/CCCC.php | 10.24.12.41:8080 | 130.176.189.46 | 2024-06-25T08:57:28.718711Z | https://my-app.example.com:443/CCCC.php | 10.24.12.41:8080 | 130.176.189.46 | 2024-06-25T08:57:28.718711Z |
https://my-app.example.com:443/api/BBBB.php | 10.24.12.41:8080 | 130.176.189.54 | 2024-06-25T08:57:27.359199Z | https://my-app.example.com:443/api/BBBB.php | 10.24.12.41:8080 | 130.176.189.54 | 2024-06-25T08:57:27.359199Z |
https://my-app.example.com:443/api/EEEE.php | 10.24.12.41:8080 | 130.176.189.18 | 2024-06-25T08:57:27.275572Z | https://my-app.example.com:443/api/EEEE.php | 10.24.12.41:8080 | 130.176.189.18 | 2024-06-25T08:57:27.275572Z |
https://my-app.example.com:443/DDDD.php | 10.24.13.208:8080 | 130.176.189.14 | 2024-06-25T08:57:26.938586Z | https://my-app.example.com:443/DDDD.php | 10.24.13.208:8080 | 130.176.189.14 | 2024-06-25T08:57:26.938586Z |
https://my-app.example.com:443/api/AAAA.php | 10.24.10.201:8080 | 130.176.189.19 | 2024-06-25T08:57:25.517015Z | https://my-app.example.com:443/api/AAAA.php | 10.24.10.201:8080 | 130.176.189.19 | 2024-06-25T08:57:25.517015Z |
同じc_ipでもtarget_port_listのIPは異なっているので、スティッキーセッションが有効になっていないことが分かりました。
ALBのトラブルシューティング
下記を参考にALBのCookieが機能しているか確認します。
トラブルシューティングのドキュメントのサンプルではCookieの値が出力されています
...
< Set-Cookie: PHPSESSID=k0qu6t4e35i4lgmsk78mj9k4a4; path=/
< Set-Cookie:
AWSALBAPP-0=438DC7A50C516D797550CF7DE2A7DBA19D6816D5E6FB20329CD6AEF2B40030B12FF2839757A60E2330136A2182D27D049FB9D887FBFE9E80FB0724130FB3A86A4B0BAC296FDEB9E943EC9272FF52F5A8AEF373DF33;PATH=/
...
アプリケーションのFQDNにcurlします。
Cookieがセットされていることをcurlで調べる
$ curl -vko /dev/null https://my-app.example.com/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying xx.xx.xx.xx:443...
* Connected to my-app.example.com(xx.xx.xx.xx) port 443 (#0)
~(SSLハンドシェイク)~
< HTTP/2 200
< content-type: text/html; charset=UTF-8
< content-length: 6538
< date: Wed, 26 Jun 2024 01:06:42 GMT
< server: Apache/2.4.59 (Debian)
< x-powered-by: PHP/8.1.29
< set-cookie: APP_SESSION_ID=4350937ea221ccf29299aa42076b03223d146363db8906d0948c529d22e57254; expires=Wed, 26-Jun-2024 02:06:42 GMT; Max-Age=3600; path=/; secure; HttpOnly
< vary: Accept-Encoding
< set-cookie: AWSALBAPP-0=AAAAAAAAAAAe5cDw1wDdEF70G2Eb+RVYPC2CBIDqn2874HEjJjJGl26uPHQDNtsPuQcnyq6WusGr5KD1mYlaGBK22n5nbnVZ1PHWYgklr/tVYZK6i9sx7W3mI/FLI466TbALqyOwQOBLA/E=; Expires=Wed, 03 Jul 2024 01:06:42 GMT; Path=/
< set-cookie: AWSALBAPP-1=_remove_; Expires=Wed, 03 Jul 2024 01:06:42 GMT; Path=/
< set-cookie: AWSALBAPP-2=_remove_; Expires=Wed, 03 Jul 2024 01:06:42 GMT; Path=/
< set-cookie: AWSALBAPP-3=_remove_; Expires=Wed, 03 Jul 2024 01:06:42 GMT; Path=/
< x-cache: Miss from cloudfront
< via: 1.1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)
< x-amz-cf-pop: NRT57-P1
< x-amz-cf-id: ZanzZl9urOZDw66__1Aj7wbjZZ37Hjf2vcIHIgmHpkwucUovNNVKFQ==
<
{ [6538 bytes data]
100 6538 100 6538 0 0 22284 0 --:--:-- --:--:-- --:--:-- 22313
* Connection #0 to host my-app.example.com left intact
結果: APP_SESSION_ID
< set-cookie: APP_SESSION_ID=4350937ea221ccf29299aa42076b03223d146363db8906d0948c529d22e57254; expires=Wed, 26-Jun-2024 02:06:42 GMT; Max-Age=3600; path=/; secure; HttpOnly
結果: AWSALBAPP
< set-cookie: AWSALBAPP-0=AAAAAAAAAAAe5cDw1wDdEF70G2Eb+RVYPC2CBIDqn2874HEjJjJGl26uPHQDNtsPuQcnyq6WusGr5KD1mYlaGBK22n5nbnVZ1PHWYgklr/tVYZK6i9sx7W3mI/FLI466TbALqyOwQOBLA/E=; Expires=Wed, 03 Jul 2024 01:06:42 GMT; Path=/
Cookie自体はセットされており問題はなさそうです。
ALB設定の見直し
こちらのドキュメントに気になる項目がありました。
考慮事項
- クロスゾーン負荷分散がオフの場合、ターゲットに対するスティッキーなセッションはサポートされません。
ターゲットグループの設定を確認すると、クロスゾーン負荷分散はロードバランサーからの継承となっています。
Onに変更します。
再検証
クロスゾーン負荷分散を有効に変更して再度検証します。
各タスクのIPアドレス
task ID | localIP |
---|---|
36eb0d8... | 10.24.10.70 |
aaaf633f... | 10.24.13.134 |
a63b577... | 10.24.8.206 |
結果
date | time | c_ip | cs_uri_stem | request_url | target_port_list | client_ip | time |
---|---|---|---|---|---|---|---|
2024/6/26 | 2:09:21 | 1xx.xx.xxx.xx2 | /FFFF.php | https://my-app.example.com:443/FFFF.php?id=19&report_format_id=4 | 10.24.10.70:8080 | 130.176.189.40 | 2024-06-26T02:09:21.785955Z |
2024/6/26 | 2:09:19 | 1xx.xx.xxx.xx2 | /DDDD.php | https://my-app.example.com:443/CCCC.php | 10.24.10.70:8080 | 130.176.189.50 | 2024-06-26T02:09:19.400964Z |
2024/6/26 | 2:09:19 | 1xx.xx.xxx.xx2 | /api/BBBB.php | https://my-app.example.com:443/api/BBBB.php | 10.24.10.70:8080 | 130.176.189.14 | 2024-06-26T02:09:19.823591Z |
2024/6/26 | 2:09:19 | 1xx.xx.xxx.xx2 | /DDDD.php | https://my-app.example.com:443/api/report/report_search.php | 10.24.10.70:8080 | 130.176.189.48 | 2024-06-26T02:09:19.845695Z |
2024/6/26 | 2:09:13 | 1xx.xx.xxx.xx2 | /api/BBBB.php | https://my-app.example.com:443/api/BBBB.php | 10.24.10.70:8080 | 130.176.189.12 | 2024-06-26T02:09:13.100984Z |
2024/6/26 | 2:09:12 | 1xx.xx.xxx.xx2 | /CCCC.php | https://my-app.example.com:443/v2/project/project_management.php | 10.24.10.70:8080 | 130.176.189.4 | 2024-06-26T02:09:12.465696Z |
同じ時間帯にアクセスした一連のクライアント動作が同じtarget_port_listに向いていることが分かります。
まとめ
- ALBとCloudFrontのログを確認し、スティッキーセッションが有効になっていないことを確認しました。
- ALBの設定を見直し、クロスゾーン負荷分散をオンに変更しました。
- 再検証を行い、同じ時間帯にアクセスした一連の動作が同じターゲットに向いていることを確認しました。
- これにより、スティッキーセッションが有効になっていることが確認できました。
課題
今回はALBとCloudFrontのログから状態を確認しました。
Athenaからクエリしてログを確認する手間がかかるため、長期的な検証が必要な場合はタスク自身のローカルIPを表示させるようなロジックを組み込む方が良さそうです。
(※この後、ApacheのログにローカルIPを表示させるようにしました。)
実装して理解できたことですが、セッションを固定化するとロードバランシングの恩恵を損ねることにつながり、パフォーマンスの問題を発生させる原因にもなりかねません。
EFSを使用する前の暫定措置でしたが、特別な用途がなければスティッキーセッションを使用せずステートレス化したほうが良いでしょう。
参考: スティッキーセッションをやめ可用性・弾力性を高める
参考にさせていただいたドキュメントや記事など
AWS ALB スティッキーセッション メモ
Application Load Balancer のスティッキーセッション - Elastic Load Balancing
ターゲットグループに対するクロスゾーン負荷分散 - Elastic Load Balancing
Application Load Balancer のセッション維持機能のトラブルシューティング
スティッキーセッションをやめ可用性・弾力性を高める
スティッキーセッションを使っていなければApplication Load Balancer障害に耐えれたかも??? Amazon EC2をステートレスにする為にやるべきこと | DevelopersIO
Discussion