AWS Lambda lets you run code without provisioning or managing servers, which is so-called Serverless or Function as a Service (FaaS).
Apex is a Go command-line tool to manage and deploy your serverless functions on AWS Lambda. Apex is also integrated with Terraform to provide cloud infrastructure management, for instance, configuring your AWS Lambda functions with Amazon API Gateway.
ref:
https://aws.amazon.com/lambda/
https://aws.amazon.com/api-gateway/
https://github.com/apex/apex
You could browse projects created in this post on GitHub:
https://github.com/vinta/pangu.space
https://github.com/CodeTengu/LambdaBaku
Install
$ curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh
ref:
https://apex.run/#installation
Initialize
It is recommended to configure your AWS credentials with awscli
.
$ pip install awscli
$ aws configure
ref:
https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html
To use Apex to manage Lambda functions, you have to make sure your AWS credential has minimum IAM permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"iam:CreateRole",
"iam:CreatePolicy",
"iam:AttachRolePolicy",
"iam:PassRole",
"lambda:GetFunction",
"lambda:ListFunctions",
"lambda:CreateFunction",
"lambda:DeleteFunction",
"lambda:InvokeFunction",
"lambda:GetFunctionConfiguration",
"lambda:UpdateFunctionConfiguration",
"lambda:UpdateFunctionCode",
"lambda:CreateAlias",
"lambda:UpdateAlias",
"lambda:GetAlias",
"lambda:ListAliases",
"lambda:ListVersionsByFunction",
"logs:FilterLogEvents",
"cloudwatch:GetMetricStatistics"
],
"Effect": "Allow",
"Resource": "*"
}
]
}
$ apex init
ref:
https://apex.run/#getting-started
After running apex init
, Apex creates a Role and a Policy. You should be able to find them on AWS IAM Management Console. If you want to access other AWS resources, for instance, S3 buckets, DynamoDB tables, SNS, in your Lambda functions, you must create a new Policy which grants appropriate permissions and attachs itself to the Role that Apex created.
Here is a Policy example of operating certain DynamoDB tables:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt123456789",
"Effect": "Allow",
"Action": [
"dynamodb:*"
],
"Resource": [
"arn:aws:dynamodb:ap-northeast-1:123456789:table/CodeTengu_Preference",
"arn:aws:dynamodb:ap-northeast-1:123456789:table/CodeTengu_Preference/*",
"arn:aws:dynamodb:ap-northeast-1:123456789:table/CodeTengu_WeeklyIssue",
"arn:aws:dynamodb:ap-northeast-1:123456789:table/CodeTengu_WeeklyIssue/*",
"arn:aws:dynamodb:ap-northeast-1:123456789:table/CodeTengu_WeeklyPost",
"arn:aws:dynamodb:ap-northeast-1:123456789:table/CodeTengu_WeeklyPost/*"
]
}
]
}
Write Lambda Functions
ref:
https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html
https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html
Node.js
The simplest handler:
const aws = require('aws-sdk');
exports.handle = (event, context, callback) => {
doYourShit();
callback(null, 'DONE');
};
ref:
https://docs.aws.amazon.com/lambda/latest/dg/programming-model.html
Call another Lambda function in a Lambda function:
You must make sure your Lambda role has the permission of invoking other Lambda functions.
const util = require('util');
const aws = require('aws-sdk');
const params = {
FunctionName: 'LambdaBaku_syncIssue',
InvocationType: 'Event', // means asynchronous execution
Payload: JSON.stringify({ issue_number: curatedIssue.number }),
};
lambda.invoke(params, (err, data) => {
if (err) {
console.log('FAIL', params);
console.log(util.inspect(err));
} else {
console.log(data);
}
});
ref:
https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Lambda.html
https://stackoverflow.com/questions/31714788/can-an-aws-lambda-function-call-another
Go
Write a Lambda function triggered by Amazon API Gateway:
package main
import (
"encoding/json"
"errors"
"log"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/vinta/pangu"
)
var (
// ErrTextNotProvided is thrown when text is not provided in HTTP query string
ErrTextNotProvided = errors.New("No text was provided in HTTP query string")
)
// Handler is the AWS Lambda function handler
func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
log.Printf("request id: %s\n", request.RequestContext.RequestID)
text, ok := request.QueryStringParameters["t"]
if !ok {
errMap := map[string]string{
"message": ErrTextNotProvided.Error(),
}
errMapJSON, _ := json.MarshalIndent(errMap, "", " ")
return events.APIGatewayProxyResponse{
Body: string(errMapJSON),
StatusCode: 400,
}, nil
}
log.Printf("text: %s\n", text)
textPlainHeaders := map[string]string{
"content-type": "text/plain; charset=utf-8",
}
return events.APIGatewayProxyResponse{
Body: pangu.SpacingText(text),
Headers: textPlainHeaders,
StatusCode: 200,
}, nil
}
func main() {
lambda.Start(Handler)
}
ref:
https://aws.amazon.com/blogs/compute/announcing-go-support-for-aws-lambda/
https://docs.aws.amazon.com/lambda/latest/dg/go-programming-model-handler-types.html
https://docs.aws.amazon.com/lambda/latest/dg/go-programming-model-errors.html
Your "Integration Request" configurations in API Gateway should be like:
- Integration type:
Lambda Function
- Use Lambda Proxy integration:
Yes
- Lambda Region:
ap-northeast-1
- Lambda Function:
panguspace_spacing_text
- Invoke with caller credentials:
No
- Credentials cache:
Do not add caller credentials to cache key
- Use Default Timeout:
Yes
It's also worth noting that the API response is mainly defined by APIGatewayProxyResponse
in Lambda function code. Configurations in API Gateway, i.e., "Integration Response" and "Method Response" do not matter.
Usage
Deploy all functions:
$ apex deploy
ref:
https://apex.run/#deploying-functions
Invoke a function:
# invoke a function directly
$ apex invoke spacing_text --logs
{
"statusCode": 400,
"headers": null,
"body":"{\"message\": \"No text was provided in the HTTP query string\"}"
}
# invoke a function with an API Gateway event
$ cat fixtures/spacing_text_event.json
{
"queryStringParameters": {"t": "與PM戰鬥的人,應當小心自己不要成為PM"}
}
$ apex invoke spacing_text --logs < fixtures/spacing_text_event.json
{
"statusCode": 200,
"headers": {"content-type": "text/plain; charset=utf-8"},
"body": "與 PM 戰鬥的人,應當小心自己不要成為 PM"
}
ref:
https://apex.run/#invoking-functions
View logs which might delay several seconds:
$ apex logs -f
Pack a function:
$ apex build spacing_text > spacing_text.zip
Configure API Gateway
Create API Keys
To setup API keys, do the following:
- Configure your API methods to require an API key
- Deploy your API
- Create an API key for the API in a region
- Create an Usage Plan and assign an API key with a certain Stage
In step 1, your "Method Request" configurations in API Gateway should be like:
- Authorization:
NONE
- Request Validator:
NONE
- API Key Required:
true
Now you are able to call the API with a x-api-key
header:
$ curl -H "x-api-key: YOUR-API-KEY" https://xxx.execute-api.ap-northeast-1.amazonaws.com/v1/your-endpoint/
ref:
https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-usage-plans-with-rest-api.html
https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-use-postman-to-call-api.html
Actually, you could release your APIs without API keys if you like.
Setup a Custom Domain
To setup a custom domain which managed by Cloudflare, see the following link:
https://stackoverflow.com/a/46061708/885524
It is worth noting that even the Stack Overflow answer said using Full (Strict)
SSL mode but actually Full
also works.
Moreover, it might take a long time to generate "Target Domain Name" (xxx.cloudfront.net
).
Don't forget to add "Base Path Mappings" in API Gateway Custom Domain Names:
api.pangu.space
- Target Domain Name:
xxx.cloudfront.net
- ACM Certificate:
*.pangu.space
- Base Path Mappings:
- Path:
/v1
- Destination:
Pangu:v1
- Path:
- Target Domain Name:
Manage Infrastructures with Terraform
Terraform is a tool to manage your cloud infrastructures as code.
$ brew install terraform
$ tree .
.
├── functions
│ ├── introduce
│ │ └── main.go
│ └── spacing_text
│ └── main.go
└── infrastructure
├── main.tf
└── variables.tf
Define variables and data sources:
# infrastructure/variables.tf
data "aws_caller_identity" "current" {}
variable "aws_region" {}
variable "apex_environment" {}
variable "apex_function_role" {}
variable "apex_function_arns" {
type = "map"
}
variable "apex_function_names" {
type = "map"
}
variable "apex_function_introduce" {}
variable "apex_function_spacing_text" {}
ref:
https://www.terraform.io/docs/providers/aws/d/caller_identity.html
Define AWS resources:
# infrastructure/main.tf
resource "aws_api_gateway_rest_api" "pangu" {
name = "Pangu"
}
resource "aws_api_gateway_method" "pangu_root" {
rest_api_id = "${aws_api_gateway_rest_api.pangu.id}"
resource_id = "${aws_api_gateway_rest_api.pangu.root_resource_id}"
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "pangu_root_get" {
rest_api_id = "${aws_api_gateway_rest_api.pangu.id}"
resource_id = "${aws_api_gateway_rest_api.pangu.root_resource_id}"
http_method = "${aws_api_gateway_method.pangu_root.http_method}"
integration_http_method = "POST"
type = "AWS_PROXY"
uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/${var.apex_function_introduce}/invocations"
}
resource "aws_api_gateway_method_response" "pangu_root_get_200" {
rest_api_id = "${aws_api_gateway_rest_api.pangu.id}"
resource_id = "${aws_api_gateway_rest_api.pangu.root_resource_id}"
http_method = "${aws_api_gateway_method.pangu_root.http_method}"
status_code = "200"
response_models = {
"application/json" = "Empty"
}
response_parameters = {
"method.response.header.Access-Control-Allow-Origin" = true
}
}
resource "aws_api_gateway_resource" "pangu_spacing_text" {
rest_api_id = "${aws_api_gateway_rest_api.pangu.id}"
parent_id = "${aws_api_gateway_rest_api.pangu.root_resource_id}"
path_part = "spacing-text"
}
resource "aws_api_gateway_method" "pangu_spacing_text_get" {
rest_api_id = "${aws_api_gateway_rest_api.pangu.id}"
resource_id = "${aws_api_gateway_resource.pangu_spacing_text.id}"
http_method = "GET"
authorization = "NONE"
api_key_required = true
}
resource "aws_api_gateway_integration" "pangu_spacing_text_get" {
rest_api_id = "${aws_api_gateway_rest_api.pangu.id}"
resource_id = "${aws_api_gateway_resource.pangu_spacing_text.id}"
http_method = "${aws_api_gateway_method.pangu_spacing_text_get.http_method}"
integration_http_method = "POST"
type = "AWS_PROXY"
uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/${var.apex_function_spacing_text}/invocations"
}
resource "aws_api_gateway_method_response" "pangu_spacing_text_get_200" {
rest_api_id = "${aws_api_gateway_rest_api.pangu.id}"
resource_id = "${aws_api_gateway_resource.pangu_spacing_text.id}"
http_method = "${aws_api_gateway_method.pangu_spacing_text_get.http_method}"
status_code = "200"
response_models = {
"application/json" = "Empty"
}
response_parameters = {
"method.response.header.Access-Control-Allow-Origin" = true
}
}
resource "aws_api_gateway_deployment" "pangu" {
depends_on = [
"aws_api_gateway_method.pangu_root",
"aws_api_gateway_integration.pangu_root_get",
"aws_api_gateway_method_response.pangu_root_get_200",
"aws_api_gateway_resource.pangu_spacing_text",
"aws_api_gateway_method.pangu_spacing_text_get",
"aws_api_gateway_integration.pangu_spacing_text_get",
"aws_api_gateway_method_response.pangu_spacing_text_get_200",
]
rest_api_id = "${aws_api_gateway_rest_api.pangu.id}"
stage_name = "v1"
}
resource "aws_lambda_permission" "pangu_root_get" {
statement_id = "AllowInvokeFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = "${var.apex_function_introduce}"
principal = "apigateway.amazonaws.com"
source_arn = "arn:aws:execute-api:${var.aws_region}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.pangu.id}/*/${aws_api_gateway_integration.pangu_root_get.http_method}/"
}
resource "aws_lambda_permission" "pangu_spacing_text" {
statement_id = "AllowInvokeFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = "${var.apex_function_spacing_text}"
principal = "apigateway.amazonaws.com"
source_arn = "arn:aws:execute-api:${var.aws_region}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.pangu.id}/*/${aws_api_gateway_integration.pangu_spacing_text_get.http_method}${aws_api_gateway_resource.pangu_spacing_text.path}"
}
ref:
https://www.terraform.io/docs/providers/aws/guides/serverless-with-aws-lambda-and-api-gateway.html
# donwload provider plugins
$ apex infra init
# view the generated execution plan
$ apex infra plan
# deploy your infrastructures
$ apex infra apply
$ apex infra apply -auto-approve