Starry Wisdom

Entropic Words from Neilathotep

Tuesday, October 24, 2017

Putting It Together Part 1: Deploying AWS Chalice apps with Terraform.

Chalice

Chalice is the “Python Serverless Microframework for AWS”. It allows quick and simple development of REST APIs, and comes with a a deploy tool that does all the work necessary to deploy your lambda, as well as create policy and integrate with the API Gateway. Let’s start out by creating an example app:

$ chalice new-project example-app

And then we can quickly create a simple app that reads in a couple of parameters and creates a JSON response. Note the use of the decorator to declare the route and method:

from chalice import Chalice
app = Chalice(app_name='example-app')
@app.route('/customer/{customer_id}/order/{order_id}', methods=['PUT'])
def register_order(customer_id, order_id):
    # imagine inserting this into Dynamo, etc...
    return {'customer':customer_id,
            "order_id": order_id }
Deploying this as simple as:

$ chalice deploy
Creating role: example-app-dev
Creating deployment package.
Creating lambda function: example-app-dev
Initiating first time deployment.
Deploying to API Gateway stage: api
https://id.execute-api.us-west-2.amazonaws.com/api/

At which point you can access it like this:

$ curl -X PUT https://id.execute-api.us-west-2.amazonaws.com/api/customer/customer01/order/123183123
{"customer": "customer01", "order_id": "123183123"}

See the tutorial, which is quite good, for more information on what you can do inside the apps (such as tying in other AWS services).

Enter Terraform

But what if you want to use Terraform to deploy your infrastructure? The first step is to create a deployment package:

$ chalice package .
Creating deployment package.
$ ls -l deployment.zip
-rw-r--r-- 1 nchazin staff 9022 Oct 18 22:24 deployment.zip

And now we’re ready to code up our terraform. We’ll begin by defining a few variables, which we can store values for in a terraform.tfvars file.

variable "environment" {
  description = "AWS account environment environment for the lambda and api gateway)"
}
variable "region" {
  description = "AWS region"
  default     = "us-west-2"
}
variable "account_id" {
   description = "AWS account id of the environment"
}

Next we’ll define the lambda itself, along with its associated role, and a policy which allows us to log and monitor with Cloudwatch:

 

provider "aws" {
  profile  = "${var.environment}"
  region   = "${var.region}"
}
resource "aws_iam_role" "lambda_example_app_role" {
  name = "lambda_example_app_role"
  assume_role_policy = <<EOF
{
  "Version""2012-10-17",
  "Statement": [
    {
      "Action""sts:AssumeRole",
      "Principal": {
        "Service""lambda.amazonaws.com"
      },
      "Effect""Allow",
      "Sid"""
    }
  ]
}
EOF
}
# Logging and metric policy
resource "aws_iam_role_policy" "lambda_example_app_role_policy" {
    name = "lambda_example_app_role_policy"
    role = "${aws_iam_role.lambda_example_app_role.id}"
    policy = <<EOF
{    
  "Version""2012-10-17",
  "Statement": [
    {
      "Effect""Allow",
      "Action": [
        "cloudwatch:PutMetricData",
      ],
      "Resource""*"
    },
    {
      "Effect""Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource""arn:aws:logs:*:*:*"
    }
  ]
}
EOF
}
resource "aws_lambda_function" "example_app" {
    function_name = "example_app"
    # This is the archive we created with chalice package
    filename = "deployment.zip"
    description = "An example app"
    role = "${aws_iam_role.lambda_example_app_role.arn}"
    handler = "app.app"
    timeout = 300
    runtime = "python3.6"
}

 

With our lambda set up, we can create out API Gateway:

 

# this declares the api gateway
resource "aws_api_gateway_rest_api" "example_api" {
    name = "CustomerOrderAPI"
    description = "API Gateway to register customer orders"
}
 
/*
 these four blocks declare the path for our api
-------------------------------------------------------------------------
*/
resource "aws_api_gateway_resource" "customer" {
    rest_api_id = "${aws_api_gateway_rest_api.example_api.id}"
    parent_id = "${aws_api_gateway_rest_api.example_api.root_resource_id}"
    path_part = "customer"
}
 
resource "aws_api_gateway_resource" "customer_id" {
    rest_api_id = "${aws_api_gateway_rest_api.example_api.id}"
    parent_id = "${aws_api_gateway_resource.customer.id}"
    path_part = "{customer_id}"
}
 
resource "aws_api_gateway_resource" "order" {
    rest_api_id = "${aws_api_gateway_rest_api.example_api.id}"
    parent_id = "${aws_api_gateway_resource.customer_id.id}"
    path_part = "order"
}
 
resource "aws_api_gateway_resource" "order_id" {
    rest_api_id = "${aws_api_gateway_rest_api.example_api.id}"
    parent_id = "${aws_api_gateway_resource.order.id}"
    path_part = "{order_id}"
}
 
/*
-------------------------------------------------------------------------
*/
 
 
# Declare a PUT method on that our full path
resource "aws_api_gateway_method" "example_method" {
    rest_api_id = "${aws_api_gateway_rest_api.example_api.id}"
    resource_id = "${aws_api_gateway_resource.order_id.id}"
    http_method = "PUT"
    authorization = "NONE"
}
 
 
# Tie the API method into our lambda backent
# Note: the integration_http_method for a lambda is POST, regardless of the gateway method
resource "aws_api_gateway_integration" "example_api_integration" {
    rest_api_id             = "${aws_api_gateway_rest_api.example_api.id}"
    resource_id             = "${aws_api_gateway_resource.order_id.id}"
    http_method             = "${aws_api_gateway_method.example_method.http_method}"
    integration_http_method = "POST"
    type                    = "AWS_PROXY"
    uri                     = "${aws_lambda_function.example_app.invoke_arn}"
}
 
 
 
# API gateway uses stages for release control - we'll define dev and prod
resource "aws_api_gateway_deployment" "example_deployment_dev" {
  depends_on = [
    "aws_api_gateway_method.example_method",
    "aws_api_gateway_integration.example_api_integration",
  ]
  rest_api_id = "${aws_api_gateway_rest_api.example_api.id}"
  stage_name = "dev"
}
 
resource "aws_api_gateway_deployment" "example_deployment_prod" {
  depends_on = [
    "aws_api_gateway_method.example_method",
    "aws_api_gateway_integration.example_api_integration",
  ]
  rest_api_id = "${aws_api_gateway_rest_api.example_api.id}"
  stage_name = "api"
}
 
# these output variables will show the base of the endpoint we'll query
output "dev_url" {
}
 
output "prod_url" {
}

The final piece is the permission that allows our API Gateway to access the lambda. Note that you can be more specific and limit acess to the specific API method and path if desired by adding them to the source_arn.

Now we can install our API:


$ terraform apply
...
Apply complete! Resources: 13 added, 0 changed, 0 destroyed.
The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.
State path:
Outputs:
ev_url = https://id.execute-api.us-west-2.amazonaws.com/dev
prod_url = https://id.execute-api.us-west-2.amazonaws.com/api

And query it at both the stage and prod endpoints:


$ curl -X PUT  https://id.execute-api.us-west-2.amazonaws.com/dev/customer/aaa/order/bbb
{"customer""aaa""order_id""bbb"}
$ curl -X PUT  https://id.execute-api.us-west-2.amazonaws.com/dev/customer/yoyodyne/order/100
{"customer""yoyodyne""order_id""100"}
posted by neil at 10:41 pm
under technology  

Powered by WordPress