无服务器的微服务

bger 8年前

无服务器的微服务

在 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 实现的,但你也可以使用 JavaClojureScala 来构建这项服务, 或者使用 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 包,以运行下面的示例代码) 。从概念上来讲有这么四个部分:

  1.  POJO 数据模式

  2.  远程服务(操作)定义

  3.  初始化

  4.  服务调用

我们将会逐一地来看看各个部分。

数模模式定义了任何需要在客户端和服务器之间进行传递的对象。这里没有“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" href="https://cdnjs.cloudflare.com/ajax/libs/推ter-bootstrap/3.3.4/css/bootstrap.min.css">      <link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Open+Sans:400,700">      <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