AWS Lambda 上手很輕鬆

2020-12-02 日拱一兵

前言

微服務架構有別於傳統的單體式應用方案,我們可將單體應用拆分成多個核心功能。每個功能都被稱為一項服務,可以單獨構建和部署,這意味著各項服務在工作時不會互相影響

這種設計理念被進一步應用,就變成了無服務(Serverless)。「無服務」看似挺荒唐的,其實伺服器依舊存在,只是我們不需要關注或預置伺服器。這讓開發人員的精力更集中——只關注功能實現

Serverless 的典型便是 AWS Lambda

AWS Lambda

如果你是 Java 開發人員,你應該聽說過或使用過 JDK 1.8 裡面的 Lambda,但是 AWS 中的 Lambda 和 JDK 中的 Lambda 沒有任何關係

這裡的 AWS Lambda 就是一種計算服務,無需預置或管理伺服器即可運行代碼,藉助 Lambda,我們幾乎可以為任何類型的應用程式或後端服務運行代碼,而且完全無需管理,我們要做的只是上傳相應的代碼,Lambda 會處理運行和擴展 HA 代碼所需的一切工作

說的直白一點

Lambda 就好比實現某一個功能的方法 (現實中,通常會讓 Lambda 功能儘可能單一),我們將這個方法做成了一個服務供調用

到這裡你可能會有個困惑,Lambda 既然就是一個「方法」,那誰來調用?或怎麼來調用呢?

如何調用 Lambda

為了回答上面這個問題,我們需要登陸到 AWS,打開 Lambda 服務,然後創建一個 Lambda Function (hello-lambda)

Lambda 既然是個方法,就要選擇相應的 Runtime 環境,如下圖所示,總有一款適合你的(最近在用 Node.js, 這裡就用這個吧)

點擊右下角的 Create function 按鈕進入配置頁面

在上圖紅色框線的位置就可以配置出發 Lambda 的觸發器了,點擊 Add trigger

從上圖可以看出,AWS 內置的很多服務都可以觸發 Lambda,我在工作中常用的有:

  • API Gateway (一會的 demo 會用到,也是最常見的調用方式)
  • ALB - Application Loac Balancer
  • CloudFront
  • DynamoDB
  • S3
  • SNS - Simple Notification Service
  • SQS - Simple Queue Service

上面只是 AWS 內置的一些服務,向下滑動,你會發現,你也可以配置很多非 AWS 的事件源

到這裡,上面的問題你應該已經有了答案了。這裡暫時先無需任何 trigger,先點擊右上角的 Test 測試一下 Lambda

一個簡單的 Lambda Function 就實現了,紅色框線的 response 只是告訴大家,每個請求都會有相應的 Request ID,更有 START/END 標識快速定位 Log 內容 (可以通過 CloudWatch 查看,這裡暫不展開說明)

你也可能已經開始發散你的思維了,如何運用 AWS Lambda,其實在 AWS 官網有很多樣例:

經典案例

比如為了適應多平臺圖片展示,一張原始圖片上傳到 S3 後,會通過 Lambda resize 適應不同平臺大小的圖片

比如使用 AWS Lambda 和 Amazon API Gateway 構建後端,以驗證和處理 API 請求,當某一個用戶發布一條動態,訂閱用戶將收到相應的通知

接下來我們就用 Lambda 實現經典的分布式訂單服務案例

訂單服務 Demo

為了增強用戶使用體驗,或者為了提升程序吞吐量,亦或是為了架構設計程序解耦,考慮到以上這些情況,我們通常都會藉助消息中間件來完成

假設有一常見場景,用戶下訂單時如果選擇開具發票,則需要調用發票服務,很顯然調用發票服務不是程序運行的關鍵路徑,這種場景,我們就可以通過消息中間件來解耦。這裡有兩個服務:

  1. 訂單服務
  2. 發票服務

如果用 Lambda 來實現兩個服務,整體設計思想就是這樣滴:

現實中,我們不可能在 AWS console 通過點擊按鈕來創建各個服務的,在 AWS 實際開發中, 我們通過寫 CloudFormation Template (以下會簡稱 CFT,其實就是一種 YAML 或者 JSON 格式的定義)來創建相關 AWS 服務,如果上述這個 Demo,從圖中可以看出,我們要創建的服務還是非常多的:

  • Lambda * 2
  • API Gateway
  • SQS

如果寫 AWS 原生的 CFT,要實現的內容還是挺多的

但是...... 懶惰的程式設計師總是能帶來很多驚喜

Serverless Framework

寫 JDBC 麻煩,就有了各種持久層框架的出現,同樣寫 AWS 原生 CFT 麻煩,就有了 Serverless Framework (以下會簡稱 SF)的出現幫助我們定義相關 Serverless 組件 (順便問一下,GraphQL 你們有在用嗎?)

SF 不但簡化了 AWS 原生 CFT 的編寫,還簡化了跨雲服務的定義,就好比設計模式當中的 Facade,在上面建立了一層門面,隱藏了底部不同服務的細節,降低了跨雲並用雲的門檻,目前支持的雲服務有下面這些

這裡暫時不會對 SF 展開深入的說明,在我們的 demo 中只不過是要應用 SF 來定義

安裝 Serverless Framework

如果你有安裝 Node,那只需要一條 npm 命令全局安裝即可:

npm update -g serverless

安裝過後檢查一下安裝版本是否成功

sls -version

配置 Serverless Framework

由於要使用 AWS 的 Lambda,所以要對 SF 做基本的配置,至少要讓 SF 有權限創建 AWS 服務,當你創建一個 AWS 用戶時,你可以獲取 AK 「access_key_id」和 SK 「secret_access_key」(不是 SKII 哦),其實就是一種用戶名和密碼形式

然後通過下面一條命令添加配置就可以了:

serverless config credentials --provider aws --key 1234 --secret 5678 --profile custom-profile

  • --provider 雲服務商
  • --key 你的AK
  • --secret 你的SK
  • --profile 如果你有多個帳戶時,你可以添加這個 profile 做快速區分

運行上述命令後,就會在 ~/.aws/目錄創建一個名為 credentials 的文件存儲上述配置,就像這樣:

到這裡準備工作就都完成了,開始寫我們的定義就好了

創建 Serverless 應用

通過下面一條命令創建 serverless 應用

sls create --template aws-nodejs --path ./demo --name lambda-sqs-lambda

  • --template 指定創建的模版
  • --path 指定創建的目錄
  • --name 指定創建的服務名稱

運行上述命令後,進入 demo 目錄就是下面這個結構和內容了

➜ demo tree.├── handler.js└── serverless.yml0 directories, 2 files

因為我們是用 Node.js 來編寫 Serverless 應用,同樣在 demo 目錄下執行下面命令來初始化該目錄,因為我們後面要用到兩個 npm package

npm init -y

現在的結構是這樣的(其實就多了一個 package.json):

➜ demo tree.├── handler.js├── package.json└── serverless.yml0 directories, 3 files

至此,準備工作都已就緒,接下來就在 serverless.yml 中寫相應的定義就可以了 (門檻很低:按照相應的 key 寫 YAML 即可,是不是很簡單?),打開 serverless.yml 文件來看一下,瞬間懵逼?

# Welcome to Serverless!## This file is the main config file for your service.# It's very minimal at this point and uses default values.# You can always add more config options for more control.# We've included some commented out config examples here.# Just uncomment any of them to get that config option.## For full config options, check the docs:# docs.serverless.com## Happy Coding!service: lambda-sqs-lambda# app and org for use with dashboard.serverless.com#app: your-app-name#org: your-org-name# You can pin your service to only deploy with a specific Serverless version# Check out our docs for more details# frameworkVersion: "=X.X.X"provider: name: aws runtime: nodejs12.x# you can overwrite defaults here# stage: dev# region: us-east-1# you can add statements to the Lambda function's IAM Role here# iamRoleStatements:# - Effect: "Allow"# Action:# - "s3:ListBucket"# Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] }# - Effect: "Allow"# Action:# - "s3:PutObject"# Resource:# Fn::Join:# - ""# - - "arn:aws:s3:::"# - "Ref" : "ServerlessDeploymentBucket"# - "/*"# you can define service wide environment variables here# environment:# variable1: value1# you can add packaging information here#package:# include:# - include-me.js# - include-me-dir/**# exclude:# - exclude-me.js# - exclude-me-dir/**functions: hello: handler: handler.hello# The following are a few example events you can configure# NOTE: Please make sure to change your handler code to work with those events# Check the event documentation for details# events:# - http:# path: users/create# method: get# - websocket: $connect# - s3: ${env:BUCKET}# - schedule: rate(10 minutes)# - sns: greeter-topic# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx# - iot:# sql: "SELECT * FROM 'some_topic'"# - cloudwatchEvent:# event:# source:# - "aws.ec2"# detail-type:# - "EC2 Instance State-change Notification"# detail:# state:# - pending# - cloudwatchLog: '/aws/lambda/hello'# - cognitoUserPool:# pool: MyUserPool# trigger: PreSignUp# - alb:# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/# priority: 1# conditions:# host: example.com# path: /hello# Define function environment variables here# environment:# variable2: value2# you can add CloudFormation resource templates here#resources:# Resources:# NewResource:# Type: AWS::S3::Bucket# Properties:# BucketName: my-new-bucket# Outputs:# NewOutput:# Description: "Description for the output"# Value: "Some output value"

乍一看,你可能覺得眼花繚亂,其實這是一個相對完整的 Lambda 配置全集,我們不需要這麼詳細的內容,不過這個文件作為我們的參考

接下來我們就定義 demo 所需要的一切 (關鍵注釋已經寫在代碼中)

service: name: lambda-sqs-lambda # 定義服務的名稱provider: name: aws # 雲服務商為 aws runtime: nodejs12.x # 運行時 node 的版本 region: ap-northeast-1 # 發布到 northeast region,其實就是東京 region stage: dev # 發布環境為 dev iamRoleStatements: # 創建 IAM role,允許 lambda function 向隊列發送消息 - Effect: Allow Action: - sqs:SendMessage Resource: - Fn::GetAtt: [ receiverQueue, Arn ]functions: # 定義兩個 lambda functions order: handler: app/order.checkout # 第一個 lambda function 程序入口是 app 目錄下的 order.js 裡面的 checkout 方法 events: # trigger 觸發器是 API Gateway 的方式,當接收到 /order 的 POST 請求時觸發該 lambda function - http: method: post path: order invoice: handler: app/invoice.generate # 第二個 lambda function 程序入口是 app 目錄下的 invoice.js 裡面的 generate 方法 timeout: 30 events: # trigger 觸發器是 SQS 服務,消息隊列有消息時觸發該 lambda function 消費消息 - sqs: arn: Fn::GetAtt: - receiverQueue - Arnresources: Resources: receiverQueue: # 定義 SQS 服務,也是 Lambda 需要依賴的服務 Type: AWS::SQS::Queue Properties: QueueName: ${self:custom.conf.queueName}# package:# exclude:# - node_modules/**custom: conf: ${file(conf/config.json)} # 引入外部定義的配置變量

config.json 內容僅僅定義了 queue 的名稱,只是為了說明配置的靈活性

{ "queueName": "receiverQueue"}

因為我們要模擬訂單的生成,這裡用 UUID 來模擬訂單號,

因為我們要調用 AWS 服務API,所以要使用 aws-sdk,

所以要安裝這兩個 package (這兩個理由夠充分嗎?)

{ "name": "lambda-sqs-lambda", "version": "1.0.0", "description": "demo for lambda", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "license": "MIT", "dependencies": { "uuid": "^8.1.0" }, "devDependencies": { "aws-sdk": "^2.6.15" }}

接下來,我們就可以編寫兩個 Lambda function 的代碼邏輯了

Order Lambda Function

訂單服務很簡單,接收一個下單請求,下單成功後快速返回給用戶,同時將訂單下單成功的消息發送到 SQS 中,供下遊發票服務開具發票使用

'use strict';const config = require('../conf/config.json')const AWS = require('aws-sdk');const sqs = new AWS.SQS();const { v4: uuidv4 } = require('uuid');module.exports.checkout = async (event, context, callback) => { console.log(event) let statusCode = 200 let message if (!event.body) { return { statusCode: 400, body: JSON.stringify({ message: 'No order body was found', }), }; } const region = context.invokedFunctionArn.split(':')[3] const accountId = context.invokedFunctionArn.split(':')[4] const queueName = config['queueName'] // 組裝 SQS 服務的 URL const queueUrl = `https://sqs.${region}.amazonaws.com/${accountId}/${queueName}` const orderId = uuidv4() try { // 調用 SQS 服務 await sqs.sendMessage({ QueueUrl: queueUrl, MessageBody: event.body, MessageAttributes: { orderId: { StringValue: orderId, DataType: 'String', }, }, }).promise(); message = 'Order message is placed in the Queue!'; } catch (error) { console.log(error); message = error; statusCode = 500; } // 快速返回訂單 ID return { statusCode, body: JSON.stringify({ message, orderId, }), };};

Invoice Lambda Function

發票服務邏輯同樣很簡單,消費 SQS 指定隊列中的消息,並將開具出的發票發送到客戶訂單信息的 email 中

module.exports.generate = (event, context, callback) => { console.log(event) try { for (const record of event.Records) { const messageAttributes = record.messageAttributes; console.log('OrderId is --> ', messageAttributes.orderId.stringValue); console.log('Message Body --> ', record.body); const reqBody = JSON.parse(record.body) // 睡眠 20 秒,模擬生成發票的耗時過程 setTimeout( () => { console.log("Receipt is generated and sent to :" + reqBody.email) }, 20000) } } catch (error) { console.log(error); }}

到此 demo 的代碼就全部實現了,從中你可以看到:

我們沒有關注 lambda 的底層服務細節,沒有關注 sqs 的服務,只是簡單的代碼邏輯實現以及服務之間的串聯定義

最後我們看一下整體的目錄結構吧:

.├── app│   ├── invoice.js│   └── order.js├── conf│   └── config.json├── package.json└── serverless.yml2 directories, 5 files

發布 Lambda 應用

在發布之前,編譯一下應用,安裝必須的 package「uuid 和 aws-sdk」

npm install

發布應用非常簡單,只需要一條命令:

sls deploy -v

運行上述命令後大概需要等帶幾十秒鐘, 在構建的最後,會列印出我們的構建服務信息:

上圖的 endpoints 就是我們一會要訪問的 API gateway 觸發 lambda 的入口,在調用之前,我們先到 AWS console 看一下我們定義的服務

lambda functions

SQS-receverQueue

API Gateway

S3

從上圖的構建信息中你應該還看到一個 S3 bucket 的名稱,我們並沒有創建 S3, 這是 SF 自動幫我們創建,用來存儲 lambda zip package 的

測試

調用 API gateway 的 endpoint 來測試 lambda

打開 SQS 服務,你會發現,接收到一條消息:

接下來我們看看 Invoice Lambda function 的消費情況,打開 CloudWatch 查看 log:

從 log 中可以看出程序「耗費」 20 秒後列印了向客戶郵件的 log(郵件也可以藉助 AWS SES 郵件服務來實現)

至此,一個完整的 demo 就完成了,實際編寫的代碼並沒有多少,就搞定了這麼緊密的串聯

刪除服務

Lambda 是按照調用次數進行收取費用的,為了防止造成額外的開銷,demo 結束後通常都會將服務銷毀,使用 SF 銷毀剛剛創建的服務也非常簡單,只需要在 serverless.yml 文件目錄執行這條命令:

sls remove

總結與感受

AWS Lambda 是 Serverless 的典型,藉助 Lambda 可以實現更小粒度的「服務」,無需服務搭建也加快了開發速度。Lambda 同樣可以結合 AWS 很多其服務,接收請求,將計算結果傳遞給下遊服務等。另外很多第三方合作夥伴也在加入 Lambda 的 trigger 大部隊,給 Lambda 更多觸發可能,同時,藉助 CI/CD,可以快速實現功能閉環

開通 AWS free tier,足夠你玩轉 Lambda :

趣味原創解析Java技術棧問題,將複雜問題簡單化,將抽象問題圖形化落地如果對我的專題內容感興趣,或搶先看更多內容,歡迎訪問我的博客

相關焦點

  • AWS Lambda更新—Python,VPC,延長的函數持續時間,調度,和更多
    它能輕鬆地按比例增加,來攝入和處理大量的數據,不需任何持久的基礎設施。為了支持這一流行的使用場景,你的Lambda函數現在可以運行長達五分鐘的時間了。一如既往地,在你創建函數時,你只需指定想要的超時時間。你的函數能夠諮詢上下文對象,查看它還有多少時間可用。
  • Amazon S3使用AWS CodeDeploy自動部署
    ", "Resource": "arn:aws:codedeploy:us-east-1:123ACCOUNTID:deploymentconfig:*" }, { "Effect": "Allow", "Action": "codedeploy:RegisterApplicationRevision", "Resource": "arn:aws:codedeploy:us-east
  • 帶你使用 AWS 無伺服器架構一步步打造個性化 API 接口
    本文將一步步帶你如何在沒有接口開發經驗的情況下輕鬆的使用 AWS 伺服器服務構建自己定製化的 API 接口。是不是很簡單,就這樣創建好了,如果你使用 awscli 創建的話,那更簡單:awsimport boto3import osimport uuidimport timefrom requests import getdef lambda_handler(event, context): recordId = str(uuid.uuid4()) ctime = time.strftime("%Y-%m-%d %H:%M:%S"
  • Java11要來了,不如學學Java8的Lambda
    今天就讓我們來通俗易懂的聊一聊Java8中的Lambda表達式~簡單上手通過Lamda表達式,可以變換為:new Thread(() -> System.out.println("thread"));針對這種實行,我們怎麼理解呢?
  • python 之lambda表達式
    在學習python的過程中,lambda的語法時常會使人感到困惑,lambda是什麼,為什麼要使用lambda,是不是必須使用lambda?lambda表達式算是python一種比較方便的語法了,主要作用是代替簡單的函數,免去了編寫函數的煩惱,語法也更加地簡潔。下面就簡單介紹一下lambda的使用場景。從本質上來講,lambda是一個函數,可以接受參數輸入。lambda 基礎用法   lambda表達式的基礎用法,做簡單的加減乘除,或者格式修改操作。
  • Java11要來了,不如學學Java8的Lambda - 碼農登陸
    今天就讓我們來通俗易懂的聊一聊Java8中的Lambda表達式~簡單上手通過Lamda表達式,可以變換為:new Thread(() -> System.out.println("thread"));針對這種實行,我們怎麼理解呢?
  • Python中的Lambda表達式
    Lambda函數的語法lambda arguments: expressionLambda函數可以具有任意數量的參數,但只能有一個表達式。>def square1(num):return num ** 2print(square(5)) # 輸出: 25在上面的lambda示例中,lambda x: x ** 2產生一個可以與任何名稱關聯的匿名函數對象。
  • Java8 lambda表達式
    java8中最大的變化就是引入了lambda表達式,一種緊湊的傳遞行為的方式,這也是本書剩下部分所要討論的內容,讓我們進入其中吧。編寫第一個lambda表達式swing是一個平臺無關的gui庫,在該庫中,有很多常見的習慣,比如為了知道用戶點點擊了什麼,註冊一個事件監聽器,這個事件監聽器可以執行一些操作響應用戶的輸入。
  • AWS Quick Start參考部署方案
    AWS Quick Start(快速上手)參考部署方案可以幫你在AWS雲上快速部署全功能企業軟體,並遵循AWS最優實踐所帶來的安全性和可用性。AWS CloudFormation模板實現了部署的自動化。部署手冊詳細地描述了整個架構和實施過程。
  • 什麼是lambda表達式
    簡介本文主要概括了其lambda表達式的用法和一個操作實例,希望大家看完這篇文章能對你有些幫助。lambda表達式是什麼是一個沒有名字的函數functionInterface1 = new FunctionInterface(){ @Override public void play(){ System.out.println(&34;); } }; functionInterface1.play(); // lambda
  • 資深架構師一文帶你吃透Lambda表達式
    { static void Main(string[] args) { Expression<del> myET = x => x * x; } }}1、表達式Lambda表達式位於 => 運算符右側的 lambda
  • Python匿名函數:Lambda表達式
    我們以一張圖形進入主題:從圖中我們可以看出lambda表達式幾點特徵:簡潔性,符合了Python的一貫宗旨;起到了函數的作用,但未顯示函數名稱,這就是匿名函數;有形參;有返回值的。【2】Lambda表達式如何實現函數功能?
  • 為什麼要使用Lambda表達式?
    為什麼要使用lambda表達式「lambda 表達式」是一段可以傳遞的代碼,因此它可以被執行一次或多次。在學習語法(甚至包括一些奇怪的術語)之前,我們先回顧一下之前在Java 中一直使用的相似的代碼塊。
  • 什麼時候使用Lambda函數?
    先來看個簡單 lambda 函數 >>> lambda x, y : x+y  <function <lambda> at 0x102bc1c80>   x 和 y 是函數的兩個參數,冒號後面的表達式是函數的返回值,你能一眼看出這個函數就是是在求兩個變量的和,但作為一個函數,沒有名字如何使用呢?
  • 三種基本用法、五種應用場景,理清C++的Lambda表達式
    lambda表達式是指能夠捕獲作用域中的變量的無名函數對象,狹義的理解,就是匿名函數。無論是在項目中,還是在開源網站,總是能夠看到lambda的身影。為了能夠輕鬆閱讀代碼,進而熟練地使用,本文首先將講解lambda表達式的基本語法、三種基本用法,然後介紹五種實際的應用場景,最有總結說明lambda表達式的作用。
  • 關於Java Lambda的教程,簡單明了
    真正缺少的就是一個名字,從這個角度來看,lambda表達式是一種匿名方法。:從概念來講,lambda表達式是一個匿名函數,它有籤名和方法體但是沒有名字當lambda表達式作為參數傳遞給方法時,接收方法把它當對象使用,在listFiles方法內部,lambda表達式是一個對象的引用,在這裡lambda表達式是一種常規的對象,比如有地址和類型。
  • lambda表達式、生成器類型與yield表達式
    lambda表達式lambda表示式用於在語句中直接定義一個匿名函數,匿名函數的結構完全和函數結構相同,僅是沒有函數名稱。使用lambda表示式定義的匿名函數內部不能包含語句,只能包含表達式,匿名函數的參數由調用方傳入。
  • python之父為什麼嫌棄lambda匿名函數?
    BNF 表示法是lambda_expr ::= &34; [parameter_list] &34; expression,也就是lambda 參數序列:表達式。接下來,本文就仔細聊一聊這個處境尷尬卻生命力頑強的 lambda 匿名函數吧!1、lambda 怎麼使用?lambda 函數通常的用法是結合 map()、reduce()、filter()、sorted() 等函數一起使用,這些函數的共性是:都可以接收其它函數作為參數。
  • 深度學習/目標檢測之python——lambda
    lambda是Python程式語言中使用頻率較高的一個關鍵字。那麼,什麼是lambda?它有哪些用法?一、一個語法 在Python中,lambda的語法是唯一的。其形式: lambda argument_list: expression。