企业应用(如 ERP、CRM、OA 和 HR 等)是企业信息系统的核心资产,支撑企业的生产、经营与管理。随着移动互联网的蓬勃发展,这些企业应用迫切需要向移动设备提供便捷、高效的访问,实现丰富的用户体验。例如,销售人员通过手机 App 管理客户信息、拜访记录和订单等;差旅途中的管理人员在平板电脑上浏览财务报表或审批公文等。此外,集团型的企业客户日益注重 IT 服务能力的输出与共享,以促进各业务板块及生态圈的协同发展。例如,构建统一的身份认证平台为所有的第三方应用提供认证与授权服务。
一般来说,企业应用通过开放 API (Application programming interfaces)的方式实现应用集成与能力共享。例如,SAP 应用(如 ERP/CRM/SRM/SCM/PLM等)发布的是基于 OData (Open Data Protocol) 协议封装的API,而且满足 REST 设计风格(关于 OData 协议的更多介绍,请参见 SAP 官方博客)。这些 API 为企业应用在云上的微服务化提供了机会。此外,还需要 API 管理平台对 API 进行统一的管理,包括发布与部署、安全认证、流量控制和监控告警等。Amazon API Gateway 是实现 API 管理平台的托管式服务,它提供了统一、安全、敏捷及可扩展的 API 生产与消费方式。API Gateway 可以创建 API直接与后台的各类企业应用集成;也可以结合 AWS Lambda,实现定制化的业务逻辑与管理功能,构建轻量级、松耦合的无服务器式微服务。利用 API Gateway 和 Lambda 实现微服务的另外一个显著优势是,可以充分发挥无服务器架构中缓存和动态扩容的特性,降低前端应用对后台企业应用的访问压力,并优化用户体验。关于API Gateway 的更多特性,请参见产品主页。
我的同事 KK Ramamoorthy 在近期的一篇文章中介绍了利用 API Gateway 部署 SAP API 的方法,移动 App 和 Web 应用可以通过 API Gateway 直接访问 SAP 开放的 OData API endpoints,轻松实现 API 的调用。
根据企业对云上环境的安全分区要求及 AWS 最佳实践,SAP 应用一般是部署在 VPC 的私有子网,不允许被公网直接访问,因此,API Gateway 无法创建 API 直接调用位于私有子网的 OData API endpoints。此外, OData API采用了安全令牌机制防范 CSRF (Cross-site request forgery) 攻击(即在调用 POST 方法之前必须先请求获得 X-CSRF-Token),这一机制也限制了 API Gateway 直接对 SAP 应用进行微服务化。针对上述两个问题,本文将介绍通过 Lambda 函数结合 API Gateway 实现安全、灵活的 SAP 应用微服务。Lambda 函数完成的工作机制如下:
作为 API Gateway 的代理,将调用请求转发到位于私有子网的 OData API endpoint;
对于 API Gateway的 GET 方法,直接提交 GET 请求;
对于 API Gateway的 POST 方法,先请求 CSRF 令牌,再提交 POST 请求;
实现基本的身份认证(用户名与密码)。
部署架构
在 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) ,因此,需要定义 AWS IAM 权限策略,授予 Lambda 函数管理 ENI 的权限。
在 Amazon CloudWatch Logs 创建 Flow Logs,对 API 调用过程中的网络流量进行监控。
以下将针对 API Gateway 的配置和 Lambda 函数的实现展开详细的介绍。
API Gateway 的配置
在 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 应用的请求次数,并优化请求的响应延迟。
Lambda 函数的实现与部署
采用 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 信息,包括子网和安全组(注意:AWS 要求至少选择 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) {
处理 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 函数中实现这一认证授权的工作流程)。后续的博客文章将会展开介绍这部分工作。
主要参考链接
Amazon Web Services ( AWS )技术峰会 2018 中国站(上海)已经于 6 月 29 日圆满落幕,感谢您对 AWS 的关注和支持。精彩仍在继续,所建皆为不凡,AWS 技术峰会 2018 中国的第二站即将启航。AWS 技术峰会 2018 北京站,将在2018 年 8 月 9 日登陆北京国家会议中心!
想和我们一起#所建不凡#?快来注册#AWS技术峰会2018,前20名注册的小伙伴将可获得 $50亚马逊优惠券,抓住机会,赶快注册吧!https://awssummit.cn?tc=1s016B2uQi