目录:
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();
}
而在生产环境中,当然是不能将角色ARN
和aws 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-s3
的policies
中,需要设置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和文件需要是真实存在的。