Why aws-sdk-js-v2 get sts assume-role token so slow in k8s node
Posted on October 10, 2022 • 4 minutes • 849 words
前言: 工作上遇到的問題,花點時間解決,並解記錄下來。
為什麼我的 node application 在 k8s 內部執行時,執行以下 function 時如此的慢,我在地端測試都沒有這個問題??
開門見山直接看有問題的 code.
aws-sdk-js
version:2.1026.0
const sts = new AWS.STS({
endpoint: 'https://sts.us-east-1.amazonaws.com ',
region: 'us-east-1',
stsRegionalEndpoints: 'regional',
});
const credentials = await sts
.assumeRole({
RoleArn: roleArn,
RoleSessionName: 'RoleSessionName',
})
.promise();
可以看到光是執行 assumeRole()
就花費了 4.668s
的時間!!
經過另一個方式用測試,採用直接塞 AKSK(ACCESS-KEY
)(SECRET-ACCESS-KEY
)的方式去執行 assumeRole()
以下為示意code:
const awsSdk = require('aws-sdk');
const stsClient = new awsSdk.STS({
endpoint: 'https://sts.us-east-1.amazonaws.com/ ',
region: 'us-east-1',
stsRegionalEndpoints: "regional",
accessKeyId: 'ACCESS-KEY', // <- AK
secretAccessKey: 'SECRET-ACCESS-KEY' <-SK
});
const credentials = stsClient.assumeRole({
RoleArn: 'ROLE-ARN',
RoleSessionName: 'SESSION-NAME'
}).promise();
credentials.then((value) => {
console.log('Value: ', value);
}).catch((reason) => {
console.log('Reason: ', reason);
});
[AWS sts 200 0.037s 0 retries] assumeRole({
RoleArn: 'ROLE-ARN',
RoleSessionName: 'SESSION-NAME'
})
[AWS sts 200 2.589s 0 retries] assumeRole({
RoleArn: 'ROLE-ARN',
RoleSessionName: 'SESSION-NAME'
})
[AWS sts 200 0.055s 0 retries] assumeRole({
RoleArn: 'ROLE-ARN',
RoleSessionName: 'SESSION-NAME'
})
可以看到直結解果有明顯縮短,到 2.589s
甚至是 0.055s
這又是為什麼呢?
使用 AWS 時,常常 Application 會需要跟其他 AWS Service 進行互動 比如說最常見的 DynamoDB, S3 etc…
而要與這些服務互動,除了常見的 aws console 登陸後,你的 IAM User 有 attach 特定的 policy 可以讓你透過console 與資源去做互動,而如果開發人員在開發應用時,如果應用要與 AWS 其他的 Service 互動,
勢必要獲得授權,而最簡單獲得授權的方式,就是在 指定的 IAM User 的 security credentials
創建一個 access key
。
這就是所謂的 AWS Access Key
AWS Secret Access Key
俗稱 AKSK
,直接用 AKSK
這個雖然方便,但越是方便的東西越是帶著風險,如果不小心有一天,你的 AKSK
不小信 hardcode 寫在你的 source code ,又不小心 publish 到像是 Github, Gitlab 的 public repo ,都有可能會造成你的 AWS Resource 被有心人士惡意竄改,或是亂開 EC2 來挖礦,你的錢包可以就會傷心了…,所以建議 AKSK 只建議在本機開發使用,千萬不要 hardcode
在 source code 內,建議使用 AWS_PROFILE
來進行管理,基本上 AWS SDK 各語言都可以透過 AWS_PROFILE
Named profiles for the AWS CLI - AWS Command Line Interface
來進行授權吃到開發環境的,aws credentials 設定檔 $HOME/.aws/=
在正式環境中,我們使用 AWS EC2 or 其他 Compute Service ,都可以透過該服務的 IAM Role 來授權換取臨時性的 Credential
,而這次的事件也是由這個原因引起的。
先來看一下 Nodejs 應用如何取得 Credential
https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html
Here are the ways you can supply your credentials in order of recommendation:
Loaded from AWS Identity and Access Management (IAM) roles for Amazon EC2
Loaded from the shared credentials file (~/.aws/credentials)
Loaded from environment variables
可以從文件中得知,預設如果是 AWS EC2 環境的話,會先使用 EC2 IAM Role 來獲得 Credential
,再來嘗試 loading ~/.aws/credentials 的檔案,再來才是環境變數,但如果你是直接 hardcode
AKSK
在 source code ,當然會直接用 hardcode
內的 AKSK
,所以我們再回到此次的問題,hardcode AKSK 速度極快,為什麼改用 EC2 role 就很慢呢,可以在
此 repo 中找到此 issue,原來在 aws-sdk version v2.575.0
之後,預設先嘗試走 IMDSv2
流程進行拿取 instance metadata
的 token,這樣做是為了加強安全性,IMDSv2
更新的流程需要在能夠調用任何元 metadata endpoint 之前獲得令牌。
那麼 IMDSv2
又是什麼呢 ?我們可以從 EC2 的文件中得知:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
How Instance Metadata Service Version 2 (IMDSv2) works
IMDSv2 使用 session-oriented requests
。對於 session-oriented requests
,您可以創建一個 session token
來定義 session 持續時間,該持續時間最短為一秒,最長為六小時(21600s)。在指定的持續時間內,您可以將相同的 session token
用於後續請求。在指定的持續時間到期後,您必須創建一個新的 session token
以用於未來的請求。
以下為請求 session token
的範例,需要在 EC2 執行:
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
拿到 TOKEN
後就可以透過他去存取 metadata endpoint
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
-v http://169.254.169.254/latest/meta-data/
回到這次的事件
為什麼透過 IMDSv2
拿取的速度這麼慢勒,原因為此環境為 k8s 的 node,也就是 application 會是以 Container ( pod ) 方式跑在此 EC2 中,
然而當前的 k8s 使用的網路,如果沒有指定 kubelet
網絡插件,則使用 noop
插件,它設置 net/bridge/bridge-nf-call-iptables=1
以確保簡單的配置(例如帶有 bridge
的 Docker
)與 iptables
代理一起正常工作。
Network Plugins: https://v1-20.docs.kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/ 所以我們來大概看一下 Docker Bridge 的架構,containers 都可以透過 docker0 到 EC2 的eth0 網卡,與外界聯絡。 https://argus-sec.com/docker-networking-behind-the-scenes/
當前環境的 k8s cluster
那麼用 Docker bridge 的模式,為什麼會影響,aws-sdk 存取 IMDSv2 這麼慢呢?因為對於 JavaScript SDK 在獲取 EC2 role 的行為上,首先會使用 MetaData Version 2 (IMDSv2), 當 IMDSv2 無法回應時經過幾次重試,最終使用 IMDSv1 獲取權限。基於您的網路架構,當 Container 要訪問 IMDSv2 時,路徑如下:
[Container] –> [bridge] –> [Instance] –> [IMDSv2]
而訪問 IMDSv2 timeout 的原因是,預設使用 put 請求,拿取 session token
時,網路層的 hop Hop networking
limit 是 1,而我們 container 的環境是透過 docker bridge 的方式與外界聯絡與 (IMDSv2),也就是 2 hop ,超過限制時 IMDSv2 的 endpoint 將會拒絕回應,所以造成 timeout。
By default, the response to PUT requests has a response hop limit (time to live) of 1 at the IP protocol level. You can adjust the hop limit using the modify-instance-metadata-options command if you need to make it larger. For example, you might need a larger hop limit for backward compatibility with container services running on the instance. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
Demo
- Launch EC2 InstanceMetadataOptions.HttpPutResponseHopLimit setting to
1
andMetadata version
useV1 and V2(token optional)
- Connect to Instance
- Request a token from IMDSv2 endpoint
curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"
- Modify EC2 Instance
InstanceMetadataOptions.HttpPutResponseHopLimit
to2
aws ec2 modify-instance-metadata-options --instance-id i-xxxxxxxxxxxxx --http-put-response-hop-limit 2 --region ap-northeast-1
{
"InstanceId": "i-xxxxxxxxxxxxx",
"InstanceMetadataOptions": {
"State": "pending",
"HttpTokens": "optional",
"HttpPutResponseHopLimit": 2,
"HttpEndpoint": "enabled",
"HttpProtocolIpv6": "disabled",
"InstanceMetadataTags": "disabled"
}
}
Demo 2
Modify instance metadata http-put-response-hop-limit
to 1
aws ec2 modify-instance-metadata-options --http-put-response-hop-limit 1 --instance-id i-xxxxxxxxxxxxx --region ap-northeast-1
{
"InstanceId": "i-0d7b8caf055b2843a",
"InstanceMetadataOptions": {
"State": "pending",
"HttpTokens": "required",
"HttpPutResponseHopLimit": 1,
"HttpEndpoint": "enabled",
"HttpProtocolIpv6": "disabled",
"InstanceMetadataTags": "disabled"
}
}
Try run container with host network…
- Request
http://169.254.169.254/latest/api/token
Get timeout with not use host network. - Request
http://169.254.169.254/latest/api/token
Not get timeout with use host network.
所以改善此 issue 的方法就是有幾種:
-
強制 SDK 使用 IMDSv1 的方式存取 metadata endpoint (不建議比較不安全,往後的 instance 應該也會淘汰v1 的方式去存取metadata endpoint )。
-
透過調整 response hop limits 的上限,可以透過 awscli modify-instance-metadata-options將上限調整到 2 以上,或是 Launch Template or Launch Configuration 預設在起動 EC2 時進行調整。
-
使用 VPC-CNI: Amazon EKS 會透過 Kubernetes 專用 Amazon VPC 容器網路介面 (CNI) 外掛程式支援原生 VPC 聯網。使用此外掛程式可讓 Kubernetes Pod 擁有與他們在 VPC 網路上 Pod 內相同的 IP 地址,這樣也解決 response hop limit 的問題。
-
使用 IRSA 給予 application 去取得權限與 AWS Service 進行互動。
-
使用 pod-identities 透過 AWS IAM Role 給予 Pod 權限。 (需要注意一下 support 的 sdk version https://docs.aws.amazon.com/eks/latest/userguide/pod-id-minimum-sdk.html )
將 IAM 角色與 Kubernetes 服務帳戶建立關聯。然後,此服務帳戶可以為使用該服務帳戶之任何 Pod 中的容器提供 AWS 許可。
Other links:
- Use IMDSv2 - Use IMDSv2 - Amazon Elastic Compute Cloud: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
- aws-sdk-js/metadata_service.js at v2.1026.0 · aws/aws-sdk-js: https://github.com/aws/aws-sdk-js/blob/v2.1026.0/lib/metadata_service.js#L123
- amazon-ecs-agent/config.go at master · aws/amazon-ecs-agent: https://github.com/aws/amazon-ecs-agent/blob/master/agent/config/config.go#L129
- 擷取執行個體中繼資料 - 查詢調節 - https://docs.aws.amazon.com/zh_tw/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#instancedata-throttling
- AWS 中的錯誤重試與指數退避 - AWS 中的錯誤重試與指數退避 - AWS 一般參考: https://docs.aws.amazon.com/zh_tw/general/latest/gr/api-retries.html
- https://aws.amazon.com/tw/blogs/security/get-the-full-benefits-of-imdsv2-and-disable-imdsv1-across-your-aws-infrastructure/