無服務器的微服務
無服務器的微服務
在 2015年的LinuxCon/ContainerCon 上我呈現了一次演示驅動的演講,標題叫做“沒有服務器的微型服務”。 其中,我創建了一個圖片處理的微型服務,將其部署到了多個區域,構建了一個移動 app 并使用它(譯者注:指的是這個微型服務)作為后臺,添加了一個使用了 Amazon API 網關的基于 HTTP 的 API 和一個網站,并且對它進行了單元和負載測試,所有這些都沒有用到任何服務器。
這篇博文對演講的細節進行了重制,為你逐步完成所有必要的比周,并深入到了架構中去。而高層次的概述,可以看看這里的 幻燈片。還有這一架構的另外一個示例,可以看看這個可執行的 gist 資源庫,SquirrelBin。
無服務器架構
這里“無服務的”, 我們的意思是不需要明確的基礎設施,如:沒有服務器,沒有要對服務器進行的部署,沒有任何類型軟件的安裝。我們將只使用被管理的云服務和一臺筆記本電腦。下面的圖形描述了高級別的組件及他們的連接: 一個 Lambda 函數作為計算器(“后臺”) 以及一個直接連接到計算器上的移動app, 再加上 Amazon API 網關,來提供一個 Amazon S3 所托管靜態網站的 HTTP 端點.
一個使用 AWS Lambda 的移動和 Web 應用無服務器架構
現在,讓我們開始構建吧!
步驟一 1: 創建圖像處理服務
為了使得過程跟進起來更加容易一點,我們將使用一個內置了 Lambda 的 nodejs 語言庫:ImageMagick。不過,那不是必須的 —— 如果你選擇使用自己的庫做替換,你可以 加載JavaScript或者本地庫, 運行Python,或者甚至去封裝一個 命令行的可執行程序。下面的示例使用 nodejs 實現的,但你也可以使用 Java, Clojure, Scala 來構建這項服務, 或者使用 AWS Lambda 中其他基于 jvm 的語言。
下面的代碼是一種“hello world” 類型的程序,用來演示 ImageMagick —— 它給我提供了一個基礎的命令架構 (又叫做 switch 語句) 并且讓我們可以獲取到內置的玫瑰圖片并返回它。除了對結果進行編碼,那樣它就可以很好的以 JSON 的形式存在,做這個并沒有太多東西。
var im = require("imagemagick"); var fs = require("fs"); exports.handler = function(event, context) { if (event.operation) console.log("Operation " + event.operation + " requested"); switch (event.operation) { case 'ping': context.succeed('pong'); return; case 'getSample': event.customArgs = ["rose:", "/tmp/rose.png"]; im.convert(event.customArgs, function(err, output) { if (err) context.fail(err); else { var resultImgBase64 = new Buffer(fs.readFileSync("/tmp/rose.png")).toString('base64'); try {fs.unlinkSync("/tmp/rose.png");} catch (e) {} // discard context.succeed(resultImgBase64); } }); break; // allow callback to complete default: var error = new Error('Unrecognized operation "' + event.operation + '"'); context.fail(error); return; } };
首先,讓我們確保服務是運行著的,可以通過在 AWS Lambda 控制臺的測試窗口向它發送下面的 JSON:
{ "operation": "ping" }
你應該會得到必要的 “pong” 回應。接下來,我們將通過發送像下面這樣的 JSON 來實際調用到 ImageMagick :
{ "operation": "getSample" }
這一請求獲取的是表示一張 PNG 版本玫瑰圖片的 base64 編碼的字符串: “”iVBORw0KGg…Jggg==”. 為了確認這個并不只是一些隨機的字符, 將它復制粘貼(沒有雙引號) 到任何方便使用的 Base64-到-圖片 解碼器, 比如 codebeautify.org/base64-to-image-converter. 你應該能看到一張漂亮的玫瑰圖片:
樣例圖片 (紅玫瑰)
現在,讓我們通過打開它的 Nodejs 包的剩余部分來完成圖像處理服務。我們將提供一些不同的操作:
-
ping: 用于驗證服務的可用性。
-
getDimensions: 用于調用識別(identify)操作來獲取圖像的寬度和高度的快捷方式。
-
identify: 獲取圖像元數據。
-
resize: 一個便捷的調整大小的程序(又稱為封面圖片的轉換convert)。
-
thumbnail: resize的同義詞。
-
convert: 一個萬能程序 —— 可以轉換媒體格式,應用變換,調整大小,等等。
-
getSample: 獲取示例圖像; 入門的基本操作。
大部分的代碼是非常簡單的由 Nodejs ImageMagick 封裝的程序,其中一些以 JSON 方式編碼(在這種情況下,傳遞給 Lambda 的事件被清理并向前傳遞),另一些以命令行(又名“自定義”)參數方式傳遞一個字符串數組。如果你之前從來沒有使用過 ImageMagick,那么,ImageMagick 作為命令行的包裝器并且文件名具有語義含義的要求可能是不被引起注意的。
我們有兩個相互矛盾的需求:我們希望客戶端傳遞語法格式(例如,輸出圖像的格式是 PNG 或者是 JPEG),但我們同時也要求服務器來決定在磁盤上何處放置臨時存儲,以便我們不遺漏具體的實現細節。為了同時實現這兩個目標,我們在 JSON 模式中定義了兩個參數:“inputExtension” 和“outputExtension”,然后,我們通過將客戶端的部分(文件擴展名)與服務器的部分(目錄和基名)相結合,構建實際的文件位置。你可以看到(和使用!)圖像處理計劃大綱(image processing blueprint)的已完成代碼。
有很多測試你都可以在這里運行(我們也會在后面做更多工作),但就像一個快速而明智的檢測一樣,檢索一個樣本會再次創建圖像并使用一個否定(顏色轉變)過濾器將其回傳。你可以在 Lambda 窗體中使用 JSON 這類工具,僅僅是用實際的圖像單元替代基于 64 位的圖像場(要在這個博客頁面下包含這個有點長)。
{ "operation": "convert", "customArgs": [ "-negate" ], "outputExtension": "png", "base64Image": "...fill this in with the rose sample image, base64-encoded..."}
輸出,解碼為一個圖像,應該是一個難懂的植物珍品,一個藍玫瑰:
藍色玫瑰(紅色玫瑰樣品圖像的底片)
因此這所有的是服務的函數方面的內容。通常,在這個地方起初會變得丑陋,從“一次工作”到“具備 24x7x365 監控和生產記錄的可伸縮和可靠的服務“。但這就是 Lambda 的漂亮所在:我們的圖像進程代碼已經是被完全摧毀了的,生產強度也是微服務。接下來,讓我們加入一個可以尋呼的移動 app 吧...
步驟2: 創建一個移動客戶端
我們的圖像處理微服務可以以多種方式訪問,但是為了展示一個樣板客戶端,我們將建立一個快速的Android app。下面我展示的客戶端代碼,是我們在 ContainerCon 演講中創建的一個簡單的 Android 應用程序。它允許你選擇一個圖像和一個濾波器,然后通過調用運行在 AWS Lambda 的圖像處理服務的“轉換”操作,最終顯示使用過濾器處理后的圖像效果。
下面的場景顯示了應用程序的工作原理,其中一個是它的示例圖片 --AWS Lambda 的圖標:
Android 模擬器顯示 AWS Lambda 的圖標
我們將選擇“相反(negate)”過濾器來反轉圖標的顏色:
選擇“相反(negate)”圖像轉換濾波器
下面是結果:一個藍色版本的 Lambda 圖標(原始版本為橙色):
使用“相反(negate)”濾鏡處理后的 AWS Lambda 圖標的結果
我們還可以選擇西雅圖照片并使用深褐色濾鏡處理,使得圖片中的現代的西雅圖天空有一種懷舊感
深褐色濾鏡處理后的西雅圖天空。
現在回到代碼上面來吧。這里我不會試著去教授基礎的 Android 編程,只特地專注于這個應用的 Lambda 元素。(如果你在創建自己的應用,你也會需要包含 AWS Mobile SDK 的 jar 包,以運行下面的示例代碼) 。從概念上來講有這么四個部分:
-
POJO 數據模式
-
遠程服務(操作)定義
-
初始化
-
服務調用
我們將會逐一地來看看各個部分。
數模模式定義了任何需要在客戶端和服務器之間進行傳遞的對象。這里沒有“Lambda形式”的東西; 這些對象都只是 POJO(普通的 Java 對象),沒有特殊的庫或者框架。我們定義了一個基礎事件,然后對它進行了擴展以反映我們的操作結構 – 你可以把這當做是之前我們定義和測試圖像處理服務所用到的 JSON 的“Java 狀態”。如果你也在使用 Java 編寫服務端,那你通常就應該會把這些文件共享出來作為通用時間結構定義的一部分;在我們的示例中,這些 POJO 會在服務端被轉換成 JSON。
LambdaEvent.java
package com.amazon.lambda.androidimageprocessor.lambda; public class LambdaEvent { private String operation; public String getOperation() {return operation;} public void setOperation(String operation) {this.operation = operation;} public LambdaEvent(String operation) {setOperation(operation);}}
ImageConvertRequest.java
package com.amazon.lambda.androidimageprocessor.lambda; import java.util.List; public class ImageConvertRequest extends LambdaEvent { private String base64Image; private String inputExtension; private String outputExtension; private List customArgs; public ImageConvertRequest() {super("convert");} public String getBase64Image() {return base64Image;} public void setBase64Image(String base64Image) {this.base64Image = base64Image;} public String getInputExtension() {return inputExtension;} public void setInputExtension(String inputExtension) {this.inputExtension = inputExtension;} public String getOutputExtension() {return outputExtension;} public void setOutputExtension(String outputExtension) {this.outputExtension = outputExtension;} public List getCustomArgs() {return customArgs;} public void setCustomArgs(List customArgs) {this.customArgs = customArgs;}}
到目前為止還不是很復雜。現在我們有了一個數據模型,再就是將要使用一些 Java 注解來定義服務端點。這里我們會暴露出兩個操作, “ping” 以及“convert”; 這也能很容易通過添加其它注解來對其進行擴展,但就下面這個示例應用而言,我們暫時還不需要這么做。
ILambdaInvoker.java
package com.amazon.lambda.androidimageprocessor.lambda; import com.amazonaws.mobileconnectors.lambdainvoker.LambdaFunction; import java.util.Map; public interface ILambdaInvoker { @LambdaFunction(functionName = "ImageProcessor") String ping(Map event); @LambdaFunction(functionName = "ImageProcessor") String convert(ImageConvertRequest request);}
現在我們已經準備好來做這個應用主要部分了。這里大部分都是樣板式的 Android 代碼或者簡單客戶端資源管理,而我將會點出幾個跟 Lambda 相關的部分:
這就是“init”部分;它創建了身份驗證功能來調用 Lambda API 并創建了一個能夠調用上面所定義的端點,而且能在我們的數據模型中傳送 POJO 的 Lambda 調用:
// Create an instance of CognitoCachingCredentialsProvider CognitoCachingCredentialsProvider cognitoProvider = new CognitoCachingCredentialsProvider( this.getApplicationContext(), "us-east-1:<YOUR COGNITO IDENITY POOL GOES HERE>", Regions.US_EAST_1); // Create LambdaInvokerFactory, to be used to instantiate the Lambda proxy. LambdaInvokerFactory factory = new LambdaInvokerFactory(this.getApplicationContext(), Regions.US_EAST_1, cognitoProvider); // Create the Lambda proxy object with a default Json data binder. lambda = factory.build(ILambdaInvoker.class);
其余的也挺有趣的部分代碼就是它自身實際的遠程過程調用了:
try { return lambda.convert(params[0]); } catch (LambdaFunctionException e) { Log.e("Tag", "Failed to convert image"); return null; }
實際上也不那么有趣,因為這戲法(參數序列化和結果的反序列化)是發生在幕后的,留給我們的僅僅只是一些錯誤的處理而已。
下面是完整的代碼文件:
MainActivity.java
package com.amazon.lambda.androidimageprocessor; import android.app.Activity; import android.app.ProgressDialog; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.os.Bundle; import android.util.Base64; import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.Spinner; import android.widget.Toast; import com.amazon.lambda.androidimageprocessor.lambda.ILambdaInvoker; import com.amazon.lambda.androidimageprocessor.lambda.ImageConvertRequest; import com.amazonaws.auth.CognitoCachingCredentialsProvider; import com.amazonaws.mobileconnectors.lambdainvoker.LambdaFunctionException; import com.amazonaws.mobileconnectors.lambdainvoker.LambdaInvokerFactory; import com.amazonaws.regions.Regions; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; public class MainActivity extends Activity { private ILambdaInvoker lambda; private ImageView selectedImage; private String selectedImageBase64; private ProgressDialog progressDialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Create an instance of CognitoCachingCredentialsProvider CognitoCachingCredentialsProvider cognitoProvider = new CognitoCachingCredentialsProvider( this.getApplicationContext(), "us-east-1:2a40105a-b330-43cf-8d4e-b647d492e76e", Regions.US_EAST_1); // Create LambdaInvokerFactory, to be used to instantiate the Lambda proxy. LambdaInvokerFactory factory = new LambdaInvokerFactory(this.getApplicationContext(), Regions.US_EAST_1, cognitoProvider); // Create the Lambda proxy object with a default Json data binder. lambda = factory.build(ILambdaInvoker.class); // ping lambda function to make sure everything is working pingLambda(); } // ping the lambda function @SuppressWarnings("unchecked") private void pingLambda() { Map event = new HashMap(); event.put("operation", "ping"); // The Lambda function invocation results in a network call. // Make sure it is not called from the main thread. new AsyncTask<Map, Void, String>() { @Override protected String doInBackground(Map... params) { // invoke "ping" method. In case it fails, it will throw a // LambdaFunctionException. try { return lambda.ping(params[0]); } catch (LambdaFunctionException lfe) { Log.e("Tag", "Failed to invoke ping", lfe); return null; } } @Override protected void onPostExecute(String result) { if (result == null) { return; } // Display a quick message Toast.makeText(MainActivity.this, "Made contact with AWS lambda", Toast.LENGTH_LONG).show(); } }.execute(event); } // event handler for "process image" button public void processImage(View view) { // no image has been selected yet if (selectedImageBase64 == null) { Toast.makeText(this, "Please tap one of the images above", Toast.LENGTH_LONG).show(); return; } // get selected filter String filter = ((Spinner) findViewById(R.id.filter_picker)).getSelectedItem().toString(); // assemble new request ImageConvertRequest request = new ImageConvertRequest(); request.setBase64Image(selectedImageBase64); request.setInputExtension("png"); request.setOutputExtension("png"); // custom arguments per filter List customArgs = new ArrayList(); request.setCustomArgs(customArgs); switch (filter) { case "Sepia": customArgs.add("-sepia-tone"); customArgs.add("65%"); break; case "Black/White": customArgs.add("-colorspace"); customArgs.add("Gray"); break; case "Negate": customArgs.add("-negate"); break; case "Darken": customArgs.add("-fill"); customArgs.add("black"); customArgs.add("-colorize"); customArgs.add("50%"); break; case "Lighten": customArgs.add("-fill"); customArgs.add("white"); customArgs.add("-colorize"); customArgs.add("50%"); break; default: return; } // async request to lambda function new AsyncTask() { @Override protected String doInBackground(ImageConvertRequest... params) { try { return lambda.convert(params[0]); } catch (LambdaFunctionException e) { Log.e("Tag", "Failed to convert image"); return null; } } @Override protected void onPostExecute(String result) { // if no data was returned, there was a failure if (result == null || Objects.equals(result, "")) { hideLoadingDialog(); Toast.makeText(MainActivity.this, "Processing failed", Toast.LENGTH_LONG).show(); return; } // otherwise decode the base64 data and put it in the selected image view byte[] imageData = Base64.decode(result, Base64.DEFAULT); selectedImage.setImageBitmap(BitmapFactory.decodeByteArray(imageData, 0, imageData.length)); hideLoadingDialog(); } }.execute(request); showLoadingDialog(); } /* Select methods for each image */ public void selectLambdaImage(View view) { selectImage(R.drawable.lambda); selectedImage = (ImageView) findViewById(R.id.static_lambda); Toast.makeText(this, "Selected image 'lambda'", Toast.LENGTH_LONG).show(); } public void selectSeattleImage(View view) { selectImage(R.drawable.seattle); selectedImage = (ImageView) findViewById(R.id.static_seattle); Toast.makeText(this, "Selected image 'seattle'", Toast.LENGTH_LONG).show(); } public void selectSquirrelImage(View view) { selectImage(R.drawable.squirrel); selectedImage = (ImageView) findViewById(R.id.static_squirrel); Toast.makeText(this, "Selected image 'squirrel'", Toast.LENGTH_LONG).show(); } public void selectLinuxImage(View view) { selectImage(R.drawable.linux); selectedImage = (ImageView) findViewById(R.id.static_linux); Toast.makeText(this, "Selected image 'linux'", Toast.LENGTH_LONG).show(); } // extract the base64 encoded data of the drawable resource `id` private void selectImage(int id) { Bitmap bmp = BitmapFactory.decodeResource(getResources(), id); ByteArrayOutputStream stream = new ByteArrayOutputStream(); bmp.compress(Bitmap.CompressFormat.PNG, 100, stream); selectedImageBase64 = Base64.encodeToString(stream.toByteArray(), Base64.DEFAULT); } // reset images to their original state public void reset(View view) { ((ImageView) findViewById(R.id.static_lambda)).setImageDrawable(getResources().getDrawable(R.drawable.lambda, getTheme())); ((ImageView) findViewById(R.id.static_seattle)).setImageDrawable(getResources().getDrawable(R.drawable.seattle, getTheme())); ((ImageView) findViewById(R.id.static_squirrel)).setImageDrawable(getResources().getDrawable(R.drawable.squirrel, getTheme())); ((ImageView) findViewById(R.id.static_linux)).setImageDrawable(getResources().getDrawable(R.drawable.linux, getTheme())); Toast.makeText(this, "Please choose from one of these images", Toast.LENGTH_LONG).show(); } private void showLoadingDialog() { progressDialog = ProgressDialog.show(this, "Please wait...", "Processing image", true, false); } private void hideLoadingDialog() { progressDialog.dismiss(); } }
這就是這個移動應用所需要的了:一個數據模型(又叫做 Java 類),一個控制模型(又叫做成對的方法),三個用來對一些東西進行初始化的語句,而后就是一個被 try/catch 塊包圍起來的遠程調用了 … 夠簡單。
多區域部署
到目前為止我們還沒有更多討論代碼運行的環境。Lambda 會指定一個區域部署你的代碼,但你必須決定你想要在哪個(或哪些)區域運行它。在我初始的版本中,我在美國東1區(又名弗吉尼亞數據中心)創建了初始程序。為了能夠在網絡中獲得更好地體驗,我們建立了一個全球性的服務,我們把它擴展到包括 eu-west-1(愛爾蘭)和 ap-northeast-1(東京),這樣我們的移動應用程序可以從世界各地快速地連接:
一種在兩個附加的區域內部署 Lambda 功能的無服務器機制
下面的內容我們已經在博客中提到:在 S3 部署博客中,我展示了如何使用 lambda 函數部署其他存儲在亞馬遜 S3 的 lambda 函數壓縮文件。在 ContainerCon 演示中,我們搭建了小型的平臺并打開了 S3 跨區域復制,這樣我們就可以以 ZIP 壓縮文件的方式上傳圖片處理服務到愛爾蘭數據中心,并自動拷貝到東京數據中心,然后將部署在兩個區域的服務關連起來,形成了各自區域的 Lambda 服務。
快來享受無服務器的解決方案吧:)
搭建無服務器 web 應用,第一部分:API 端點
經過上面的步驟,我們已經擁有了一個可以工作的 mobile 應用程序和一個為 mobile 應用程序提供圖片處理服務的后端,這個后端通過 AWS Lambda 服務部署到了全球多個節點。下面讓我們把注意力集中到為那些喜歡使用瀏覽器的群體創建一個無服務器的web應用。我們將分2步來實現它。第一步:我們將創建API端點,這個API端點對外提供了圖片處理服務;第二步:我們將在Amazon S3上面創建真正的web應用。在這個章節,我們將實現第一步的內容。
AWS Lambda 通過提供一個內置的 web service 前端來實現將一段具體的代碼變成一個 service,但是在訪問這個 service 的時候,需要客戶端使用 AWS 提供的身份信息進行對服務請求進行簽名(在我們上一節創建的 Android 應用中,我們通過使用 Amazon Congnito 驗證客戶端來實現這個功能)。但我們創建一個 web 應用去調用圖片處理服務的時候,應該如何實現這個簽名的功能呢?
為了完成這個,我們將轉向另外的服務器, Amazon API Gateway。這個服務讓你能定義一個 API 而不需要任何架構-這個 API 是由 AWS 完全管理的。我們將用這個 API 網關去創建 1 個 URL 給圖像處理服務,該服務的眾多能力之一就是能給在線的任何用戶提供訪問。亞馬遜 API 網關提供了眾多途徑來控制 API 的訪問:API 調用被 AWS 證書簽名來認證,你可以使用 OAuth 標志并且能簡單將標志頭用于認證,你也可以使用 API 鑰匙(不推薦,因為是不安全的),或者讓 API 變成公共的,如我們馬上在這兒演示的。
另外 API 網關提供了多種多樣的訪問模型,我們不會全在這次來探索。有些是內建的(比如防 DDOS 保護)和其他的,比如緩存化,可以為某些重復訪問的流行的圖片減少延時和訪問代價。通過在客戶端和(微)服務插入一個間接層,透過 API 網關,這些文件也能把通過自身的版本和分階段特性來獨立更新。從現在開始,我們將要聚焦和暴露我們的圖像處理服務 API 的基本任務了。
OK,讓我們來創建我們的 API。在 AWS 控制臺上,選擇 API 網關然后選擇“新API”,為這個 API 提供一個名字和可選的描述。在我的例子中,我取名“ImageAPI”.
下一步,為你的新 API 創建一個資源(我把它叫做"ImageProcessingService"),然后在其中創建1個 POST 方法。選擇“Lambda函數”作為集成類型,然后輸入使用你的圖像處理服務的 Lambda 函數的名字。在“Method Request”配置中,設置認證類型為空類型。(也就是說,這將會是公用的端點)。這就差不多了。
為了測試集成成功與否,點擊"Test"按鈕:
然后提供一個測試負荷比如{“operation”: “ping”}。你應該得到期待結果“pong” ,提示你已經成功把你的 API 和你的 Lambda 函數連接起來了。
旁白:我們會得到更多和(更深層次)的測試,但是我有時覺得有用的事是在我的 API 中添加一個頂級資源的 GET 方法,這會變得簡單,像 ping 操作一樣,讓我很快可以用任何瀏覽器快速審查,正如預期的那樣,我的 API 是鏈接到我的 Lambda 函數。雖說這不需要演示(或總的來說),但您會發現它有用。
接下來會發生什么(S3靜態內容),我們要使 CORS 成為必須。這總的來說是簡單的,主要有這么幾個步驟。API Gateway 團隊會繼續讓它變得簡單,在這里不是重復指令(并有可能讓他們很快變得過時),我將會在文檔中告訴你。
點擊“部署這個 API ”按鈕。然后,你的所有設置就用在了你的網站上了。
A Serverless Web App, Part 2: Static Website Hosting in Amazon S3
This part is easy – upload the following Javascript website code to your S3 bucket of choice:
var ENDPOINT = 'https://fuexvelc41.execute-api.us-east-1.amazonaws.com/prod/ImageProcessingService';angular.module('app', ['ui.bootstrap']) .controller('MainController', ['$scope', '$http', function($scope, $http) { $scope.loading = false; $scope.image = { width: 100 }; $scope.ready = function() { $scope.loading = false; }; $scope.submit = function() { var fileCtrl = document.getElementById('image-file'); if (fileCtrl.files && fileCtrl.files[0]) { $scope.loading = true; var fr = new FileReader(); fr.onload = function(e) { $scope.image.base64Image = e.target.result.slice(e.target.result.indexOf(',') + 1); $scope.$apply(); document.getElementById('original-image').src = e.target.result; // Now resize! $http.post(ENDPOINT, angular.extend($scope.image, { operation: 'resize', outputExtension: fileCtrl.value.split('.').pop() })) .then(function(response) { document.getElementById('processed-image').src = "data:image/png;base64," + response.data; }) .catch(console.log) .finally($scope.ready); }; fr.readAsDataURL(fileCtrl.files[0]); } }; }]);
And here’s the HTML source we used for the (very basic) website in the demo:
<!DOCTYPE html><html lang="en"><head> <title>Image Processing Service</title> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" > <link rel="stylesheet" type="text/css" > <link rel="stylesheet" type="text/css" href="main.css"></head><body ng-app="app" ng-controller="MainController"> <div class="container"> <h1>Image Processing Service</h1> <div class="row"> <div class="col-md-4"> <form ng-submit="submit()"> <div class="form-group"> <label for="image-file">Image</label> <input id="image-file" type="file"> </div> <div class="form-group"> <label for="image-width">Width</label> <input id="image-width" class="form-control" type="number" ng-model="image.width" min="1" max="4096"> </div> <button type="submit" class="btn btn-primary"> <span class="glyphicon glyphicon-refresh" ng-if="loading"></span> Submit </button> </form> </div> <div class="col-md-8"> <accordion close-others="false"> <accordion-group heading="Original Image" is-open="true"> <img id="original-image" class="img-responsive"> </accordion-group> <accordion-group heading="Processed Image" is-open="true"> <img id="processed-image" class="img-responsive"> </accordion-group> </accordion> </div> </div> </div> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.min.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.3/ui-bootstrap.min.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.3/ui-bootstrap-tpls.min.js"></script> <script type="text/javascript" src="main.js"></script></body></html>
Finally, here’s the CSS:
body { font-family: 'Open Sans', sans-serif; padding-bottom: 15px;}a { cursor: pointer;}/** LOADER **/.glyphicon-refresh { -animation: spin .7s infinite linear; -webkit-animation: spin .7s infinite linear;}@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); }}@-webkit-keyframes spin { from { -webkit-transform: rotate(0deg); } to { -webkit-transform: rotate(360deg); }}
…then turn on static website content serving in S3:
The URL will depend on your S3 region and object names, e.g. “http://image-processing-service.s3-website-us-east-1.amazonaws.com/”. Visit that URL in a browser and you should see your image website:
Unit and Load Testing
With API Gateway providing a classic URL-based interface to your Lambda microservice, you have a variety of options for testing. But let’s stick to our serverless approach and do it entirely without infrastructure or even a client!
First, we want to make calls through the API. That’s easy; we use Lambda’s HTTPS invocation blueprint to POST to the endpoint we got when we deployed with API Gateway:
{ "options": { "host": "fuexvelc41.execute-api.us-east-1.amazonaws.com", "path": "/prod/ImageProcessingService", "method": "POST" }, "data": { "operation": "getSample" }}
Now that we have that, let’s wrap a unit test around it. Our unit test harness doesn’t do much; it just runs another Lambda function and pops the result into an Amazon DynamoDB table that we specify. We’ll use the unit and load test harness Lambda blueprint for this in its “unit test” mode:
{ "operation": "unit", "function": "HTTPSInvoker", "resultsTable": "unit-test-results", "testId": "LinuxConDemo", "event": { "options": { "host": "fuexvelc41.execute-api.us-east-1.amazonaws.com", "path": "/prod/ImageProcessingService", "method": "POST" }, "data": { "operation": "getSample" } }}
Finally, we ‘ll do a simple load test by running the unit test multiple times. We’ll use the Lambda unit and load test harness again, this time in “load test” mode:
{ "operation": "load", "iterations": 100, "function": "TestHarness", "event": { "operation": "unit", "function": "HTTPSInvoker", "resultsTable": "unit-test-results", "testId": "LinuxConLoadTestDemo", "event": { "options": { "host": "fuexvelc41.execute-api.us-east-1.amazonaws.com", "path": "/prod/ImageProcessingService", "method": "POST" }, "data": { "operation": "getSample" } } }}
Here’s a picture of our serverless testing architecture:
A Serverless Unit and Load Test Harness
你可以簡單地改變這個方法來合并驗證,運行各種各樣的單元測試,等等。如果你不需要web應用的基礎設施,你可以跳過API網關和HTTP調用,并在你的單元測試中直接運行圖像處理服務。如果你想要匯總或分析測試輸出,你可以簡單附加一個Lambda函數作為一個事件處理添加到DynamoDB表中,來保留測試結果。
總結
這真是一篇很長的文章,但是它包含著建立一個真實的,可擴展的前(移動端/網頁)后端服務的內容,所有這些內容不需要服務器或其他基礎設施系統的任何部分:前端,后端,API,部署,或者測試。來吧,無服務!
下一次,只需要有快樂的Lambda (無服務,微服務)編程!
-Tim
本文地址:http://www.oschina.net/translate/microservices-without-the-servers
原文地址:http://aws.amazon.com/cn/blogs/compute/microservices-without-the-servers/