🦎

ALBのスティッキーセッションを検証する

2024/06/26に公開

目的

ECS on Fargateで稼働しているWEBアプリケーションにALBからトラフィックを通しています。

1つのセッションで1つのタスクストレージを使い続ける必要があり、ELBがECS on Fargateのタスクに向けてApplication-based cookieでセッションを固定しようと考えています。
今回、スティッキーセッションの検証方法や挙動で躓いたのでまとめを作成しました。

スティッキーセッションの仕組みは下記の記事を参考にさせていただきました。

https://qiita.com/KWS_0901/items/de9487aa041b2718f113

前提

  • アプリケーション側にはセッション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/notification/unread_unreact_count.php 10.24.10.201:8080 130.176.189.48 2024-06-25T08:57:29.117812Z
https://my-app.example.com:443/api/workflow/apply_search.php 10.24.13.208:8080 130.176.189.48 2024-06-25T08:57:29.024817Z
https://my-app.example.com:443/v2/workflow/workflow.php 10.24.12.41:8080 130.176.189.46 2024-06-25T08:57:28.718711Z
https://my-app.example.com:443/api/notification/unread_unreact_count.php 10.24.12.41:8080 130.176.189.54 2024-06-25T08:57:27.359199Z
https://my-app.example.com:443/api/admin/user_search.php 10.24.12.41:8080 130.176.189.18 2024-06-25T08:57:27.275572Z
https://my-app.example.com:443/v2/kintai/view_v3.php 10.24.13.208:8080 130.176.189.14 2024-06-25T08:57:26.938586Z
https://my-app.example.com:443/api/workflow/apply_search.php 10.24.10.201:8080 130.176.189.19 2024-06-25T08:57:25.517015Z
https://my-app.example.com:443/api/notification/unread_unreact_count.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/workflow/apply_search.php
2024/6/25 8:57:29 1xx.xxx.xxx.xx2 /api/notification/unread_unreact_count.php
2024/6/25 8:57:28 1xx.xxx.xxx.xx2 /v2/workflow/workflow.php
2024/6/25 8:57:27 1xx.xxx.xxx.xx2 /api/admin/user_search.php
2024/6/25 8:57:27 1xx.xxx.xxx.xx2 /api/notification/unread_unreact_count.php
2024/6/25 8:57:26 1xx.xxx.xxx.xx2 /v2/kintai/view_v3.php
2024/6/25 8:57:25 1xx.xxx.xxx.xx2 /v2/workflow/workflow.php
2024/6/25 8:57:25 1xx.xxx.xxx.xx2 /api/notification/unread_unreact_count.php
2024/6/25 8:57:25 1xx.xxx.xxx.xx2 /api/workflow/apply_search.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/notification/unread_unreact_count.php 10.24.10.201:8080 130.176.189.48 2024-06-25T08:57:29.117812Z https://my-app.example.com:443/api/notification/unread_unreact_count.php 10.24.10.201:8080 130.176.189.48 2024-06-25T08:57:29.117812Z
https://my-app.example.com:443/api/workflow/apply_search.php 10.24.13.208:8080 130.176.189.48 2024-06-25T08:57:29.024817Z https://my-app.example.com:443/api/workflow/apply_search.php 10.24.13.208:8080 130.176.189.48 2024-06-25T08:57:29.024817Z
https://my-app.example.com:443/v2/workflow/workflow.php 10.24.12.41:8080 130.176.189.46 2024-06-25T08:57:28.718711Z https://my-app.example.com:443/v2/workflow/workflow.php 10.24.12.41:8080 130.176.189.46 2024-06-25T08:57:28.718711Z
https://my-app.example.com:443/api/notification/unread_unreact_count.php 10.24.12.41:8080 130.176.189.54 2024-06-25T08:57:27.359199Z https://my-app.example.com:443/api/notification/unread_unreact_count.php 10.24.12.41:8080 130.176.189.54 2024-06-25T08:57:27.359199Z
https://my-app.example.com:443/api/admin/user_search.php 10.24.12.41:8080 130.176.189.18 2024-06-25T08:57:27.275572Z https://my-app.example.com:443/api/admin/user_search.php 10.24.12.41:8080 130.176.189.18 2024-06-25T08:57:27.275572Z
https://my-app.example.com:443/v2/kintai/view_v3.php 10.24.13.208:8080 130.176.189.14 2024-06-25T08:57:26.938586Z https://my-app.example.com:443/v2/kintai/view_v3.php 10.24.13.208:8080 130.176.189.14 2024-06-25T08:57:26.938586Z
https://my-app.example.com:443/api/workflow/apply_search.php 10.24.10.201:8080 130.176.189.19 2024-06-25T08:57:25.517015Z https://my-app.example.com:443/api/workflow/apply_search.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が機能しているか確認します。

https://repost.aws/ja/knowledge-center/elb-alb-stickiness

トラブルシューティングのドキュメントのサンプルでは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設定の見直し

こちらのドキュメントに気になる項目がありました。

https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/disable-cross-zone.html

考慮事項

  • クロスゾーン負荷分散がオフの場合、ターゲットに対するスティッキーなセッションはサポートされません。

ターゲットグループの設定を確認すると、クロスゾーン負荷分散はロードバランサーからの継承となっています。


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 /v2/report/report_write_edit.php https://my-app.example.com:443/v2/report/report_write_edit.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 /v2/report/report_write_list.php https://my-app.example.com:443/v2/report/report_write_list.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/notification/unread_unreact_count.php https://my-app.example.com:443/api/notification/unread_unreact_count.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 /api/report/report_search.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/notification/unread_unreact_count.php https://my-app.example.com:443/api/notification/unread_unreact_count.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 /v2/project/project_management.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を使用する前の暫定措置でしたが、特別な用途がなければスティッキーセッションを使用せずステートレス化したほうが良いでしょう。

参考: スティッキーセッションをやめ可用性・弾力性を高める
https://note.com/kurashicom_tech/n/n7f64ce704300

参考にさせていただいたドキュメントや記事など

AWS ALB スティッキーセッション メモ
Application Load Balancer のスティッキーセッション - Elastic Load Balancing
ターゲットグループに対するクロスゾーン負荷分散 - Elastic Load Balancing
Application Load Balancer のセッション維持機能のトラブルシューティング
スティッキーセッションをやめ可用性・弾力性を高める
スティッキーセッションを使っていなければApplication Load Balancer障害に耐えれたかも??? Amazon EC2をステートレスにする為にやるべきこと | DevelopersIO

Discussion