发布于: Oct 26, 2022
继企业应用上云之后,微服务云应用平台又初露头角,如何利用 Amazon API Gateway 和 Amazon Web Services Lambda 实现 SAP 应用微服务化,本文就将分步为您演示。
- 在 VPC 私有子网中部署 SAP 应用。其中,SAP Gateway 是 OData API 的开发和运行环境,SAP 后台应用可以是 ERP/CRM/SCM 等。本示例采用的是 S/4 HANA(内嵌了 Gateway)。启用 Gateway 内置的 “RMTSAMPLEFLIGHT” 服务。该示例服务提供了一组管理航班旅行的 OData API。在后续的示例中,将展示如何在 API Gateway 创建 API 以查询和添加旅行社信息。
- 在 VPC 公共子网中部署 Lambda 函数 ”sapapi-proxy”,作为 API Gateway 调用后台 OData API 的代理。
- 定义安全组 “SAP” 对 SAP S/4 HANA进行隔离保护,即只允许来自安全组 “SAP Proxy” 的 Lambda 函数访问 OData API。
- 必须为 VPC 中的 Lambda 函数分配网络接口即 ENI (Elastic Network Interfaces) ,因此,需要定义 Amazon Web Services IAM 权限策略,授予 Lambda 函数管理 ENI 的权限。
- 在 Amazon CloudWatch Logs 创建 Flow Logs,对 API 调用过程中的网络流量进行监控。
以下将针对 API Gateway 的配置和 Lambda 函数的实现展开详细的介绍。
在 API Gateway 为 OData API 服务创建对应的资源和方法,这样前端应用的调用这些方法的请求将被传递给 Lambda 函数;而 Lambda 函数执行结束后,结果将返回给前端应用。其中,请求和响应内容均是按照预定义的 Body Mapping Templates 转换成 JSON 格式。
- 创建一个新的 API,并定义 ”travelagency” 的资源,然后声明 GET 和 POST 两个方法,分别用于实现“查询旅行社“和”添加旅行社“的服务调用;
- 在该资源的“Integration Request” 页面中配置与 Lambda 函数的集成方式(下图以 GET 方法为例);
-
为 GET 方法定义 URL 查询字符串
“agencynum”
,该字符串是查询请求的参数,例如:“/travelagency?agencynum=00000055”
;
-
为 GET 方法定义如下的 Body Mapping Template,从而 API Gateway 可以捕捉到请求里的必要参数信息并传递给 Lambda 函数,包括 SAP 应用的私网地址、端口、OData 服务路径、认证信息以及查询字符串
“agencynum”
;
- 为 POST 方法定义如下的 Body Mapping Template,其中,JSON 格式的 ”body” 是该方法的主要参数,定义了将要提交给 OData API 的数据,例如以下是待添加的旅行社信息:
{ "agencynum":"00133333", "NAME":"ACME Holiday", "STREET":"Jiuxianqiao Road", "POSTCODE":"100000", "CITY":"Beijing", "COUNTRY":"CN", "TELEPHONE":"010-88888888", "URL":"http://www.acmeholiday.aws", "LANGU":"CN", "CURRENCY":"CNY", "mimeType":"text/html" }
- 部署 API,并启用缓存功能,这样 API Gateway 将缓存请求的响应,从而降低对后台 SAP 应用的请求次数,并优化请求的响应延迟。
采用 Node.js 实现的 Lambda 函数负责:a). 接收 API Gateway 传递过来的请求和参数,根据不同的方法,转发给后台的 OData API endpoints;b). 针对 POST 方法,先调用 HTTP GET 方法请求 X-CSRF-Token 和 Cookie,然后调用 HTTP POST 方法提交待添加的数据;c). 将 OData API endpoints 的响应结果返回给 API Gateway。
- 创建执行 Lambda 函数所需的IAM 角色 “LambdaVpcProxyExecutionRole”,采用的权限策略如下:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws-cn:logs:*:*:*" }, { "Effect": "Allow", "Action": [ "ec2:CreateNetworkInterface", "ec2:DescribeNetworkInterfaces", "ec2:DetachNetworkInterface", "ec2:DeleteNetworkInterface" ], "Resource": "*" } ] }
- 创建 Lambda 函数 “sapapi-proxy”, 并赋予刚刚创建的 IAM 角色;
- 配置 Lambda 函数访问的 VPC 信息,包括子网和安全组(注意:Amazon Web Services 要求至少选择 2 个子网以在高可用性模式下运行 Lambda 函数);
- 实现 Lambda 函数,以下是接收请求和返回响应的主函数体代码:
exports.handler = (event, context, callback) => { const done = (err, res) => callback(null, { statusCode: err ? '400' : '200', body: err ? err.message : res, headers: { 'Content-Type': 'application/json', }, }); var endpoint = { host: event.requestParams.hostname, port: event.requestParams.port, path: event.requestParams.path, username: event.requestParams.username, password: event.requestParams.password }; switch (event.requestParams.httpMethod) { case 'GET': getTravelAgency(endpoint, event.requestParams.agencynum, done); break; case 'POST': postTravelAgency(endpoint, event.requestParams.body, done); break; default: done(new Error(`Unsupported methods "${event.requestParams.httpMethod}"`)); }}; • • 处理 GET 方法的 “getTravelAgency” 函数代码如下图所示: //return travel agency information based on parameters//'use strict';var http = require('http'); exports.getTravelAgency = (ep, params, callback) => { if (params == "" ) callback(new Error("Parameter 'carrierid' has not been provided")); var sAuth = 'Basic '; sAuth += new Buffer(ep.username + ':' + ep.password).toString('base64'); var headers = { 'Authorization': sAuth }; var options = { host : ep.host, port : ep.port, path : ep.path + "(\'"+ params+ "\')?$format=json", method : "GET", headers : headers }; var req=http.request(options,function(res){ res.setEncoding("utf-8"); var responseString = ''; res.on('data',function(chunk){ responseString += chunk; }); res.on('end', function () { callback(undefined, JSON.parse(responseString)); }); }); req.end(); req.on("error",function(err){ callback(new Error(err.message)); });} • • 处理 POST 方法的 “postTravelAgency” 函数代码如下图所示: //add new travel agency based on parameters//'use strict';var http = require('http');var xml2js = require('xml2js');//var sapapi = require('./sapapi');//var extsys = require('./settings').extsys; exports.postTravelAgency = (ep, data, callback) => { //callback(new Error(JSON.stringify(data))); if (data == "" ) callback(new Error("Parameter 'data' has not been provided")); var sAuth = 'Basic '; sAuth += new Buffer(ep.username + ':' + ep.password).toString('base64'); var oGetRequest = new Promise(function (resolve, reject) { // body... var headers = { 'Authorization': sAuth, 'x-csrf-token': "fetch" }; var options = { host : ep.host, port : ep.port, path : ep.path, method : "GET", headers : headers }; //request x-csrf-token var req=http.request(options,function(res){ resolve(res); }); req.setTimeout(60000, function () { reject( new Error("Server is unreachable")); }); req.end(); req.on('error', function (error) { reject(error); }); }); oGetRequest.then( //resolve function (oGetRes) { //payload from request var dataString = JSON.stringify(data); var headers = {}; headers['Authorization'] = sAuth; headers['Accept-Language'] = 'en'; headers['X-Requested-With'] = "XMLHttpRequest"; headers['Content-Type'] = 'application/json'; headers['X-CSRF-Token'] = oGetRes.headers['x-csrf-token']; headers['cookie'] = oGetRes.headers['set-cookie']; var options = { host : ep.host, port : ep.port, path : ep.path, method : "POST", headers : headers }; var req = http.request(options, function (res) { if (res.statusCode !== 201) return callback(new Error(res.statusCode)); res.setEncoding('utf-8'); var responseString = ''; res.on('data', function (data) { responseString += data; //callback(new Error(responseString)); }); res.on('end', function () { var parser = new xml2js.Parser(); //callback(undefined, parser.parseString(responseString)); callback(undefined, responseString); }); }); req.write(dataString); req.end(); req.on('error', function (error) { callback(new Error(error.message)); }); }, // reject function (err) { callback(new Error(err.message)); } );}
使用 Postman 对 API 进行测试,以下是调用 GET 方法即查询旅行社的测试结果。其中,”body” 是 SAP返回的 JSON 格式的 OData 资源描述信息。
以下是调用 POST 方法即添加新旅行社的测试结果。其中,”body” 是 SAP 返回的 Atom 格式的 OData 资源描述信息。
本文介绍了使用 API Gateway 与 Lambda 实现 SAP 应用的微服务,该方式无需将OData API endpoints暴露在公网,从而满足企业应用对于安全合规的要求;同时, Lambda 函数代理 CRSF 安全令牌的申请,为前端应用提供了更加透明的开发接口。
本示例在 API 调用请求中采用了基础的认证方式(即 HTTP 报文头中的 “Authorization” 字段),但是在生产环境中,建议采用 OAuth 2.0 的认证方式(例如在 Lambda 函数中实现这一认证授权的工作流程)。后续的博客文章将会展开介绍这部分工作。相关文章