用API网关和S3创建一个简单的API存根的教程

231 阅读4分钟

用API网关和S3创建一个简单的API存根

这篇文章展示了一种使用API Gateway中的映射模板来转换路径并创建一个简单的存根的好方法。

不久前,我的团队想为一个内部基于JSON HTTP的API创建一个存根。要存根的服务很简单。该服务暴露了一个REST API端点,用于列出特定类型的资源。该API支持分页和一些特定的请求/查询参数。

对该服务的GET请求是这样的:

/items?type=sometype&page=2

我们想在我们的AWS账户中为该服务创建一个简单的存根,我们可以用它来测试。我们想要执行的测试之一是测试服务是否被关闭。如果是这样,我们的应用程序将遵循一个特定的代码路径。当然,我们不能轻易地用远程API的真实服务来测试,所以我们尽量使事情简单。

用API网关、Lambda和S3创建API

由于我们领域内的大多数服务都是基于亚马逊API网关和AWS Lambda的,我们一开始就开始研究这个方向。由于我们的存根是只读的,而且我们不需要修改项目,我们选择从远程API创建一个初始的数据集导出为JSON文件,我们可以将其存储在S3中。为了存储这些文件,我们选择使用一个与我们的类型和页面参数相似的文件名模式。

{TYPE}_{PAGE_NUMER}(.json)

这导致了一个像这样的桶:

API存根的简化设计将如下:

我们的代码的一个示例版本看起来类似于这样。

Java

public static final String TYPE_QUERY_PARAM = "type";
public static final String JSON_FILE_EXTENSION = ".json";
public static final String PAGE_PREFIX = "_p";

private S3Client s3Client = S3Client.builder()
        .region(Region.EU_WEST_1)
        .httpClient(UrlConnectionHttpClient.builder().build())
        .credentialsProvider(EnvironmentVariableCredentialsProvider.create())
        .build();

@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
    Map<String, String> queryStringParameters = input.getQueryStringParameters();

    String page = queryStringParameters.getOrDefault("page", "1");
    String type;

    if(!queryStringParameters.containsKey(TYPE_QUERY_PARAM)){
        throw new MissingTypeException("Parameter Type cannot be null");
    } else {
        type = queryStringParameters.get(TYPE_QUERY_PARAM);
    }

    String key = type + PAGE_PREFIX + page + JSON_FILE_EXTENSION;

    ResponseInputStream<GetObjectResponse> s3ClientObject = s3Client
            .getObject(GetObjectRequest
                    .builder()
                    .bucket("apigatelambdas3integrationdemo")
                    .key(key)
                    .build());

    return new APIGatewayProxyResponseEvent()
            .withStatusCode(200)
            .withHeaders(
                    Map.of("Content-Type", s3ClientObject.response().contentType(),
                            "Content-Length", s3ClientObject.response().contentLength().toString()))
            .withBody(getFileContentAsString(context, s3ClientObject));
}

private String getFileContentAsString(Context context, ResponseInputStream<GetObjectResponse> s3ClientObject) {
    String fileAsString = "";
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(s3ClientObject))) {
        fileAsString = reader.lines().collect(Collectors.joining(System.lineSeparator()));
    } catch (IOException e) {
        context.getLogger().log("Oops! Something went wrong while converting the file from S3 to a String");
        e.printStackTrace();
    }
    return fileAsString;
}

正如你在上面的片段中看到的,我们基本上是根据一些请求参数来计算通往S3中的对象的路径。当解决了路径问题后,我们就从S3中获取文件,并将其转换为字符串回复给API Gateway,后者又将文件返回给消费者。我们的lambda函数只是充当了一个简单的代理,我们在想我们是否可以完全摆脱lambda函数。维护代码、依赖关系等是一个负担,所以如果我们不需要它,我们想摆脱它。

只用API Gateway和S3创建API

API Gateway对与其他AWS服务的直接集成有很大的支持,所以我们开始探索我们的选择。我们希望的解决方案是类似于下图的东西:

在浏览API Gateway的文档时,我们发现了一个很好的例子,说明如何使用API Gateway作为S3的代理。所提供的解决方案让API Gateway镜像S3中的文件夹和文件结构:很有用,但它并不包括我们的使用情况。

其他看起来有希望的选项之一是配置映射模板。我们以前曾使用过这种方法,将传入的请求体转换为远程后端的不同格式。如果你不熟悉API Gateway的映射模板,映射模板是一个用Velocity模板语言(VTL)表达的脚本,并使用JSONPath表达式应用于有效载荷。

在挖掘了API Gateway的文档后,我们还发现,映射模板可以用来改变查询字符串、头文件和路径

请求主体映射模板响应主体映射模板
$context.requestOverride.header.header_name$context.responseOverride.header.header_name
$context.requestOverride.path.path_name$context.responseOverride.status
$context.requestOverride.querystring.querystring_name

修改路径正是我们想要的,所以我们试了一下,效果非常好。让我们看一下我们的API网关GET请求的结果设置。 正如你在上面的部分所看到的,我们已经在我们的API的根部添加了一个GET方法。这个GET方法有一个方法请求,定义了两个查询参数;页面类型

对于集成请求,我们定义了我们要集成的远程服务,并指定了桶和一个动态fileName。

在集成请求的路径覆盖中,有两件重要的事情需要注意:

  1. 路径覆盖参数的开头,我们提供了S3桶的名称。
  2. 作为第二个参数,我们提供一个名为fileName的动态值

因此,路径覆盖将是{bucket-name}/{fileName}。

现在在我们的映射模板中,我们将填写fileName参数,这样API Gateway就知道要从S3中获取哪个文件。

让我们来看看这个映射模板。

正如你所看到的,我们已经为内容类型为application/json的请求设置了一个模板。现在,当一个带类型页面查询参数的GET请求到达时,它将在路径覆盖中集合生成fileName变量。

速度

#set($type = $input.params('type')
#set($page = $input.params('page'))
#if( $page == '') 
  #set($context.requestOverride.path.fileName = $type + '_p1.json')
#else
  #set($context.requestOverride.path.fileName = $type + '_p'+ $page + '.json')
#end

当该特定桶中的文件被找到时,它将返回相应的JSON。当文件没有找到时,它将抛出一个404响应体。我们还可以用一个映射模板来映射响应代码,以产生一些漂亮的错误信息。

最后的一些想法

在研究这个解决方案的时候,我还看到了Tom Vincent的一个帖子。汤姆写了一篇关于他称之为Lambdaless的好文章。我喜欢这个词,它与我们在这篇文章中试图实现的目标很有共鸣。

我认为这篇文章展示了在API网关中使用映射模板来转换路径并创建一个简单的存根的好方法。请记住,我们只是将其用于测试,我们不使用这种设置来运行生产工作负载。我还认为,如果API更复杂,我们就不会采取这种方法。尽管如此,对于我们的存根来说,它的设置和使用都很简单。