This is an automated email from the ASF dual-hosted git repository.
spacewander pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new e90e3b7 feat(plugins): aws lambda serverless (#5594)
e90e3b7 is described below
commit e90e3b7aa1d1789999b3ac740ebf6fcf887ff951
Author: Bisakh <[email protected]>
AuthorDate: Wed Dec 1 08:09:45 2021 +0530
feat(plugins): aws lambda serverless (#5594)
---
apisix/plugins/aws-lambda.lua | 183 +++++++++++++++++++++
conf/config-default.yaml | 1 +
docs/en/latest/config.json | 3 +-
docs/en/latest/plugins/aws-lambda.md | 156 ++++++++++++++++++
t/admin/plugins.t | 1 +
t/plugin/aws-lambda.t | 299 +++++++++++++++++++++++++++++++++++
6 files changed, 642 insertions(+), 1 deletion(-)
diff --git a/apisix/plugins/aws-lambda.lua b/apisix/plugins/aws-lambda.lua
new file mode 100644
index 0000000..fe4d7f3
--- /dev/null
+++ b/apisix/plugins/aws-lambda.lua
@@ -0,0 +1,183 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+local ngx = ngx
+local hmac = require("resty.hmac")
+local hex_encode = require("resty.string").to_hex
+local resty_sha256 = require("resty.sha256")
+local str_strip = require("pl.stringx").strip
+local norm_path = require("pl.path").normpath
+local pairs = pairs
+local tab_concat = table.concat
+local tab_sort = table.sort
+local os = os
+
+
+local plugin_name = "aws-lambda"
+local plugin_version = 0.1
+local priority = -1899
+
+local ALGO = "AWS4-HMAC-SHA256"
+
+local function hmac256(key, msg)
+ return hmac:new(key, hmac.ALGOS.SHA256):final(msg)
+end
+
+local function sha256(msg)
+ local hash = resty_sha256:new()
+ hash:update(msg)
+ local digest = hash:final()
+ return hex_encode(digest)
+end
+
+local function get_signature_key(key, datestamp, region, service)
+ local kDate = hmac256("AWS4" .. key, datestamp)
+ local kRegion = hmac256(kDate, region)
+ local kService = hmac256(kRegion, service)
+ local kSigning = hmac256(kService, "aws4_request")
+ return kSigning
+end
+
+local aws_authz_schema = {
+ type = "object",
+ properties = {
+ -- API Key based authorization
+ apikey = {type = "string"},
+ -- IAM role based authorization, works via aws v4 request signing
+ -- more at
https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
+ iam = {
+ type = "object",
+ properties = {
+ accesskey = {
+ type = "string",
+ description = "access key id from from aws iam console"
+ },
+ secretkey = {
+ type = "string",
+ description = "secret access key from from aws iam console"
+ },
+ aws_region = {
+ type = "string",
+ default = "us-east-1",
+ description = "the aws region that is receiving the
request"
+ },
+ service = {
+ type = "string",
+ default = "execute-api",
+ description = "the service that is receiving the request"
+ }
+ },
+ required = {"accesskey", "secretkey"}
+ }
+ }
+}
+
+local function request_processor(conf, ctx, params)
+ local headers = params.headers
+ -- set authorization headers if not already set by the client
+ -- we are following not to overwrite the authz keys
+ if not headers["x-api-key"] then
+ if conf.authorization and conf.authorization.apikey then
+ headers["x-api-key"] = conf.authorization.apikey
+ return
+ end
+ end
+
+ -- performing aws v4 request signing for IAM authorization
+ -- visit
https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
+ -- to look at the pseudocode in python.
+ if headers["authorization"] or not conf.authorization or not
conf.authorization.iam then
+ return
+ end
+
+ -- create a date for headers and the credential string
+ local t = ngx.time()
+ local amzdate = os.date("!%Y%m%dT%H%M%SZ", t)
+ local datestamp = os.date("!%Y%m%d", t) -- Date w/o time, used in
credential scope
+ headers["X-Amz-Date"] = amzdate
+
+ -- computing canonical uri
+ local canonical_uri = norm_path(params.path)
+ if canonical_uri ~= "/" then
+ if canonical_uri:sub(-1, -1) == "/" then
+ canonical_uri = canonical_uri:sub(1, -2)
+ end
+ if canonical_uri:sub(1, 1) ~= "/" then
+ canonical_uri = "/" .. canonical_uri
+ end
+ end
+
+ -- computing canonical query string
+ local canonical_qs = {}
+ for k, v in pairs(params.query) do
+ canonical_qs[#canonical_qs+1] = ngx.unescape_uri(k) .. "=" ..
ngx.unescape_uri(v)
+ end
+
+ tab_sort(canonical_qs)
+ canonical_qs = tab_concat(canonical_qs, "&")
+
+ -- computing canonical and signed headers
+
+ local canonical_headers, signed_headers = {}, {}
+ for k, v in pairs(headers) do
+ k = k:lower()
+ if k ~= "connection" then
+ signed_headers[#signed_headers+1] = k
+ -- strip starting and trailing spaces including strip multiple
spaces into single space
+ canonical_headers[k] = str_strip(v)
+ end
+ end
+ tab_sort(signed_headers)
+
+ for i = 1, #signed_headers do
+ local k = signed_headers[i]
+ canonical_headers[i] = k .. ":" .. canonical_headers[k] .. "\n"
+ end
+ canonical_headers = tab_concat(canonical_headers, nil, 1, #signed_headers)
+ signed_headers = tab_concat(signed_headers, ";")
+
+ -- combining elements to form the canonical request (step-1)
+ local canonical_request = params.method:upper() .. "\n"
+ .. canonical_uri .. "\n"
+ .. (canonical_qs or "") .. "\n"
+ .. canonical_headers .. "\n"
+ .. signed_headers .. "\n"
+ .. sha256(params.body or "")
+
+ -- creating the string to sign for aws signature v4 (step-2)
+ local iam = conf.authorization.iam
+ local credential_scope = datestamp .. "/" .. iam.aws_region .. "/"
+ .. iam.service .. "/aws4_request"
+ local string_to_sign = ALGO .. "\n"
+ .. amzdate .. "\n"
+ .. credential_scope .. "\n"
+ .. sha256(canonical_request)
+
+ -- calculate the signature (step-3)
+ local signature_key = get_signature_key(iam.secretkey, datestamp,
iam.aws_region, iam.service)
+ local signature = hex_encode(hmac256(signature_key, string_to_sign))
+
+ -- add info to the headers (step-4)
+ headers["authorization"] = ALGO .. " Credential=" .. iam.accesskey
+ .. "/" .. credential_scope
+ .. ", SignedHeaders=" .. signed_headers
+ .. ", Signature=" .. signature
+end
+
+
+local serverless_obj = require("apisix.plugins.serverless.generic-upstream")
+
+return serverless_obj(plugin_name, plugin_version, priority,
request_processor, aws_authz_schema)
diff --git a/conf/config-default.yaml b/conf/config-default.yaml
index dc42e04..7cf130b 100644
--- a/conf/config-default.yaml
+++ b/conf/config-default.yaml
@@ -355,6 +355,7 @@ plugins: # plugin list (sorted by
priority)
# <- recommend to use priority (0, 100) for your custom plugins
- example-plugin # priority: 0
#- skywalking # priority: -1100
+ - aws-lambda # priority: -1899
- azure-functions # priority: -1900
- openwhisk # priority: -1901
- serverless-post-function # priority: -2000
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index d40a0de..8a775e0 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -130,7 +130,8 @@
"items": [
"plugins/serverless",
"plugins/azure-functions",
- "plugins/openwhisk"
+ "plugins/openwhisk",
+ "plugins/aws-lambda"
]
},
{
diff --git a/docs/en/latest/plugins/aws-lambda.md
b/docs/en/latest/plugins/aws-lambda.md
new file mode 100644
index 0000000..5afb805
--- /dev/null
+++ b/docs/en/latest/plugins/aws-lambda.md
@@ -0,0 +1,156 @@
+---
+title: aws-lambda
+---
+
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+## Summary
+
+- [Summary](#summary)
+- [Name](#name)
+- [Attributes](#attributes)
+ - [IAM Authorization Schema](#iam-authorization-schema)
+- [How To Enable](#how-to-enable)
+- [Disable Plugin](#disable-plugin)
+
+## Name
+
+`aws-lambda` is a serverless plugin built into Apache APISIX for seamless
integration with [AWS Lambda](https://aws.amazon.com/lambda/), a widely used
serverless solution, as a dynamic upstream to proxy all requests for a
particular URI to the AWS cloud - one of the highly used public cloud platforms
for production environment. If enabled, this plugin terminates the ongoing
request to that particular URI and initiates a new request to the AWS lambda
gateway uri (the new upstream) on beha [...]
+At present, the plugin supports authorization via AWS api key and AWS IAM
Secrets.
+
+## Attributes
+
+| Name | Type | Requirement | Default | Valid |
Description
|
+| ----------- | ------ | ----------- | ------- | ----- |
------------------------------------------------------------
|
+| function_uri | string | required | | | The AWS api
gateway endpoint which triggers the lambda serverless function code. |
+| authorization | object | optional | | | Authorization
credentials to access the cloud function.
|
+| authorization.apikey | string | optional | | | Field
inside _authorization_. The generate API Key to authorize requests to that
endpoint of the AWS gateway. | |
+| authorization.iam | object | optional | | | Field inside
_authorization_. AWS IAM role based authorization, performed via AWS v4 request
signing. See schema details below ([here](#iam-authorization-schema)). |
|
+| timeout | integer | optional | 3000 | [100,...] | Proxy
request timeout in milliseconds. |
+| ssl_verify | boolean | optional | true | true/false | If
enabled performs SSL verification of the server. |
+| keepalive | boolean | optional | true | true/false | To
reuse the same proxy connection in near future. Set to false to disable
keepalives and immediately close the connection. |
+| keepalive_pool | integer | optional | 5 | [1,...] | The
maximum number of connections in the pool. |
+| keepalive_timeout | integer | optional | 60000 | [1000,...]
| The maximal idle timeout (ms). |
+
+### IAM Authorization Schema
+
+| Name | Type | Requirement | Default | Valid |
Description
|
+| ----------- | ------ | ----------- | ------- | ----- |
------------------------------------------------------------
|
+| accesskey | string | required | | |
Generated access key ID from AWS IAM console.
|
+| secret_key | string | required | | |
Generated access key secret from AWS IAM console.
|
+| aws_region | string | optional | "us-east-1" | |
The AWS region where the request is being sent.
|
+| service | string | optional | "execute-api" | |
The service that is receiving the request (In case of Http Trigger it is
"execute-api"). |
+
+## How To Enable
+
+The following is an example of how to enable the aws-lambda faas plugin for a
specific route URI. Calling the APISIX route uri will make an invocation to the
lambda function uri (the new upstream). We are assuming your cloud function is
already up and running.
+
+```shell
+# enable aws lambda for a route via api key authorization
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY:
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+ "plugins": {
+ "aws-lambda": {
+ "function_uri":
"https://x9w6z07gb9.execute-api.us-east-1.amazonaws.com/default/test-apisix",
+ "authorization": {
+ "apikey": "<Generated API Key from aws console>",
+ },
+ "ssl_verify":false
+ }
+ },
+ "uri": "/aws"
+}'
+```
+
+Now any requests (HTTP/1.1, HTTPS, HTTP2) to URI `/aws` will trigger an HTTP
invocation to the aforesaid function URI and response body along with the
response headers and response code will be proxied back to the client. For
example (here AWS lambda function just take the `name` query param and returns
`Hello $name`) :
+
+```shell
+$ curl -i -XGET localhost:9080/aws\?name=APISIX
+HTTP/1.1 200 OK
+Content-Type: application/json
+Connection: keep-alive
+Date: Sat, 27 Nov 2021 13:08:27 GMT
+x-amz-apigw-id: JdwXuEVxIAMFtKw=
+x-amzn-RequestId: 471289ab-d3b7-4819-9e1a-cb59cac611e0
+Content-Length: 16
+X-Amzn-Trace-Id: Root=1-61a22dca-600c552d1c05fec747fd6db0;Sampled=0
+Server: APISIX/2.10.2
+
+"Hello, APISIX!"
+```
+
+For requests where the mode of communication between the client and the Apache
APISIX gateway is HTTP/2, the example looks like ( make sure you are running
APISIX agent with `enable_http2: true` for a port in `config-default.yaml`. You
can do it by uncommenting the port 9081 from `apisix.node_listen` field ) :
+
+```shell
+$ curl -i -XGET --http2 --http2-prior-knowledge localhost:9081/aws\?name=APISIX
+HTTP/2 200
+content-type: application/json
+content-length: 16
+x-amz-apigw-id: JdwulHHrIAMFoFg=
+date: Sat, 27 Nov 2021 13:10:53 GMT
+x-amzn-trace-id: Root=1-61a22e5d-342eb64077dc9877644860dd;Sampled=0
+x-amzn-requestid: a2c2b799-ecc6-44ec-b586-38c0e3b11fe4
+server: APISIX/2.10.2
+
+"Hello, APISIX!"
+```
+
+Similarly, the lambda can be triggered via AWS API Gateway by using AWS `IAM`
permissions to authorize access to your API via APISIX aws-lambda plugin.
Plugin includes authentication signatures in their HTTP calls via AWS v4
request signing. Here is an example:
+
+```shell
+# enable aws lambda for a route via iam authorization
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY:
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+ "plugins": {
+ "aws-lambda": {
+ "function_uri":
"https://ajycz5e0v9.execute-api.us-east-1.amazonaws.com/default/test-apisix",
+ "authorization": {
+ "iam": {
+ "accesskey": "<access key>",
+ "secretkey": "<access key secret>"
+ }
+ },
+ "ssl_verify": false
+ }
+ },
+ "uri": "/aws"
+}'
+```
+
+**Note**: This approach assumes you already have an iam user with the
programmatic access enabled and required permissions
(`AmazonAPIGatewayInvokeFullAccess`) to access the endpoint.
+
+## Disable Plugin
+
+Remove the corresponding JSON configuration in the plugin configuration to
disable the `aws-lambda` plugin and add the suitable upstream configuration.
+APISIX plugins are hot-reloaded, therefore no need to restart APISIX.
+
+```shell
+$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY:
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+ "uri": "/aws",
+ "plugins": {},
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1980": 1
+ }
+ }
+}'
+```
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index 4305b66..dbe5859 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -109,6 +109,7 @@ kafka-logger
syslog
udp-logger
example-plugin
+aws-lambda
azure-functions
openwhisk
serverless-post-function
diff --git a/t/plugin/aws-lambda.t b/t/plugin/aws-lambda.t
new file mode 100644
index 0000000..78a1149
--- /dev/null
+++ b/t/plugin/aws-lambda.t
@@ -0,0 +1,299 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ my $inside_lua_block = $block->inside_lua_block // "";
+ chomp($inside_lua_block);
+ my $http_config = $block->http_config // <<_EOC_;
+
+ server {
+ listen 8765;
+
+ location /httptrigger {
+ content_by_lua_block {
+ ngx.req.read_body()
+ local msg = "aws lambda invoked"
+ ngx.header['Content-Length'] = #msg + 1
+ ngx.header['Connection'] = "Keep-Alive"
+ ngx.say(msg)
+ }
+ }
+
+ location /generic {
+ content_by_lua_block {
+ $inside_lua_block
+ }
+ }
+ }
+_EOC_
+
+ $block->set_value("http_config", $http_config);
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+ if (!$block->no_error_log && !$block->error_log) {
+ $block->set_value("no_error_log", "[error]\n[alert]");
+ }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: checking iam schema
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.aws-lambda")
+ local ok, err = plugin.check_schema({
+ function_uri = "https://api.amazonaws.com",
+ authorization = {
+ iam = {
+ accesskey = "key1",
+ secretkey = "key2"
+ }
+ }
+ })
+ if not ok then
+ ngx.say(err)
+ else
+ ngx.say("done")
+ end
+ }
+ }
+--- response_body
+done
+
+
+
+=== TEST 2: missing fields in iam schema
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.aws-lambda")
+ local ok, err = plugin.check_schema({
+ function_uri = "https://api.amazonaws.com",
+ authorization = {
+ iam = {
+ secretkey = "key2"
+ }
+ }
+ })
+ if not ok then
+ ngx.say(err)
+ else
+ ngx.say("done")
+ end
+ }
+ }
+--- response_body
+property "authorization" validation failed: property "iam" validation failed:
property "accesskey" is required
+
+
+
+=== TEST 3: create route with aws plugin enabled
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "aws-lambda": {
+ "function_uri":
"http://localhost:8765/httptrigger",
+ "authorization": {
+ "apikey" : "testkey"
+ }
+ }
+ },
+ "uri": "/aws"
+ }]],
+ [[{
+ "node": {
+ "value": {
+ "plugins": {
+ "aws-lambda": {
+ "keepalive": true,
+ "timeout": 3000,
+ "ssl_verify": true,
+ "keepalive_timeout": 60000,
+ "keepalive_pool": 5,
+ "function_uri":
"http://localhost:8765/httptrigger",
+ "authorization": {
+ "apikey": "testkey"
+ }
+ }
+ },
+ "uri": "/aws"
+ },
+ "key": "/apisix/routes/1"
+ },
+ "action": "set"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say("fail")
+ return
+ end
+
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 4: test plugin endpoint
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local core = require("apisix.core")
+
+ local code, _, body, headers = t("/aws", "GET")
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ -- headers proxied 2 times -- one by plugin, another by this test
case
+ core.response.set_header(headers)
+ ngx.print(body)
+ }
+ }
+--- response_body
+aws lambda invoked
+--- response_headers
+Content-Length: 19
+
+
+
+=== TEST 5: check authz header - apikey
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ -- passing an apikey
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "aws-lambda": {
+ "function_uri":
"http://localhost:8765/generic",
+ "authorization": {
+ "apikey": "test_key"
+ }
+ }
+ },
+ "uri": "/aws"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ ngx.say("fail")
+ return
+ end
+
+ ngx.say(body)
+
+ local code, _, body = t("/aws", "GET")
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ ngx.print(body)
+ }
+ }
+--- inside_lua_block
+local headers = ngx.req.get_headers() or {}
+ngx.say("Authz-Header - " .. headers["x-api-key"] or "")
+
+--- response_body
+passed
+Authz-Header - test_key
+
+
+
+=== TEST 6: check authz header - IAM v4 signing
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+
+ -- passing the iam access and secret keys
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "aws-lambda": {
+ "function_uri":
"http://localhost:8765/generic",
+ "authorization": {
+ "iam": {
+ "accesskey": "KEY1",
+ "secretkey": "KeySecret"
+ }
+ }
+ }
+ },
+ "uri": "/aws"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ ngx.say("fail")
+ return
+ end
+
+ ngx.say(body)
+
+ local code, _, body, headers = t("/aws", "GET")
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ ngx.print(body)
+ }
+ }
+--- inside_lua_block
+local headers = ngx.req.get_headers() or {}
+ngx.say("Authz-Header - " .. headers["Authorization"] or "")
+ngx.say("AMZ-Date - " .. headers["X-Amz-Date"] or "")
+ngx.print("invoked")
+
+--- response_body eval
+qr/passed
+Authz-Header - AWS4-HMAC-SHA256 [ -~]*
+AMZ-Date - [\d]+T[\d]+Z
+invoked/