Serverless đơn giản – Gắn watermark tự động với Lambda, S3 và JIMP plugin

Xin chào mọi người mình đã trở lại rồi đây. Cũng đã 6 tháng kể từ bài viết đầu tiên Serverless – Giới thiệu chung chung của mình trên VTI Tech Blog này thì đến hôm nay mới tiếp tục với bài viết thứ 2 về chủ đề Serverless (Do hơi lười chút, gomen).

Ở bài viết trước mình đã giới thiếu về Serverless, điểm mạnh, yếu cũng như những lợi ích mà nó đem lại. Thì để tiếp nối chủ đề này, hôm nay mình sẽ hướng dẫn tạo 1 event-based function đơn giản với Lambda và S3 để mọi người có thể có cái nhìn ban đầu về serverless trên AWS cũng như use-case sử dụng.

Event-driven hay event-based là gì?

Cái này mình có nhắc đến ở trên nên muốn nói qua để mọi người hiểu hơn về cách Lambda được vận hành và vị trí trong 1 hệ thống sẽ là gì.

Hiểu đơn giản thì Event-driven hay event-base là mô hình mà ở đây Lambda function (logic) sẽ được trigger bởi 1 event từ bên ngoài với các parameter tương ứng để khởi chạy 1 luồng xử lí nhất định. Ví dụ có thể tạo 1 luồng với Lambda xử lí video, ảnh sẽ được trigger bởi event từ S3 để mỗi khi có 1 video, ảnh mới được upload lên thì nó sẽ được tự động xử lí và lưu trữ thông tin vào database.

Nói chung thì về cơ bản thì Lambda sẽ luôn được trigger bởi các event như vậy bởi các dịch vụ như S3, API Gateway, DynamoDB, SNS, SQS, … cho nên để thiết kế lên 1 hệ thống dựa trên serverless architecture thì cần vận dụng và kết hợp tốt các dịch vụ này với nhau.
Thôi lan man thế là đủ rồi mình bắt đầu nhỉ :)))

Hôm nay code gì?

Ở trên thì nói toàn là lí thuyết sáo rỗng, thì để hiểu rõ hơn được các vận hành cũng như hoạt động thì chúng ta sẽ bắt tay và code.
Giống như mình có nhắc tới 1 use case không hiếm gặp ở trên đó là tạo ra xử lí realtime tự động để xử lí mọi bức ảnh ảnh khi nó được upload vào S3. Thì cụ thể hơn thì mình sẽ tạo ra 1 luồng xử lí tự động gắn watermark lên tất cả các bức ảnh được upload sau đó lưu trữ lại. Nói chung là nó cũng đơn giản thôi vì mục đích chỉ là giới thiệu về Lambda, event-driven hay serverless cho nên mình cũng sẽ sử dụng thư viện cho nhanh.

Các thành phần:

  • Service: S3, Lambda, Cloudwatch Logs
  • Thư viện: AWS-SDK for Javascript, JIMP
  • Môi trường: NodeJS 8.10 trở lên

AWS-SDK thì là bộ thư viện để làm việc với các dịch vụ của AWS, cái này thì ở local thì cần cài đặt qua NPM tuy nhiên nếu cho lên Lambda thì không cần vì mặc định đã có rồi.

JIMP – một thư viện xử lí ảnh cho javascript. Lúc đầu khi chuẩn bị viết bài này thì mình cũng có tìm 1 vài thư viện khác để xử lí ảnh trên javascript tuy nhiên tất cả đều có 1 yêu cầu đó là phải cài thêm 1 thư viện xử lí ảnh native nữa là imageMagick. Việc này gặp phải 1 vấn đề của việc sử dụng Serverless mà ở bài trước mình có đề cập tới đó là việc môi trường phụ thuộc vào nhà cung cấp. Các setting, thư viện runtime sẽ do AWS cung cấp cho nên về cơ bản thì người dùng sẽ không thể dễ dàng thay đổi như việc quản lí 1 server thông thường. Và sau 1 hồi bế tắc google thì mình cũng tìm thấy được thư viện này với 1 dòng giới thiệu như sau:

An image processing library written entirely in JavaScript for Node, with zero external or native dependencies.

Quả đúng là cứu cánh bất ngờ trước bến bờ phá sản phải đổi sang viết cái khác. Ngoài ra thì thư viện này cũng có tới 7748 star và 487 fork, khá ngon cho anh em code Javascript.

Thật ra thì gần đây AWS mới ra mắt Lambda Layer để hỗ trợ việc tạo ra custom runtime tuy nhiên thì việc này cũng khá phức tạp vượt quá scope bài viết và mình cũng chưa làm thử nên hẹn 1 dịp khác sẽ có 1 bài về việc này. Anh em nào muốn thử thì tham khảo ở đây:
https://aws.amazon.com/about-aws/whats-new/2018/11/aws-lambda-now-supports-custom-runtimes-and-layers/

Step by step

Trước khi tạo được các dịch vụ trên AWS S3 thì chúng ta cần tạo 1 tài khoản AWS. Và năm đầu tiên sẽ thuộc diện Free Tier với rất nhiều dịch vụ free đủ cho việc học tập và làm làm các dự án cá nhân. Chi tiết bạn có thể tham khao link bên dưới:
https://viblo.asia/p/huong-dan-tao-tai-khoan-aws-free-1-nam-GrLZDpwJZk0
https://aws.amazon.com/free

Step 1: Tạo S3 để upload và lưu trữ file đã xử lí

  • Login vào AWS Console và chọn S3
  • Bấm Create Bucket
  • Nhập Bucket name và chọn Region sau đó bấm Create
  • Vào S3 bucket vừa tạo, tạo 2 folder là inputoutput (cái này tên tùy ý)

Step 2: Tạo IAM Role cho Lambda

IAM Role sẽ định nghĩa quyền truy cập S3 và lưu log lên CloudWatch Log.

Chọn dịch vụ IAM > mục Roles và bấm Create Role. Chọn Type of Trusted Entity là AWS Service và dịch vụ sẽ sử dụng role là Lamba (nếu không chọn Lambda sẽ không thể add được vào lambda). Tiếp theo bấm Next: Permissions để sang bước add Policy (quyền)

Có 2 cách là chọn các policy trong danh sách có sẵn của AWS (AmazonS3FullAccess, CloudWatchLogsFullAccess) hoặc cách 2 là bấm vào nút Create policy để tự tạo policy cho mình. Ở đây mình sẽ lựa chọn cách 1 cho nhanh nhưng trong khi làm dự án thì nên làm cách 2 để đảm bảo security.

Chọn xong thì click Next cho đến màn hình review và nhập Role name. Xong, bấm Create Role để kết thúc.

Step 3: Tạo Lambda function

Chọn dịch vụ Lambda > mục Functions và bấm Create Function. Khi màn hình setting hiện ra thì chọn Author from Scratch và nhập Function name.
Mục run time chọn Node.js 6.10 hoặc 8.10, mục Permission chọn Use an existing role và chọn role vừa tạo ở trên. OK thì bấm Create function

Default thì Lambda sẽ được setting timeout3s tuy nhiên xử lí ảnh sẽ mất nhiều thời gian hơn nên cần tăng thời gian timeout (mình để tạm 1 phút). Bấm Save để lưu lại thay đổi

Step 4: Tạo liên kết event giữa S3 và Lambda

  • Quay trở lại S3 > click vào bucket mà mình đã tạo trước đó.
  • Chọn tab Properties > mục Events
  • Bấm Add notification > nhập Name và chọn Events là All object create events
  • Prefix điền folder chứa input, Suffix có thể giới hạn loại file muốn xử lí (ở đây mình không điền)
  • Mục Send to chọn Lambda Function và chọn function đã tạo ở step 3
  • Bấm Save

Ngay sau khi save bạn có thể quay lại lambda function đã tạo để kiểm tra xem S3 events đã được liên kết hay chưa.

OK đã xong phần AWS, giờ tới code nhỉ….

Step 5: Setup Node.JS project

Trước khi làm phần này thì cần cài đặt Node.JS trong máy rồi nhé.

  1. Khởi tạo project với command npm init
  2. Cài đặt thư viện JIMP: npm install --save jimp
  3. Tạo file index.js

Step 6: Code code code

Open file index.js và add require thư viện aws-sdk và jimp

"use strict";

const Jimp = require("jimp");
var AWS = require("aws-sdk");
const s3 = new AWS.S3();

Load enviroment paramter, ở đây mình sẽ sử dụng 1 biến là tên BUCKET_NAME. Việc sử dụng biến enviroment này trong demo có thể là không cần thiết tuy nhiên trong khi làm dự án, sử dụng biến enviroment sẽ giúp dễ dàng thay đổi các setting hơn mà không cần thay đổi vào trong source code cũng như khi deploy trên nhiều môi trường khác nhau.

const BUCKET_NAME = process.env["BUCKET_NAME"];
const OUTPUT_FOLDER = "output";

Tạo function get signedUrl để download file từ S3.

var getDownloadUrl = async function(bucket, key) {
  var params = { Bucket: bucket, Key: key }; // default thì exprire time là 15 phút
  var url = s3.getSignedUrl("getObject", params);
  console.log(url);

  return url;
};

Function này sẽ sử dụng AWS-SDK để lấy download url cho object tương ứng trong event. Về cơ bản thì file của chúng ta là không public cho nên để download được bằng url thì phải sử dụng signedUrl, 1 loại url có gắn sẵn thông tin access và sẽ chỉ tồn tại trong 2 thời gian nhất định. Ngoài download thì cũng có thể tạo url cho upload, chi tiết tham khảo link bên dưới:
https://docs.aws.amazon.com/AmazonS3/latest/dev/PresignedUrlUploadObject.html

Tạo function upload file lên S3. Function này thì upload trực tiếp thông qua AWS-SDK mà không dùng signedUrl. Thực ra function download mình cũng có thể làm như vậy tuy nhiên do JIMP đã hỗ trợ đọc file từ url rồi nên mình dùng luôn cho tiện. (Lười tí :D)

var uploadImageToS3 = function(imageBuffer, bucket, key) {
  var params = {
    Body: imageBuffer,
    Bucket: bucket,
    Key: key
  };

  return s3.putObject(params).promise();
};

Tiếp theo là tạo function xử lí gắn watermark. Function này sẽ đọc ảnh watermark từ local (gói sẵn trong source code) và ảnh upload từ S3 rồi thực hiện combine lại với nhau.

var addWatermark = async function(url) {
  // step 1: đọc file input
  const original = await Jimp.read(url);
  // // step 2: đọc file watermark
  const mark = await Jimp.read("./wm.png"); // file watermark sẽ đặt luôn trong source folder
  // // step 3: // set độ mờ của watermark
  mark.opacity(0.5);
  // // step 4: add watermark vào ảnh
  const watermarkedImage = await original.composite(mark, 50, 50);

  return await watermarkedImage.getBufferAsync(Jimp.MIME_PNG);
};

Tạo function handler cho lambda. Khi có event thì function này sẽ được gọi và bắt đầu tiến trình xử lí. Mặc định của lamda sẽ setting handler là index.handler cho nên trong file index.js chúng ta sẽ tạo function handler như bên dưới. Ở đây mình có add thêm 1 số điểm checkpoint với console.log() để tiện cho việc debug.

exports.handler = async function(event, context) {
  // lấy thông tin file đã được upload từ event
  let key = event.Records[0].s3.object.key;
  console.log(key);

  let fileName = key.split("/").pop();
  console.log(fileName);
  console.log(BUCKET_NAME);

  // gắn watermark
  return getDownloadUrl(BUCKET_NAME, key)
    .then(addWatermark) // gọi hàm gắn watermark với input là downloadUrl trả về từ getDownloadUrl
    .then(buffer => {
      let outputKey = `${OUTPUT_FOLDER}/${fileName}`;
      console.log(outputKey);

      return uploadImageToS3(buffer, BUCKET_NAME, outputKey);
    })
    .then(data => {
      console.log(data);

      return data;
    })
    .catch(err => {
      console.log(err);

      return err;
    });
};

Function này sẽ được gọi khi có event được gửi tới, các thông tin của event sẽ nằm trong biến event được truyền vào). Dưới đây là thông tin chi tiết 1 event được gửi từ S3

{
  "Records":[
    {
      "eventVersion":"2.0",
      "eventSource":"aws:s3",
      "awsRegion":"us-west-2",
      "eventTime":"1970-01-01T00:00:00.000Z",
      "eventName":"ObjectCreated:Put",
      "userIdentity":{  
        "principalId":"AIDAJDPLRKLG7UEXAMPLE"
      },
      "requestParameters":{  
        "sourceIPAddress":"127.0.0.1"
      },
      "responseElements":{  
        "x-amz-request-id":"C3D13FE58DE4C810",
        "x-amz-id-2":"FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"
      },
      "s3":{  
        "s3SchemaVersion":"1.0",
        "configurationId":"testConfigRule",
        "bucket":{  
          "name":"sourcebucket",
          "ownerIdentity":{  
            "principalId":"A3NL1KOZZKExample"
          },
          "arn":"arn:aws:s3:::sourcebucket"
        },
        "object":{  
          "key":"HappyFace.jpg",
          "size":1024,
          "eTag":"d41d8cd98f00b204e9800998ecf8427e",
          "versionId":"096fKKXTRTtl3on89fVO.nfljtsv6qko"
        }
      }
    }
  ]
}

Có thể thấy ở đây chúng ta sẽ chỉ cần quan tâm tới giá trị key của object. Giá trị này chính là giá trị cho biết file vừa upload lên là file nào. Ngoài ra nếu Lambda nhận event từ nhiều S3 bucket khác nhau thì cần quan tâm thêm đến giá trị name của bucket tuy nhiên trong bài này chỉ dùng 1 nên cũng không cần.

Code xong rồi, giờ cho lên mây. :v

Step 7: Deploy source code lên S3

Nén toàn bộ source code bao gồm cả node_modules vào 1 file zip. Lưu ý là không nén folder chứa source mà vào trong folder chọn tất cả rồi zip lại. Lưu ý nhớ cho cả file ảnh watermark vào trong source folder.

// ảnh copy source

Quay trở lại AWS, mở Lambda function và add giá trị cho biến BUCKET_NAME vào enviroment

Upload source code file zip lên lambda

Done. Copy paste mỏi tay quá, nghỉ tí làm tách trà...

Có vẻ ổn ổn rồi đấy, check hàng nào!!!

Cách debug và checklog

Ở đây thì mặc định Lambda sẽ lưu log vào dịch vụ CloudWatch Log, chính vì vậy khi add quyền cho Lambda Role thì ngoài quyền truy cập S3 thì luôn phải add thêm quyền liên quan đến CloudWatch Log.

Để view được log của 1 function Lambda thì cách đơn giản nhất là vào trong Lambda Function, chọn tab Monitoring rồi click nút view logs in CloudWatch

Lần đầu vào thì tất nhiên sẽ trống trơn vì chưa test gì rồi…

Upload ảnh và test

Giờ thì check xem hàng họ chạy ổn không.

  • Quay trở lại S3, tìm đến bucket trước đó đã gắn event liên kết với Lambda
  • Vào folder input đã tạo trước đó, upload ảnh bất kì (to to chút)
  • Sang folder output và check thành quả ( có thể phải chờ 1 lúc để lambda xử lí xong)

Sau khoảng 1 phút bạn sẽ thấy ảnh đã được gắn watermark của mình trong folder output, còn nếu không thấy thì chắc chắn là lỗi rồi :)).

Bây giờ thì có thể quay lại CloudWatch và thấy được log mà Lambda trả ra. Ảnh dưới của mình có rất nhiều log do đã chạy test Lambda này rất nhiều lần khi code demo :)).

Danh sách log stream:

Chi tiết 1 log stream:

Làm y hệt mà vẫn lỗi, nguyên nhân có thể ở đâu.

Nhiều khi làm theo tutorial, đã làm các step y hệt rồi mà lúc chạy vẫn bung bét. Sự thật này nghe nó cũng bình thường như việc coder bị FA vậy 🙁

Nếu gặp lỗi thì do đâu, trong quá trình làm demo mình cũng lỗi bung bét và phải debug. Để biết lỗi ở đâu thì đầu tiên phải đi check log đã nhỉ.
Nếu may mắn có log luôn, cái này thì easy rồi cứ nhìn error log và debug thôi. Có 1 số lỗi như sau:

  • IAM role bị thiếu quyền S3
  • Code lỗi syntax
  • File upload lên có vấn đề thư viện không đọc được
  • Quên không set environment variable 😀

Đó là có log, vậy nếu không có log thì sao:

  • IAM role bị thiếu quyền lưu log lên CloudWatch (cái này fix xong thì có log ta lại quay lên check ở trên)
  • Cài đặt event sang Lambda bị sai (prefix sai, nhầm function)

Tóm lại

Bài viết trên mình đã hướng dẫn tạo 1 Lambda function đơn giản để xử lí ảnh realtime bất cứ khi nào được upload lên folder chỉ định bằng việc kết hợp 2 dịch vụ Lambda và S3. Tùy vào yêu cầu mà chúng ta có thể kết hợp Lambda với rất nhiều các dịch vụ khác để xây dựng lên 1 hệ thống theo mô hình Serverless để xử lí các tác vụ một cách nhanh nhất mà không phải lo lắng về vấn đề scale hay quản lí server phức tạp. Tuy nhiên như mình có nhấn mạnh ở bài trước đó là Không phải cái gì cũng cho lên Serverless được! cho nên luôn phải phân tích đầy đủ yêu cầu để có thể lựa chọn được phương án tối ưu nhất.

OK thế là hết rồi, hẹn quay lại với bài tiếp về series Severless đơn giản này.

Leave a Reply