The Application

How about we run an application now! We have everything in place to make it happen. Our application was already deployed by Terraform and is running in a Lambda behind an application load balancer. It is also deployed into the same private subnet as our database and database access works exactly the same as for our migration Lambda - Terraform creates a database dynamic producer and puts the path to that producer in the Lambda’s environment. The application will retrieve the path to the producer, login to Akeyless using IAM auth, fetch database credentials from the producer, and connect. There’s not really anything new here, so let’s talk about:

The Code

clearskies will build us API endpoints for our product service - standard CRUD endpoints for products, categories, and manufacturers. For quick background, clearskies can be used like any other “standard” framework with models and controllers. However, it also leans heavily into declarative programming principles and allows you to approach data processing from the perspective of a state machine (which often works well for business logic). In this case, we’re just going to build simple APIs that shuffle data in and out of the database, which clearskies can do with minimal amounts of code. Let’s look at some of the code from our example application. We define a model to declare our schema in python. We have a product, category, and manufacturer model. You can find all of them in the repo but here is the category model for reference:

from collections import OrderedDict
from clearskies import Model
from clearskies import column_types
from clearskies.input_requirements import required, maximum_length
from . import product_category, product


class Category(Model):
    def __init__(self, cursor_backend, columns):
        super().__init__(cursor_backend, columns)

    def columns_configuration(self):
        return OrderedDict([
            column_types.belongs_to('parent_id', parent_models_class=Category),
            column_types.string('name', input_requirements=[required(), maximum_length(255)]),
            column_types.string('description', input_requirements=[required(), maximum_length(1024)]),
            column_types.updated('updated_at'),
            column_types.created('created_at'),
            column_types.many_to_many('products', pivot_models_class=product_category.ProductCategory, related_models_class=product.Product),
            column_types.has_many('sub_categories', child_models_class=Category, foreign_column_name='parent_id'),
        ])

Our category has a name and a description. It automatically tracks when it was last created and updated. It contains many products (and we have a pivot table to map products and cateogries) and is also related to itself, with each category having up to one parent category. Conveniently, this tells clearskies everything it needs to manage an API for us. All we have to tell it is what we actually want to expose via the API:

import clearskies
from . import models

categories_api = clearskies.Application(
    clearskies.handlers.RestfulAPI,
    {
        'model_class': models.Category,
        'readable_columns': ['parent_id', 'name', 'description', 'created_at', 'updated_at'],
        'writeable_columns': ['parent_id', 'name', 'description'],
        'searchable_columns': ['parent_id', 'name', 'description'],
        'default_sort_column': 'name',
    },
    binding_modules=[models],
)

and now we have a RESTful API with full CRUD support for our category model, just waiting to be launched. To actually launch it though we need to tell clearskies what context it is running in: perhaps it is running in a Lambda, or behind a WSGI server, or even from the command line. All of these require different ways of receiving requests and returning responses, and so clearskies uses the context to normalize data input for the application.

In addition, we can adjust central framework behavior based on the context. For instance, when our application runs in a Lambda context we can tell it to use IAM auth to connect to Akeyless and then use a database provider to connect to the database. If running an application locally, we can switch to a WSGI context, tell clearskies to connect to Akeyless using SAML, and instruct it to connect to the database using a dynamic producer but to also funnel traffic through a bastion host. We’ll see a full example of this later, but for now we’ll focus on how to configure clearskies to run in the Lambda via a context:

import app
import clearskies
import clearskies_aws

api_in_lambda = clearskies_aws.contexts.lambda_elb(
    app.api,
    additional_configs=[
        clearskies.secrets.additional_configs.mysql_connection_dynamic_producer(),
        clearskies.secrets.akeyless_aws_iam_auth(),
    ],
)
def lambda_handler(event, context):
    return api_in_lambda(event, context)

and… that’s it! We declare our lambda_elb context (which tells clearskies that it is running in a Lambda behind an application load balancer), pass it an application, and give it some simple configuration to tell it how to connect to Akeyless and the database. The specific details of the connections (access id, producer paths) are all fetched from the environment.

Naturally, lambda_handler is the function that we will have the Lambda call. We create our context object outside of the lambda_handler because this will allow AWS to cache it for us which improves execution times, allows the application to reuse the database connection, etc… Finally, when the Lambda is invoked it calls our lambda_handler function, which passes the event and context variables from AWS off to the context. The context then normalizes input for the application, takes the response from the application, and returns it in the way that AWS requires. Thus, we get a fully functional application.

Let’s try our application with some simple curl commands! We can check the list of categories but of course it is empty:

$ curl 'https://shopping.example.com/categories' | jq
{
  "status": "success",
  "error": "",
  "data": [],
  "pagination": {
    "number_results": 0,
    "limit": 100,
    "next_page": {}
  },
  "input_errors": {}
}

The API endpoint that clearskies automatically creates comes with strict error checking, so if we try to create a category but send invalid data we get lots of details back:

$ curl -d '{"hey": "sup"}' 'https://shopping.example.com/categories' | jq
{
  "status": "input_errors",
  "error": "",
  "data": [],
  "pagination": {},
  "input_errors": {
    "hey": "Input column 'hey' is not an allowed column",
    "name": "'name' is required.",
    "description": "'description' is required."
  }
}

But we can go ahead and create a category and sub category if we fix our input errors:

$ curl -d '{"name": "Widgets", "description": "Only the best!"}' 'https://shopping.example.com/categories' | jq
{
  "status": "success",
  "error": "",
  "data": {
    "id": "ad72b526-eed8-40ae-8857-d8e9c8c90890",
    "parent_id": null,
    "name": "Widgets",
    "description": "Only the best!",
    "created_at": "2022-05-19T18:24:11+00:00",
    "updated_at": "2022-05-19T18:24:11+00:00"
  },
  "pagination": {},
  "input_errors": {}
}

$ curl -d '{
  "name": "Trinkets",
  "description": "Also great!",
  "parent_id": "ad72b526-eed8-40ae-8857-d8e9c8c90890"
}' 'https://shopping.example.com/categories' | jq
{
  "status": "success",
  "error": "",
  "data": {
    "id": "0092c7f5-9baf-40a5-b329-155ba0354049",
    "parent_id": "ad72b526-eed8-40ae-8857-d8e9c8c90890",
    "name": "Trinkets",
    "description": "Also great!",
    "created_at": "2022-05-19T18:24:11+00:00",
    "updated_at": "2022-05-19T18:24:11+00:00"
  },
  "pagination": {},
  "input_errors": {}
}

And then we can see the final results:

$ curl 'https://products.always-upgrade.us/categories' | jq
{
  "status": "success",
  "error": "",
  "data": [
    {
      "id": "0092c7f5-9baf-40a5-b329-155ba0354049",
      "parent_id": "ad72b526-eed8-40ae-8857-d8e9c8c90890",
      "name": "Trinkets",
      "description": "Also great!",
      "created_at": "2022-05-19T18:24:11+00:00",
      "updated_at": "2022-05-19T18:24:11+00:00"
    },
    {
      "id": "ad72b526-eed8-40ae-8857-d8e9c8c90890",
      "parent_id": null,
      "name": "Widgets",
      "description": "Only the best!",
      "created_at": "2022-05-19T18:24:11+00:00",
      "updated_at": "2022-05-19T18:24:11+00:00"
    }
  ],
  "pagination": {
    "number_results": 2,
    "limit": 100,
    "next_page": {}
  },
  "input_errors": {}
}

Next we can create a manufacturer:

$ curl -d '{"name":"Cimpress","description":"Best of class!"}' 'https://shopping.example.com/manufacturers'
{
  "status": "success",
  "error": "",
  "data": {
    "id": "402c7399-1980-4b8c-b23f-1c83d4e23d78",
    "name": "Cimpress",
    "description": "Best of class!",
    "created_at": "2022-05-19T18:24:11+00:00",
    "updated_at": "2022-05-19T18:24:11+00:00"
  },
  "pagination": {},
  "input_errors": {}
}

And now we can create a product!

$ curl -d '{
  "name":"Gadget",
  "description":"Yes!",
  "manufacturer_id":"402c7399-1980-4b8c-b23f-1c83d4e23d78",
  "manufacturer_part_number":"123456",
  "categories":["0092c7f5-9baf-40a5-b329-155ba0354049"],
  "price":100
}' 'https://shopping.example.com/products'
{
  "status": "success",
  "error": "",
  "data": {
    "id": "94fb4bad-c5de-4157-87f9-bab2ba04bd4e",
    "manufacturer_id": "402c7399-1980-4b8c-b23f-1c83d4e23d78",
    "name": "Gadget",
    "short_description": "",
    "manufacturer_part_number": "123456",
    "description": "Yes!",
    "price": 100,
    "categories": [
      {
        "id": "0092c7f5-9baf-40a5-b329-155ba0354049",
        "name": "Trinkets"
      }
    ],
    "created_at": "2022-05-19T18:24:11+00:00",
    "updated_at": "2022-05-19T18:24:11+00:00"
  },
  "pagination": {},
  "input_errors": {}
}

As you can see, we have built and deployed a fully functional application, without needing a single hard-coded credential anywhere!

Previous
Migration