aws lambda 在lambda环境中访问eks集群
参考资料
- https://github.com/kubernetes-client/python
现象
直接load集群的config文件作为凭证,会出现以下报错
HTTPSConnectionPool(host='c9611e71a15ac11de8cf33921d4bc09b.yl4.cn-north-1.eks.amazonaws.com.cn', port=443): Max retries exceeded with url: /api/v1/pods?watch=False (Caused by SSLError(SSLError(9, '[X509] PEM lib (_ssl.c:4140)')))",
原因是没有拿到token,进一步发现以下问题
[Errno 2] No such file or directory: 'aws': 'aws'
由于kubectl访问eks集群的kubeconfig中,是用过awscli工具获取token
users:
- name: arn:aws-cn:eks:cn-north-1:xxxxxxxx:cluster/xxxxx
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
args:
- --region
- cn-north-1
- eks
- get-token
- --cluster-name
- xxxxx
command: aws
但是,默认的lambda环境中没有aws的二进制命令行工具。解决此问题有两个办法,
aws的两种安装方式有区别
-
二进制安装需要依赖一些文件
$ ./aws s3 ls [10208] Error loading Python lib '/home/ec2-user/libpython3.9.so.1.0': dlopen: /home/ec2-user/libpython3.9.so.1.0: cannot open shared object file: No such file or directory -
pip安装会有一个入口文件,需要修改
shebang为lambda环境中的解释器路径#!/var/lang/bin/python import sys import os if os.environ.get('LC_CTYPE', '') == 'UTF-8': os.environ['LC_CTYPE'] = 'en_US.UTF-8' import awscli.clidriver def main(): return awscli.clidriver.main() if __name__ == '__main__': sys.exit(main())
思路
通过layer扩展函数
lambda环境中的非预留环境变量包括
PATH– 执行路径 (/usr/local/bin:/usr/bin/:/bin:/opt/bin)LD_LIBRARY_PATH– 系统库路径 (/lib64:/usr/lib64:$LAMBDA_RUNTIME_DIR:$LAMBDA_RUNTIME_DIR/lib:$LAMBDA_TASK_ROOT:$LAMBDA_TASK_ROOT/lib:/opt/lib)
这些环境变量可以在函数配置中扩展
lambda函数环境中重要的路径有以下
- 在函数中包含图层时,内容将提取到执行环境中的
/opt目录中 - 可执行命令的路径,
/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin - 可写的
tmp目录
在python环境中获取/var/lang/bin目录文件如下
easy_install
easy_install-3.7
openssl
pip
pip3
pip3.7
pydoc
pydoc3
pydoc3.7
python
python3
python3.7
python3.7-config
python3.7m
python3.7m-config
python3-config
python-config
pyvenv
pyvenv-3.7
通过layer为函数增加awscli的功能,只需要将以上文件打包为bin文件夹即可,因为需要指定默认的可执行文件路径/opt/bin
压缩文件路径如下
$ ll bin/
total 56
-rwxrwxr-x 1 ec2-user ec2-user 819 Feb 17 07:05 aws
drwxrwxr-x 8 ec2-user ec2-user 4096 Feb 17 07:03 awscli
drwxrwxr-x 2 ec2-user ec2-user 123 Feb 17 07:03 awscli-1.27.73.dist-info
drwxrwxr-x 3 ec2-user ec2-user 4096 Feb 17 07:03 bin
drwxrwxr-x 8 ec2-user ec2-user 4096 Feb 17 07:03 botocore
drwxrwxr-x 2 ec2-user ec2-user 120 Feb 17 07:03 botocore-1.29.73.dist-info
...
可以通过aws命令更新环境中的kubeconfig配置(写到tmp目录),最终的代码如下
import json
import os
from kubernetes import client, config
def lambda_handler(event, context):
print(os.system("aws sts get-caller-identity"))
print(os.system("aws eks update-kubeconfig --name test124 --kubeconfig /tmp/kubeconfig"))
config.load_kube_config("/tmp/kubeconfig")
v1 = client.CoreV1Api()
print("Listing pods with their IPs:")
ret = v1.list_pod_for_all_namespaces(watch=False)
for i in ret.items:
print("%s\t%s\t%s" % (i.status.pod_ip, i.metadata.namespace, i.metadata.name))
return {
'statusCode': 200,
'body': json.dumps('Hello from Lambda!')
}
注意:由于lambda函数使用lambda执行角色访问eks集群,因此需要在集群中的aws-auth中授权
由于eks集群不识别role中的path,因此需要将role中的servicerole path去除才能被集群识别
mapRoles: |
- groups:
- system:masters
rolearn: arn:aws-cn:iam::xxxxxxx:role/test-func-py37-role-ema8fplj
由于aws命令执行时间较长,需要将lambda函数的基础配置超时时间和内存都调整为较大值
签发sigv4请求
手动签发token
由于sdk没有提供直接获取yoken的方式,这种方式通过手动向sts服务签发sigv4获取tokne,然后使用这个token访问apiserver
先来看一下awscli如何获取eks的token,eks签发的请求如下,说明向sts.cn-north-1.amazonaws.com.cn发送请求,请求方法居然是GetCallerIdentity
2023-02-17 14:42:41,491 - MainThread - botocore.auth - DEBUG - CanonicalRequest:
GET
/
Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAQRIBWRJKNSX3ID6O%2F20230217%2Fcn-north-1%2Fsts%2Faws4_request&X-Amz-Date=20230217T144241Z&X-Amz-Expires=60&X-Amz-Security-Token=FwoDYXxxxxxxis5I%2FIZgYqGL4wX6PU21mdGcLOD%2BfbjL7lS4rFNNXo%2FD7HroWIPYF1vqe4aSE4afaC9p2MVZ8s755%2B52%2FKWjgXvRNcDjRnz4boIZI2tlxsFbgtPTLerg50p1ys0O88CGs9XIGm%2B4H35cn%2BNeKTrJklNQd%2BImUWbQ6jOygH3OlMcnM7s2%2BOP9vVopRyJUccJFfOWGPyG%2BO5oX0dcMs58K4ULhSWv3rM5MME0bSFxmzZouDgX%2Bhkr5TwbXf5JMVecXhbVEsF5YdfkJkkSarfd%2FsRlN8ih%2B8AKV4og4BTRG7EXU8W9FOFI44F9fdiX8cGgIDUTbDtMo05m%2BnwYyOVd7AljDMRbH%2Fsueny979luPzCQ9REZ6JgMvN9cV6tE6kwXCXy1DCalepDMz2agSCJUl2YetE1qBIA%3D%3D&X-Amz-SignedHeaders=host%3Bx-k8s-aws-id
host:sts.cn-north-1.amazonaws.com.cn
x-k8s-aws-id:test124
host;x-k8s-aws-id
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
以下是使用sdk获取session凭证签发的请求头示例
https://github.com/aws-samples/sigv4a-signing-examples
import boto3
from botocore import crt, awsrequest
class SigV4ASign:
def __init__(self, boto3_session=boto3.Session()):
self.session = boto3_session
def get_headers(self, service, region, aws_request_config):
sigV4A = crt.auth.CrtS3SigV4AsymAuth(self.session.get_credentials(), service, region)
request = awsrequest.AWSRequest(**aws_request_config)
sigV4A.add_auth(request)
prepped = request.prepare()
return prepped.headers
def get_headers_basic(self, service, region, method, url):
sigV4A = crt.auth.CrtS3SigV4AsymAuth(self.session.get_credentials(), service, region)
request = awsrequest.AWSRequest(method=method, url=url)
sigV4A.add_auth(request)
prepped = request.prepare()
return prepped.headers
service = 'sts'
region = 'cn-north-1'
method = 'GET'
url = 'https://sts.cn-north-1.amazonaws.com.cn'
headers = SigV4ASign().get_headers_basic(service, region, method, url)
print(headers)
输出结果如下,即构造了标注的sigv4请求头:
{
"host": "sts.cn-north-1.amazonaws.com.cn",
"X-Amz-Security-Token": "FwoDYXdzEPf//wEaDK0QxxxxKV4og4BTRG7EXU8W9FOFI44F9fdiX8cGgIDUTbDtMo05m+nwYyOVd7AljDMRbH/sueny979luPzCQ9REZ6JgMvN9cV6tE6kwXCXy1DCalepDMz2agSCJUl2YetE1qBIA==",
"X-Amz-Date": "20230217T145513Z",
"X-Amz-Region-Set": "cn-north-1",
"x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"Authorization": "AWS4-ECDSA-P256-SHA256 Credential=ASIAQRIBWRJKNSX3ID6O/20230217/sts/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, Signature=3045022100e7ed2c7e09e09x84x31b9dfa9dabdeb93583ff07a725b6ac3a1a2a54631"
}
关于签发sigv4具体的完整实现可以参考
https://pypi.org/project/requests-auth-aws-sigv4/
以下使用aksk获取凭证并发送请求
import requests
from requests_auth_aws_sigv4 import AWSSigV4
aws_auth = AWSSigV4('sts',
aws_access_key_id="xxxxxxx",
aws_secret_access_key="xxxxxx",
region="cn-north-1"
)
r = requests.request('POST', 'https://sts.cn-north-1.amazonaws.com.cn',
data=dict(Version='2011-06-15', Action='GetCallerIdentity'),
auth=aws_auth)
print(r.text)
以上方式比较复杂,但是最接近请求的本质
sdk获取token
官网提供了获取token的方式,还有大佬实现了一个eks-token的仓库
https://github.com/boto/boto3/issues/2309
import base64
import boto3
import re
from botocore.signers import RequestSigner
def get_bearer_token(cluster_id, region):
STS_TOKEN_EXPIRES_IN = 60
session = boto3.session.Session()
client = session.client('sts', region_name=region)
service_id = client.meta.service_model.service_id
signer = RequestSigner(
service_id,
region,
'sts',
'v4',
session.get_credentials(),
session.events
)
params = {
'method': 'GET',
'url': 'https://sts.{}.amazonaws.com.cn/?Action=GetCallerIdentity&Version=2011-06-15'.format(region),
'body': {},
'headers': {
'x-k8s-aws-id': cluster_id
},
'context': {}
}
signed_url = signer.generate_presigned_url(
params,
region_name=region,
expires_in=STS_TOKEN_EXPIRES_IN,
operation_name=''
)
base64_url = base64.urlsafe_b64encode(signed_url.encode('utf-8')).decode('utf-8')
# remove any base64 encoding padding:
return 'k8s-aws-v1.' + re.sub(r'=*', '', base64_url)
# If making a HTTP request you would create the authorization headers as follows:
headers = {'Authorization': 'Bearer ' + get_bearer_token('test124', 'cn-north-1')}
print(headers)
然后使用这个token访问集群即可
from kubernetes import client, config
Token = "k8s-aws-v1.aHR0cHM6Ly9zdHMuY24tbm9ydGgtxxJQVFSSUJXUkpLSEY3VzJLQkklMkYyMDIzMDIxNyUyRmNuLW5vcnRoLTElMkZzdHMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDIzMDIxN1QxNjEzNDZaJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCUzQngtazhzLWF3cy1pZCZYLUFtei1FeHBpcmVzPTYwJlgtQW16LVNlYxxx"
configuration = client.Configuration()
configuration.host = "https://C9611E7xxxx921D4BC09B.yl4.cn-north-1.eks.amazonaws.com.cn"
configuration.verify_ssl = False
configuration.api_key = {"authorization": "Bearer " + Token}
client.Configuration.set_default(configuration)
v1 = client.CoreV1Api()
print("Listing pods with their IPs:")
ret = v1.list_pod_for_all_namespaces(watch=False)
for i in ret.items:
print("%s\t%s\t%s" % (i.status.pod_ip, i.metadata.namespace, i.metadata.name))

浙公网安备 33010602011771号