AWS 有个教学课程,教学课程:使用 Amazon S3 触发条件建立缩图影像,今天我们就以这个教程为基础,并结合Day 32 - 透过手机呼叫 Amazon API Gateway 上传图片到 S3这篇文章,让使用者可以上传一个图片後,就完成图片镜像的动作。
以上这个实验需要的 AWS 服务有
下图中显示,透过具有 CORS 设定的 API Gateway 取得角色 A 的授权许可,将图片写入到储存贮体 A,写入的动作会触发 Lambda 函数,於是 Lambda 函数就会读取储存贮体 A 的图片,进行镜像运算後,写入储存贮体 B,此时它是取得角色 B 的许可授权,而因为 S3 储存贮体 B 具有对外公开的读取权限,所以网际网路中的用户就可以直接读取镜像图片。
图 1、实作 S3 驱动 Lambda 函数进行镜像架构图
上传图片的 API Gateway 已於 Day 32 - 透过手机呼叫 Amazon API Gateway 上传图片到 S3 这篇文章中完成实作,接着我们需要完成的就是建立一个产生镜像图片的 Lambda 函数,步骤如下:
建立缩图程序
以下命令用来建立虚拟环境 pil,并激活虚拟环境,接着将安装套件用的 pip3 更新到最新版本,并安装所需套件 pillow。
python3 -m venv pil
. pil/bin/activate
cd pil
pip3 install --upgrade pip
pip3 install Pillow
tree -L 2
撰写需要的程序 mirror.py,会将指定的图片呈现镜像,左右颠倒,需要先放一张图片 00-frame-0054.jpg 到目录中,文件夹结构如下图所示。
图 2、虚拟环境 pil 的文件夹结构
_mirror.py
from PIL import Image, ImageOps
import PIL.Image
def resize_image(image_path, resized_path):
with Image.open(image_path) as image:
im_mirror = ImageOps.mirror(image)
im_mirror.save(resized_path)
OriginImg = '00-frame-0054.jpg'
ResizeImg = '00-frame-0054_mirror.jpg'
resize_image(OriginImg,ResizeImg)
运行以下命令後,可以得到一张呈现镜像,左右颠倒的图片,如下图所示,
python3 mirror.py
图 3、呈现镜像,左右颠倒的图片
建立 IAM 角色与政策
需要建立一个角色需要由 Lambda 函数来执行,且具有读取储存贮体 A 与写入储存贮体 B 的许可授权。进入 IAM 管理控制台,选择新增角色,接下来如下图所示,选择 Lambda 的使用案例後点击 下一个:许可 按钮。
图 4、建立一个角色选择 Lambda 的使用案例
在搜寻文字框中输入 basic 找到 AWSLambdaBasicExecutionRole 进行连接,这将允许这个角色有写入 CloudWatch 记录档的全县,方便程序除错之用,如下图所示。
图 5、连接基础的 CloudWatch 除错用的许可政策
最後确定先前的设定後并输入角色名称後,就可以建立角色,如下图所示。
图 6、检阅设定并建立角色
编辑一个新的政策,内容如下图所示,给定读取 (GetObject) 储存贮体 A 与写入物件 (PutObject) 与权限 (PutObjectAcl) 到储存贮体 B。
图 7、新增政策
接着到角色设定画面,将新建政策连接到角色上,如下图所示。
图 8、将新增的政策连接到先前的角色
建立 Lambda layer
因为接下来的 Lambda 函数会用到 pillow 函式库,所以必须要这个函式库的套件一起打包到 Lambda 函式中,使用的方法有两种,一种是跟主程序打包在一起,另一种则是以分层 (Layer) 的方式,独立打包成一层,在 Lambda 函数中再添加需要的函式库层,我们采用第二种方法。
根据 AWS 的官方教程 建立和共用 Lambda 层,所建立的 pillow 函式库会出现 cannot import name '_imaging' from 'PIL'
的错误讯息,後来看了很多讨论後,发现应该是 pillow 套件会用到已经编译好的函式库 (cpython) ,以致於会出现这样的错误,解决方法就是直接去官方网站下载套件安装包,如下图所示。在 python 套件的官方网站 https://pypi.org/ 中找到 pillow 专案所在,找寻适合的安装包。比方说,如果你的 Python 版本是 3.8,pillow 版本是 8.3.2,那就可以找 Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl 这个档案下载,manylinux_2_5 只的是它适用於多种版本linux且核心为2.5, x86_64 则是适用的 CPU 类型,切勿以自己电脑的环境来看,因为这是被放在 lambda 的虚拟环境中的,而 lambda 的虚拟环境大多是以 linux 容器为主,当然也有 arm 架构跟 x86 架构,所以要根据建立函式库层的设定来挑选合适的安装包。
图 9、至 Python 套件管理官方网站找寻安装包
下载後直接用 zip 工具解压缩,会得到 3 个文件夹,将这三个文件夹放在 python 的文件夹中,在进行压缩即可,如下图所示。
图 10、压缩至 python 文件夹中
进入 AWS Lambda 管理控制台,选择建立 Layer,输入 Layer 名称,并上传先前建立的压缩档,在选择相容架构 x86_64,而相容的执行时间指的是希望可以在哪些版本的 python 中执行,确定後按下 建立 键即完成建立 Layer,如下图所示。
图 11、建立 Layer 设定画面
建立 Lambda 函数
进入 AWS Lambda 管理控制台,选择建立 Lambda 函数,设定内容如下图所示。
图 12、建立 Lambda 函数设定画面
建立 Lambda 函数後,选择进入 resizeFunc 函数的设定画面,在画面的最底端,为本函数新建 Layer,如下图所示。
图 13、为 Lambda 函数新增 Layer
进入新增 Layer 画面後,选择层来源为 自订 Layer,接着选择先前建立的 pillow8_3_2 函式库层,如下图所示。
图 14、选择 pillow8_3_2 函式库层
建立新测试事件,事件范本选择 hello-world,事件名称输入 mirror,内容所下图所示,这是用来模拟当 S3 触发 Lambda 函数後所传过来的参数内容,记得将 [INPUT_BUCKET] 改成实际的输入储存贮体名称,而[INPUT_OBJECT]要确保有这个档案。
{
"Records": [
{
"s3": {
"bucket": {
"name": "[INPUT_BUCKET]",
"arn": "arn:aws:s3:::[INPUT_BUCKET]"
},
"object": {
"key": "[INPUT_OBJECT]"
}
}
}
]
}
图 15、设定 Lambda 函数测试事件
而处理完後的镜像图片所在的储存贮体名称,则是设定在 Lambda 函数组态中的环境变量,变量金钥为 putbucket ,值是根据自己的实际设定来给定,设定画面如下所示。
图 16、设定 Lambda 函数组态中的环境变量
以下为 Lambda 函数的代码部分,会根据测试事件与环境参数的参数来进行读取。
lambda_function.py
import boto3
import os
import sys
import uuid
from urllib.parse import unquote_plus
from PIL import Image, ImageOps
import PIL.Image
s3_client = boto3.client('s3')
def mirror_image(image_path, mirror_path):
with Image.open(image_path) as image:
im_mirror = ImageOps.mirror(image)
im_mirror.save(mirror_path)
print('mirror the image {} to {}'.format(image_path, mirror_path))
def lambda_handler(event, context):
for record in event['Records']:
inputbucket = record['s3']['bucket']['name']
outputbucket = os.environ['putbucket']
key = unquote_plus(record['s3']['object']['key'])
tmpkey = key.replace('/', '')
download_path = '/tmp/{}{}'.format(uuid.uuid4(), tmpkey)
upload_path = '/tmp/mirror-{}'.format(tmpkey)
s3_client.download_file(inputbucket, key, download_path)
mirror_image(download_path, upload_path)
s3_client.upload_file(upload_path, outputbucket, key,ExtraArgs={'ACL': 'public-read','ContentType':'image/jpeg'})
图 17、Lambda 函数测试结果
建立 S3 bucket事件通知
进入 S3 管理控制台画面,选择储存贮体 A (上传时的储存贮体),选择 属性 页签,找到 事件通知 这个属性,点击 建立事件通知 按钮来进入建立事件通知画面,完成以下配置:
图 18、建立事件通知设定画面
进入储存贮体 A 物件 页签画面,手动上传一个档案,测试先前建立的事件通知是否正常运行,如下图所示。
图 19、手动上传一个档案
切换到储存贮体 B 物件 页签画面,确认镜像图片档案是否写入,如下图所示。
图 20、确认镜像图片档案是否写入储存贮体 B
打开 CloudWatch 管理控制台,找到 CloudWatch 日志中的 Lambda 函数日志,检查是否有运行,结果如下图所示。
图 21、检查 CloudWatch 日志中的 Lambda 函数日志
最後就是整合 API Gateway,在本地端撰写一个网页,可以上传本地档案到储存贮体 A ,因而触发 Lambda 函数後,生成一个镜像图片,放在储存贮体 B ,因为镜像图片的属性是公开存取,所以可以直接用网页的 <img> 标签读取,下图中的下方图片就是位於储存贮体 B 的档案。
图 22、呈现镜像,左右颠倒的图片
参考网页程序如下所示。
uploadtoS3.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Upload file to S3</title>
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<h1>S3 Uploader Test</h1>
<div v-if="!image">
<h2>Select an image</h2>
<input type="file" @change="onFileChange">
</div>
<div v-else>
<img :src="image" />
<button v-if="!uploadURL" @click="removeImage">Remove image</button>
<button v-if="!uploadURL" @click="uploadImage">Upload image</button>
</div>
<h2 v-if="uploadURL">Success! Image uploaded to bucket.<br/>
<img :src="returnImage" />
</h2>
</div>
<script>
const MAX_IMAGE_SIZE = 10000000
/* ENTER YOUR ENDPOINT HERE */
const API_ENDPOINT = '[API_ENDPOINT]' // e.g. https://ab1234ab123.execute-api.us-east-1.amazonaws.com/uploads
const RET_ENDPOINT = '[RET_ENDPOINT]' // e.g. 'https://bucketb.s3.ap-southeast-1.amazonaws.com/'
uploadFile=''
new Vue({
el: "#app",
data: {
image: '',
uploadURL: '',
returnImage: ''
},
methods: {
onFileChange (e) {
let files = e.target.files || e.dataTransfer.files
if (!files.length) return
for( attr in files[0])
console.log(attr)
console.log(files[0].name)
uploadFile = files[0].name
this.createImage(files[0])
},
createImage (file) {
// var image = new Image()
let reader = new FileReader()
reader.onload = (e) => {
console.log('length: ', e.target.result.includes('data:image/jpeg'))
if (!e.target.result.includes('data:image/jpeg')) {
return alert('Wrong file type - JPG only.')
}
if (e.target.result.length > MAX_IMAGE_SIZE) {
return alert('Image is loo large.')
}
this.image = e.target.result
}
reader.readAsDataURL(file)
},
removeImage: function (e) {
console.log('Remove clicked')
this.image = ''
},
uploadImage: async function (e) {
console.log('Upload clicked')
console.log('Uploading: ', uploadFile)
let binary = atob(this.image.split(',')[1])
let array = []
for (var i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i))
}
let blobData = new Blob([new Uint8Array(array)], {type: 'image/jpeg'})
this.uploadURL = API_ENDPOINT + uploadFile
console.log('Uploading to: ', this.uploadURL)
const result = await fetch(this.uploadURL, {
method: 'PUT',
body: blobData
})
console.log('Result: ', result)
this.returnImage = RET_ENDPOINT + uploadFile
}
}
})
</script>
<style type="text/css">
body {
background: #20262E;
padding: 20px;
font-family: sans-serif;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
text-align: center;
}
#logo {
width: 100px;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
h1, h2 {
font-weight: normal;
margin-bottom: 15px;
}
a {
color: #42b983;
}
img {
width: 30%;
margin: auto;
display: block;
margin-bottom: 10px;
}
</style>
</body>
</html>
<<: Day 22 贝式分类器 Bayesian Classifier
终於最後一天,照惯例大家都在这里写後记吧~ 三年前第一次知道铁人赛,就是以社群身份去参加了颁奖典礼X...
今天要带大家做另外一个简单的场境应用,我们继续沿用昨天所处理的 parquet File 来做今天的...
前言 在 ES6 後,新增了 class 类别,一个更简洁的语法来建立物件,也是建立继承的语法糖。 ...
前言 今年13th铁人终於来到了尾声,又过了一年时间过得很快,今年是第二年的挑战(依旧主管迫害啊~)...
今天在执行 WordPress 上的版本更新时,因为更新档案太大,出现了错误「FastCGI 处理序...