AWS S3 和 Lambda 使用

发布于:2025-04-02 ⋅ 阅读:(21) ⋅ 点赞:(0)

目录:
AWS概述
EMR Serverless
AWS VPC及其网络
关于AWS网络架构的思考
AWS S3 和 Lambda 使用

本文将通过一个实例来说明如何使用 AWS S3 和 Lambda。


使用场景:通过代码将文件上传到S3,该文件需要是公开访问的,并对上传的文件进行安全检测。

文件上传到S3

S3 bucket 设置

首先创建一个S3的bucket,例如 my-test-cn-north-1-bucket。为了公开访问,该bucket必须关闭"Block public access (bucket settings)",除了bucket级别之外,账号级别也需要关闭。这个设置是 public read 的前提。

Bucket policy 必须设置Principal和Action,Principal可以设置为当前账户下的用户或角色。如果允许当前账户下的所有的用户/角色,可以这样设置:

{
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::123456789012:root" },
  "Action": [
                "s3:GetObject",
                "s3:GetObjectAcl",
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
  "Resource": "arn:aws:s3:::my-test-cn-north-1-bucket/*"
}

由于Bucket policy限制,此时bucket中的文件只有当前账户下的用户或角色才能读写。因此需要在上传文件时将文件的权限设置为public read的,也就是说要修改文件的ACL。编辑 Object Ownership,开启 “ACLs enabled”,至于对象拥有关系,选择 “Bucket owner preferred” 即可。

上传文件,并在文件的权限设置中将ACL修改为"public access"。这样设置完成之后,即可以保证bucket的put操作是受限的,同时read操作是公开的。

S3 API 文件上传

本地通过代码进行文件上传:

private static S3AsyncClient getAsyncClient() {
        AssumeRoleRequest assumeRoleRequest = AssumeRoleRequest.builder()
                .roleArn("arn:aws-cn:iam:: 123456789012:role/product/operation")
                .roleSessionName("AssumeRoleSession").build();

        StsClient stsClient = StsClient.builder()
                .credentialsProvider(ProfileCredentialsProvider.builder()
                        .profileName("your-profile") // replace with your profile
                        .build())
                .build();

        StsAssumeRoleCredentialsProvider creProvider =
                StsAssumeRoleCredentialsProvider.builder().stsClient(stsClient)
                        .refreshRequest(assumeRoleRequest).build();
        return S3AsyncClient.crtBuilder().credentialsProvider(creProvider).build();
}

而在生产环境中,当然是不能将角色ARNaws profile写到代码中的。因此需要用过 IRSA(IAM Roles for Service Accounts) 来实现 AWS API 调用。

创建一个ServiceAccount:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-s3-access
  namespace: your-ns
  annotations:
    eks.amazonaws.com/role-arn: arn:aws-cn:iam:: 123456789012:role/my-test-cn-north-1-eks-access-s3

在角色my-test-cn-north-1-eks-access-s3policies中,需要设置bucket的访问策略:

{
	"Effect": "Allow",
        "Action": "s3:*",
        "Resource": [
            "arn:aws-cn:s3:::my-test-cn-north-1-bucket/*"
        ]
}

然后在deployment中指定serviceAccount:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ms-test
spec:
  template:
    metadata:
      labels:
    	  ...
    spec:
      serviceAccountName: my-s3-access

如此 EKS 的 workloads 即可以 ServiceAccount 绑定的角色身份来执行 S3 的 API 调用。

文件上传代码如下:

private static void putObjectToS3(InputStream is, String key, String bucketName, boolean publicRead) {
    try (S3TransferManager transferManager = S3TransferManager.builder()
        .s3Client(S3AsyncClient.crtCreate()).build()) {

        UploadRequest uploadRequest = UploadRequest.builder()
                .putObjectRequest(req -> {
                    req.bucket(bucketName).key(key);
                    if (publicRead) {
                        req.acl("public-read"); // set public read acl
                    }
                })
                .addTransferListener(LoggingTransferListener.create())
                .requestBody(AsyncRequestBody.fromInputStream(
                        config -> config.inputStream(is).executor(newFixedThreadPool(8))))
                .build();

        transferManager.upload(uploadRequest).completionFuture().join();
    }
}

某些类型的文件如果不指定contentType,通过url访问时需要下载。如果指定了contentType,则可以在浏览器中打开。

文件安全扫描

创建lambda函数

文件上传到 S3之后,可以通过 lambda 来进行安全扫描。

首先创建一个 AWS lambda,选择最简单的 Author from scratch 模板,同时选择需要的运行时环境和系统架构,这里选择arm64架构。对于权限,如果需要复用role,就选择已有的role,否则就默认创建角色。

创建好 lambda 之后,需要在 Diagram 界面添加触发器,选择需要监听的 S3 bucket。

在 lambda 的配置页面,选择 Permissions 可以看到执行lambda的角色,也是创建lambda时默认创建的角色。查看该角色的权限 policies,可以发现与lambda日志相关的权限已经有了,但是还需要以下权限:

{
    "Effect": "Allow",
    "Action": [
        "s3:GetObject",
        "sns:Publish"
    ],
    "Resource": [
        "arn:aws-cn:s3:::my-test-cn-north-1-bucket/*"
    ]
}

创建 lambda layer

对文件进行安全扫描需要用到Clamav。正常情况下需要下载 Clamav 的源码然后编译成二进制文件。但是从官方下载的source包中没有configure文件,无法编译和安装。因此可以用第二种方式,直接在容器中安装 Clamav,然后将必要的文件拷贝出来制作 lambda layer。

使用docker run -it --name lambda-clamav amazonlinux:2 bash 来创建一个docker容器。

容器使用 amazonlinux:2 镜像是为了保持与 AWS Lambda的环境保持一致。

在容器中执行以下命令:

# 安装 ClamAV 和必要工具
yum install -y clamav clamav-update tar gzip

# 确认版本
clamscan --version

# 更新病毒库
freshclam

# 创建打包目录
mkdir -p /opt/clamav-layer/bin
mkdir -p /opt/clamav-layer/lib

# 复制 ClamAV 主程序
cp /usr/bin/clamscan /opt/clamav-layer/bin/
cp /usr/bin/freshclam /opt/clamav-layer/bin/

# 复制动态链接库
ldd /usr/bin/clamscan | awk '{print $3}' | xargs -I {} cp {} /opt/clamav-layer/lib/
ldd /usr/bin/freshclam | awk '{print $3}' | xargs -I {} cp {} /opt/clamav-layer/lib/

# 复制病毒库配置文件(可选)
mkdir -p /opt/clamav-layer/etc
cp /etc/freshclam.conf /opt/clamav-layer/etc/

将文件从容器复制到本地:

docker cp lambda-clamav:/opt/clamav-layer ./clamav-layer

# 进入目录并打包
cd clamav-layer
zip -r ../clamav-layer.zip .

注意,只有更新病毒库后才能使用clamscan test.txt来扫描文件,否则没有基础的病毒库文件无法扫描。下载的病毒库文件默认放在/var/lib/clamav 目录下,总共有四个文件:

bytecode.cvd  daily.cvd  freshclam.dat  main.cvd

这里打包clamav-layer时并没有将病毒库一起打包进来,原因是 AWS Layer 限制了大小,压缩包不能超过50MB,解压后不能超过250MB。

layer 制作完成后,在 Lambda 的控制台的layer仓库中上传。上传时需要注意 layer的系统架构,可以在生成layer文件的容器中查看:

file /opt/clamav-layer/bin/clamscan

# 以下结果表明为 ARM 架构
opt/clamav-layer/bin/clamscan: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.7.0, BuildID[sha1]=ac6484d18dd79864db6d56599e020f8a968f652c, stripped

上传完成后我们回到 lambda 函数,在code tab页的最下面上传 layer。layer的架构和运行时必须与lambda函数的架构和运行时是兼容的。如果lambda 函数是 x86_64的,就无法使用 arm 架构的layer。

layer的压缩包会直接解压到 lambda 实例的 /opt 目录下,其结构会变成: /opt/bin/clamscan, /opt/lib/…

lambda 函数实现

由于受到layer的大小限制导致病毒库无法打包到layer中,因此考虑实时下载或从S3下载。

将病毒库文件打包成clamav_db.tar.gz并上传到S3,然后实现 lambda 函数:

import os
import subprocess
import boto3
import tarfile
from botocore.exceptions import ClientError

s3 = boto3.client('s3')
S3_BUCKET = "ms-test-cn-north-1-bucket"
CLAMSCAN_PATH = "/opt/bin/clamscan"
FRESHCLAM_PATH = "/opt/bin/freshclam"
LIB_DIR = "/opt/lib"
DB_KEY = "clamav_db.tar.gz"
TMP_DB_PATH = "/tmp/clamav_db.tar.gz"
DB_DIR = "/tmp/clamav_db"

def download_and_extract_db():
    try:
        s3.download_file(S3_BUCKET, DB_KEY, TMP_DB_PATH)
        os.makedirs(DB_DIR, exist_ok=True)
        with tarfile.open(TMP_DB_PATH, "r:gz") as tar:
            tar.extractall(DB_DIR)
        os.remove(TMP_DB_PATH)
    except ClientError as e:
        print(f"S3 下载失败: {e}")
        raise

def lambda_handler(event, context):
    os.environ["LD_LIBRARY_PATH"] = LIB_DIR
    if not os.path.exists(f"{DB_DIR}/main.cvd"):
        print(f"start download clamav db")
        download_and_extract_db()
        
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = event['Records'][0]['s3']['object']['key']
    tmp_path = f"/tmp/{os.path.basename(key)}"
    try:
        s3.download_file(bucket, key, tmp_path)
        print(f"the file need to be checked: ", tmp_path)

        scan_cmd = [CLAMSCAN_PATH, "-d", DB_DIR, tmp_path]
        try:
            result = subprocess.run(scan_cmd, env={"LD_LIBRARY_PATH": LIB_DIR}, capture_output=True, text=True, timeout=180)
            print("stdout:", result.stdout)
            print("stderr:", result.stderr) 
            if "Infected files: 0" not in result.stdout:
                print(f"感染文件: {key} - 结果: {result.stdout}")
                return {"status": "INFECTED"}
            else:
                print(f"安全文件: {key}")
                return {"status": "CLEAN"}
        except subprocess.TimeoutExpired:
            print("timeout!")
    except ClientError as e:
        print(f"S3 错误: {e}")
        return {"status": "ERROR"}
    finally:
        if os.path.exists(tmp_path):
            os.remove(tmp_path)

对于lambda来说,只有 /tmp目录是可写的,且最大为512MB。当 Lambda 服务复用同一个执行环境即热启动时,该目录是保留的。这种复用通常发生在短时间内连续多次调用同一个函数时。

当Lambda服务创建一个新的执行环境时,/tmp 目录会被清空并重新初始化。

如果想持久化病毒库,而不是每次重新下载,可以考虑挂载EFS

测试

在 lambda 函数的 code 页面可以创建test event用来模拟S3 trigger,并可以保存下来复用。注意,测试事件中bucketName和文件需要是真实存在的。


网站公告

今日签到

点亮在社区的每一天
去签到