Day 33 - 实作 S3 驱动 Lambda 函数进行镜像

Day 33 - 实作 S3 驱动 Lambda 函数进行镜像

AWS 有个教学课程,教学课程:使用 Amazon S3 触发条件建立缩图影像,今天我们就以这个教程为基础,并结合Day 32 - 透过手机呼叫 Amazon API Gateway 上传图片到 S3这篇文章,让使用者可以上传一个图片後,就完成图片镜像的动作。

以上这个实验需要的 AWS 服务有

  • Amazon API Gateway:提供上传图片用的 API
  • AWS Lambda: 执行将图片执行镜像处理的运算。
  • Amazon S3: 提供两个储存贮体,一个作为上传图片,一个作为镜像图片的存放。
  • Identity and Access Management (IAM): 提供两个角色,一个是授权给 API Gateway 上传图片的许可权限;另一个是授权给 AWS Lambda 读取图片与写入处理後的图片的许可权限。
  • CloudWatch:除错、监控之用。

下图中显示,透过具有 CORS 设定的 API Gateway 取得角色 A 的授权许可,将图片写入到储存贮体 A,写入的动作会触发 Lambda 函数,於是 Lambda 函数就会读取储存贮体 A 的图片,进行镜像运算後,写入储存贮体 B,此时它是取得角色 B 的许可授权,而因为 S3 储存贮体 B 具有对外公开的读取权限,所以网际网路中的用户就可以直接读取镜像图片。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510mxg8S3KuoJ.png
图 1、实作 S3 驱动 Lambda 函数进行镜像架构图

上传图片的 API Gateway 已於 Day 32 - 透过手机呼叫 Amazon API Gateway 上传图片到 S3 这篇文章中完成实作,接着我们需要完成的就是建立一个产生镜像图片的 Lambda 函数,步骤如下:

  1. 本机端建立缩图程序
    • 建立虚拟环境并安装需要套件
    • 撰写程序并运行
  2. 建立 IAM Role and Policy
  3. 建立 Lambda layer
  4. 建立 Lambda 函数
  5. 建立 S3 bucket 事件通知

建立缩图程序

以下命令用来建立虚拟环境 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 到目录中,文件夹结构如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/201295103Bplpz67On.png
图 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

https://ithelp.ithome.com.tw/upload/images/20211007/20129510Kp7yPB4Urd.png
图 3、呈现镜像,左右颠倒的图片

建立 IAM 角色与政策

需要建立一个角色需要由 Lambda 函数来执行,且具有读取储存贮体 A 与写入储存贮体 B 的许可授权。进入 IAM 管理控制台,选择新增角色,接下来如下图所示,选择 Lambda 的使用案例後点击 下一个:许可 按钮。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510WYK3SUlBvJ.png
图 4、建立一个角色选择 Lambda 的使用案例

在搜寻文字框中输入 basic 找到 AWSLambdaBasicExecutionRole 进行连接,这将允许这个角色有写入 CloudWatch 记录档的全县,方便程序除错之用,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/201295108dyqJGu4Jf.png
图 5、连接基础的 CloudWatch 除错用的许可政策

最後确定先前的设定後并输入角色名称後,就可以建立角色,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510q7otPjt1ZV.png
图 6、检阅设定并建立角色

编辑一个新的政策,内容如下图所示,给定读取 (GetObject) 储存贮体 A 与写入物件 (PutObject) 与权限 (PutObjectAcl) 到储存贮体 B。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510UXadxnJZpr.png
图 7、新增政策

接着到角色设定画面,将新建政策连接到角色上,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510ndf6jHSGos.png
图 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 架构,所以要根据建立函式库层的设定来挑选合适的安装包。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510AefaHRB9IK.png
图 9、至 Python 套件管理官方网站找寻安装包

下载後直接用 zip 工具解压缩,会得到 3 个文件夹,将这三个文件夹放在 python 的文件夹中,在进行压缩即可,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510rppvMB4fC2.png
图 10、压缩至 python 文件夹中

进入 AWS Lambda 管理控制台,选择建立 Layer,输入 Layer 名称,并上传先前建立的压缩档,在选择相容架构 x86_64,而相容的执行时间指的是希望可以在哪些版本的 python 中执行,确定後按下 建立 键即完成建立 Layer,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510pGhX9fL0Mv.png
图 11、建立 Layer 设定画面

建立 Lambda 函数

进入 AWS Lambda 管理控制台,选择建立 Lambda 函数,设定内容如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/201295100P9eNUstUv.png
图 12、建立 Lambda 函数设定画面

建立 Lambda 函数後,选择进入 resizeFunc 函数的设定画面,在画面的最底端,为本函数新建 Layer,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/201295102jDzwdPjO3.png
图 13、为 Lambda 函数新增 Layer

进入新增 Layer 画面後,选择层来源为 自订 Layer,接着选择先前建立的 pillow8_3_2 函式库层,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510ZYlTRkYN4R.png
图 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]"
        }
      }
    }
  ]
}

https://ithelp.ithome.com.tw/upload/images/20211007/20129510wGO97OS6x8.png
图 15、设定 Lambda 函数测试事件

而处理完後的镜像图片所在的储存贮体名称,则是设定在 Lambda 函数组态中的环境变量,变量金钥为 putbucket ,值是根据自己的实际设定来给定,设定画面如下所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510Cj8tv2lqaG.png
图 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'})

https://ithelp.ithome.com.tw/upload/images/20211007/20129510cmuKt2E48V.png
图 17、Lambda 函数测试结果

建立 S3 bucket事件通知

进入 S3 管理控制台画面,选择储存贮体 A (上传时的储存贮体),选择 属性 页签,找到 事件通知 这个属性,点击 建立事件通知 按钮来进入建立事件通知画面,完成以下配置:

  • 事件名称: mirrorEvent
  • 事件类型: 所有物件建立事件 (s3:ObjectCreated:): 勾选
  • 目的地: Lambda 函数
  • 指定 Lambda 函数: 从 Lambda 函数 中选择: resizeFunc

https://ithelp.ithome.com.tw/upload/images/20211007/20129510AkhG0a3Pzn.png
图 18、建立事件通知设定画面

进入储存贮体 A 物件 页签画面,手动上传一个档案,测试先前建立的事件通知是否正常运行,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510811Ohq7ZXy.png
图 19、手动上传一个档案

切换到储存贮体 B 物件 页签画面,确认镜像图片档案是否写入,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510BfHzK5jS3N.png
图 20、确认镜像图片档案是否写入储存贮体 B

打开 CloudWatch 管理控制台,找到 CloudWatch 日志中的 Lambda 函数日志,检查是否有运行,结果如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211007/201295107PtQONwqr7.png
图 21、检查 CloudWatch 日志中的 Lambda 函数日志

最後就是整合 API Gateway,在本地端撰写一个网页,可以上传本地档案到储存贮体 A ,因而触发 Lambda 函数後,生成一个镜像图片,放在储存贮体 B ,因为镜像图片的属性是公开存取,所以可以直接用网页的 <img> 标签读取,下图中的下方图片就是位於储存贮体 B 的档案。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510EArLk6ywka.png
图 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

>>:  用 event 来准备传给後端的 data

终於 30 天了,可以偷懒了ㄅ

终於最後一天,照惯例大家都在这里写後记吧~ 三年前第一次知道铁人赛,就是以社群身份去参加了颁奖典礼X...

Day27 NiFi 场景应用范例 (二)

今天要带大家做另外一个简单的场境应用,我们继续沿用昨天所处理的 parquet File 来做今天的...

D17 - 吃一颗 Class 语法糖 (上)

前言 在 ES6 後,新增了 class 类别,一个更简洁的语法来建立物件,也是建立继承的语法糖。 ...

结语

前言 今年13th铁人终於来到了尾声,又过了一年时间过得很快,今年是第二年的挑战(依旧主管迫害啊~)...

解决 IIS 的 PHP 发生 FastCGI 处理序超过设定的活动逾时问题

今天在执行 WordPress 上的版本更新时,因为更新档案太大,出现了错误「FastCGI 处理序...