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

创建已签名的 AWS API 请求

由于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))
posted @ 2023-02-18 00:22  zhaojie10  阅读(8)  评论(0)    收藏  举报  来源