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

33 篇文章 6 订阅

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 具有对外公开的读取权限,所以网际网路中的用户就可以直接读取镜像图片。

在这里插入图片描述
图 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 到目录中,文件夹结构如下图所示。

在这里插入图片描述
图 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-cp38-cp38-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 (上传时的存储桶),选择 属性 页签,找到 事件通知 这个属性,点击 建立事件通知 按钮来进入建立事件通知画面,完成以下配置:

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

在这里插入图片描述
图 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/axios@0.2.1/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>

参考资料

  • Python, Pillow: Flip image, https://note.nkmk.me/en/python-pillow-flip-mirror/
  • 教学课程:使用 Amazon S3 触发条件建立缩图影像,https://docs.aws.amazon.com/zh_tw/lambda/latest/dg/with-s3-tutorial.html
  • 教学课程:使用 Amazon S3 触发条件叫用 Lambda 函数, https://docs.aws.amazon.com/zh_tw/lambda/latest/dg/with-s3-example.html
  • 建立和共用 Lambda 层,https://docs.aws.amazon.com/zh_tw/lambda/latest/dg/configuration-layers.html
  • 使用 .zip 封存档部署 Python Lambda 函数,https://docs.aws.amazon.com/zh_tw/lambda/latest/dg/python-package.html
  • Pillow 8.3.2, https://pypi.org/project/Pillow/#files
  • AWS Lambda: cannot import name ‘_imaging’ from ‘PIL’, https://stackoverflow.com/questions/57197283/aws-lambda-cannot-import-name-imaging-from-pil
  • How to access an AWS Lambda environment variable from Python, https://stackoverflow.com/questions/40937512/how-to-access-an-aws-lambda-environment-variable-from-python
  • S3 presigned URLs with SAM, auth and sample frontend, https://github.com/aws-samples/amazon-s3-presigned-urls-aws-sam
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值