From 234b3c261062d853488c3027bac2f0dd8d636d98 Mon Sep 17 00:00:00 2001 From: Kan-Ru Chen Date: Sat, 29 Jun 2019 20:53:41 +0900 Subject: [PATCH 1/4] Implement a HttpService backend that runs on the AWS Lambda Rust Runtime --- Cargo.toml | 2 +- http-service-lambda/Cargo.toml | 24 +++ http-service-lambda/examples/README.md | 23 +++ http-service-lambda/examples/hello_world.rs | 19 ++ http-service-lambda/src/lib.rs | 179 ++++++++++++++++++ .../tests/data/alb_request.json | 24 +++ .../tests/data/apigw_proxy_request.json | 55 ++++++ 7 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 http-service-lambda/Cargo.toml create mode 100644 http-service-lambda/examples/README.md create mode 100644 http-service-lambda/examples/hello_world.rs create mode 100644 http-service-lambda/src/lib.rs create mode 100644 http-service-lambda/tests/data/alb_request.json create mode 100644 http-service-lambda/tests/data/apigw_proxy_request.json diff --git a/Cargo.toml b/Cargo.toml index dfa8096..5f5f748 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ name = "http-service" version = "0.2.0" [workspace] -members = ["http-service-hyper", "http-service-mock"] +members = ["http-service-hyper", "http-service-lambda", "http-service-mock"] [dependencies] bytes = "0.4.12" diff --git a/http-service-lambda/Cargo.toml b/http-service-lambda/Cargo.toml new file mode 100644 index 0000000..886c32f --- /dev/null +++ b/http-service-lambda/Cargo.toml @@ -0,0 +1,24 @@ +[package] +authors = ["Kan-Ru Chen "] +description = "HttpService server that AWS Lambda Rust Runtime as backend" +documentation = "https://docs.rs/http-service-lambda" +edition = "2018" +license = "MIT OR Apache-2.0" +name = "http-service-lambda" +repository = "https://github.com/rustasync/http-service" +version = "0.1.0" + +[dependencies] +http-service = "0.2.0" +lambda_http = "0.1.1" +lambda_runtime = "0.2.1" +tokio = "0.1.21" + +[dependencies.futures-preview] +features = ["compat"] +version = "0.3.0-alpha.16" + +[dev-dependencies] +log = "0.4.6" +simple_logger = { version = "1.3.0", default-features = false } +tide = "0.2.0" \ No newline at end of file diff --git a/http-service-lambda/examples/README.md b/http-service-lambda/examples/README.md new file mode 100644 index 0000000..d3194e1 --- /dev/null +++ b/http-service-lambda/examples/README.md @@ -0,0 +1,23 @@ +## Quick Start + +See the document for +[aws-lambda-rust-runtime](https://github.com/awslabs/aws-lambda-rust-runtime) +for how to build and deploy a lambda function. + +Alternatively, build the example and deploy to lambda manually: + +1. Build with musl target + + ```sh + rustup target add x86_64-unknown-linux-musl + cargo +nightly build --release --example hello_world --target x86_64-unknown-linux-musl + ``` + +2. Package + + ```sh + cp ../../target/x86_64-unknown-linux-musl/release/examples/hello_world bootstrap + zip lambda.zip bootstrap + ``` + +3. Use [AWS CLI](https://aws.amazon.com/cli/) or AWS console to create new lambda function. diff --git a/http-service-lambda/examples/hello_world.rs b/http-service-lambda/examples/hello_world.rs new file mode 100644 index 0000000..0c82109 --- /dev/null +++ b/http-service-lambda/examples/hello_world.rs @@ -0,0 +1,19 @@ +#![feature(async_await)] +use simple_logger; +use tide::middleware::DefaultHeaders; + +fn main() { + simple_logger::init_with_level(log::Level::Info).unwrap(); + + let mut app = tide::App::new(()); + + app.middleware( + DefaultHeaders::new() + .header("X-Version", "1.0.0") + .header("X-Server", "Tide"), + ); + + app.at("/").get(async move |_| "Hello, world!"); + + http_service_lambda::run(app.into_http_service()); +} diff --git a/http-service-lambda/src/lib.rs b/http-service-lambda/src/lib.rs new file mode 100644 index 0000000..c99e9e5 --- /dev/null +++ b/http-service-lambda/src/lib.rs @@ -0,0 +1,179 @@ +//! `HttpService` server that uses AWS Lambda Rust Runtime as backend. +//! +//! This crate builds on the standard http interface provided by the +//! [lambda_http](https://docs.rs/lambda_http) crate and provides a http server +//! that runs on the lambda runtime. +//! +//! Compatible services like [tide](https://github.com/rustasync/tide) apps can +//! run on lambda and processing events from API Gateway or ALB without much +//! change. +//! +//! # Examples +//! +//! **Hello World** +//! +//! ```rust,ignore +//! #![feature(async_await)] +//! +//! fn main() { +//! let mut app = tide::App::new(); +//! app.at("/").get(async move |_| "Hello, world!"); +//! http_service_lambda::run(app.into_http_service()); +//! } +//! ``` + +#![forbid(future_incompatible, rust_2018_idioms)] +#![deny(missing_debug_implementations, nonstandard_style)] +#![warn(missing_docs, missing_doc_code_examples)] +#![cfg_attr(test, deny(warnings))] +#![feature(async_await)] + +use futures::{FutureExt, TryFutureExt}; +use http_service::{Body as HttpBody, HttpService, Request as HttpRequest}; +use lambda_http::{Body as LambdaBody, Handler, Request as LambdaHttpRequest}; +use lambda_runtime::{error::HandlerError, Context}; +use std::future::Future; +use std::sync::Arc; +use tokio::runtime::Runtime as TokioRuntime; + +type LambdaResponse = lambda_http::Response; + +trait ResultExt { + fn handler_error(self, description: &str) -> Result; +} + +impl ResultExt for Result { + fn handler_error(self, description: &str) -> Result { + self.map_err(|_| HandlerError::from(description)) + } +} + +trait CompatHttpBodayAsLambda { + fn into_lambda(self) -> LambdaBody; +} + +impl CompatHttpBodayAsLambda for Vec { + fn into_lambda(self) -> LambdaBody { + if self.is_empty() { + return LambdaBody::Empty; + } + match String::from_utf8(self) { + Ok(s) => LambdaBody::from(s), + Err(e) => LambdaBody::from(e.into_bytes()), + } + } +} + +struct Server { + service: Arc, + rt: TokioRuntime, +} + +impl Server +where + S: HttpService, +{ + fn new(s: S) -> Server { + Server { + service: Arc::new(s), + rt: tokio::runtime::Runtime::new().expect("failed to start new Runtime"), + } + } + + fn serve( + &self, + req: LambdaHttpRequest, + ) -> impl Future> { + let service = self.service.clone(); + async move { + let req: HttpRequest = req.map(|b| HttpBody::from(b.as_ref())); + let mut connection = service + .connect() + .into_future() + .await + .handler_error("connect")?; + let (parts, body) = service + .respond(&mut connection, req) + .into_future() + .await + .handler_error("respond")? + .into_parts(); + let resp = LambdaResponse::from_parts( + parts, + body.into_vec().await.handler_error("body")?.into_lambda(), + ); + Ok(resp) + } + } +} + +impl Handler for Server +where + S: HttpService, +{ + fn run( + &mut self, + req: LambdaHttpRequest, + _ctx: Context, + ) -> Result { + // Lambda processes one event at a time in a Function. Each invocation + // is not in async context so it's ok to block here. + self.rt.block_on(self.serve(req).boxed().compat()) + } +} + +/// Run the given `HttpService` on the default runtime, using `lambda_http` as +/// backend. +pub fn run(s: S) { + let server = Server::new(s); + // Let Lambda runtime start its own tokio runtime + lambda_http::start(server, None); +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::future; + + struct DummyService; + + impl HttpService for DummyService { + type Connection = (); + type ConnectionFuture = future::Ready>; + type Fut = future::BoxFuture<'static, Result>; + fn connect(&self) -> Self::ConnectionFuture { + future::ok(()) + } + fn respond(&self, _conn: &mut (), _req: http_service::Request) -> Self::Fut { + Box::pin(async move { Ok(http_service::Response::new(http_service::Body::empty())) }) + } + } + + #[test] + fn handle_apigw_request() { + // from the docs + // https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-api-gateway-request + let input = include_str!("../tests/data/apigw_proxy_request.json"); + let request = lambda_http::request::from_str(input).unwrap(); + let mut handler = Server::new(DummyService); + let result = handler.run(request, Context::default()); + assert!( + result.is_ok(), + format!("event was not handled as expected {:?}", result) + ); + } + + #[test] + fn handle_alb_request() { + // from the docs + // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers + let input = include_str!("../tests/data/alb_request.json"); + let request = lambda_http::request::from_str(input).unwrap(); + let mut handler = Server::new(DummyService); + let result = handler.run(request, Context::default()); + assert!( + result.is_ok(), + format!("event was not handled as expected {:?}", result) + ); + } +} diff --git a/http-service-lambda/tests/data/alb_request.json b/http-service-lambda/tests/data/alb_request.json new file mode 100644 index 0000000..8ee0432 --- /dev/null +++ b/http-service-lambda/tests/data/alb_request.json @@ -0,0 +1,24 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09" + } + }, + "httpMethod": "GET", + "path": "/", + "queryStringParameters": { "myKey": "val2"}, + "headers": { + "accept": "text/html,application/xhtml+xml", + "accept-language": "en-US,en;q=0.8", + "content-type": "text/plain", + "cookie": "cookies", + "host": "lambda-846800462-us-east-2.elb.amazonaws.com", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)", + "x-amzn-trace-id": "Root=1-5bdb40ca-556d8b0c50dc66f0511bf520", + "x-forwarded-for": "72.21.198.66", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + }, + "isBase64Encoded": false, + "body": "request_body" +} \ No newline at end of file diff --git a/http-service-lambda/tests/data/apigw_proxy_request.json b/http-service-lambda/tests/data/apigw_proxy_request.json new file mode 100644 index 0000000..f76d2b8 --- /dev/null +++ b/http-service-lambda/tests/data/apigw_proxy_request.json @@ -0,0 +1,55 @@ +{ + "path": "/test/hello", + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, lzma, sdch, br", + "Accept-Language": "en-US,en;q=0.8", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48", + "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==", + "X-Forwarded-For": "192.168.100.1, 192.168.1.1", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "pathParameters": { + "proxy": "hello" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "us4z18", + "stage": "test", + "requestId": "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9", + "identity": { + "cognitoIdentityPoolId": "", + "accountId": "", + "cognitoIdentityId": "", + "caller": "", + "apiKey": "", + "sourceIp": "192.168.100.1", + "cognitoAuthenticationType": "", + "cognitoAuthenticationProvider": "", + "userArn": "", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48", + "user": "" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "GET", + "apiId": "wt6mne2s9k" + }, + "resource": "/{proxy+}", + "httpMethod": "GET", + "queryStringParameters": { + "name": "me" + }, + "stageVariables": { + "stageVarName": "stageVarValue" + } +} \ No newline at end of file From 94c6200beda7a21710992aa9e3890056844bb29c Mon Sep 17 00:00:00 2001 From: Nicholas Young Date: Mon, 8 Jul 2019 20:08:30 -0600 Subject: [PATCH 2/4] Fix for kanru's PR, #37 --- Cargo.toml | 6 ++++++ http-service-lambda/examples/hello_world.rs | 2 +- http-service-lambda/src/lib.rs | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5f5f748..2dd499d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,9 @@ http-service-hyper = { path = "http-service-hyper", version = "0.2.0" } [dependencies.futures-preview] version = "0.3.0-alpha.16" + +[patch.crates-io] +http-service = { path = "." } +http-service-mock = { path = "./http-service-mock" } +http-service-hyper = { path = "./http-service-hyper" } +tide = { git = "https://github.com/rustasync/tide", branch = "master" } diff --git a/http-service-lambda/examples/hello_world.rs b/http-service-lambda/examples/hello_world.rs index 0c82109..d2adbfa 100644 --- a/http-service-lambda/examples/hello_world.rs +++ b/http-service-lambda/examples/hello_world.rs @@ -5,7 +5,7 @@ use tide::middleware::DefaultHeaders; fn main() { simple_logger::init_with_level(log::Level::Info).unwrap(); - let mut app = tide::App::new(()); + let mut app = tide::App::new(); app.middleware( DefaultHeaders::new() diff --git a/http-service-lambda/src/lib.rs b/http-service-lambda/src/lib.rs index c99e9e5..694e85a 100644 --- a/http-service-lambda/src/lib.rs +++ b/http-service-lambda/src/lib.rs @@ -140,11 +140,11 @@ mod tests { impl HttpService for DummyService { type Connection = (); type ConnectionFuture = future::Ready>; - type Fut = future::BoxFuture<'static, Result>; + type ResponseFuture = future::BoxFuture<'static, Result>; fn connect(&self) -> Self::ConnectionFuture { future::ok(()) } - fn respond(&self, _conn: &mut (), _req: http_service::Request) -> Self::Fut { + fn respond(&self, _conn: &mut (), _req: http_service::Request) -> Self::ResponseFuture { Box::pin(async move { Ok(http_service::Response::new(http_service::Body::empty())) }) } } From 5b10be4a2f0a9abfb2e232d0cfba5eb0ccdb1d76 Mon Sep 17 00:00:00 2001 From: Nicholas Young Date: Mon, 8 Jul 2019 20:19:36 -0600 Subject: [PATCH 3/4] Update crate metadata authors for http-service-lambda --- http-service-lambda/Cargo.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/http-service-lambda/Cargo.toml b/http-service-lambda/Cargo.toml index 886c32f..3459574 100644 --- a/http-service-lambda/Cargo.toml +++ b/http-service-lambda/Cargo.toml @@ -1,5 +1,9 @@ [package] -authors = ["Kan-Ru Chen "] +authors = [ + "Aaron Turon ", + "Yoshua Wuyts ", + "Kan-Ru Chen ", +] description = "HttpService server that AWS Lambda Rust Runtime as backend" documentation = "https://docs.rs/http-service-lambda" edition = "2018" @@ -21,4 +25,4 @@ version = "0.3.0-alpha.16" [dev-dependencies] log = "0.4.6" simple_logger = { version = "1.3.0", default-features = false } -tide = "0.2.0" \ No newline at end of file +tide = "0.2.0" From 94b2065d1f1dcbeaf29e1ae9615d648b095d8373 Mon Sep 17 00:00:00 2001 From: Kan-Ru Chen Date: Tue, 9 Jul 2019 17:49:30 +0900 Subject: [PATCH 4/4] Fix typo in the CompatHttpBodyAsLambda trait name --- http-service-lambda/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http-service-lambda/src/lib.rs b/http-service-lambda/src/lib.rs index 694e85a..b8d8e47 100644 --- a/http-service-lambda/src/lib.rs +++ b/http-service-lambda/src/lib.rs @@ -48,11 +48,11 @@ impl ResultExt for Result { } } -trait CompatHttpBodayAsLambda { +trait CompatHttpBodyAsLambda { fn into_lambda(self) -> LambdaBody; } -impl CompatHttpBodayAsLambda for Vec { +impl CompatHttpBodyAsLambda for Vec { fn into_lambda(self) -> LambdaBody { if self.is_empty() { return LambdaBody::Empty;