Multiple Deployment Enviroments With Klotho
Overview
In this tutorial, we'll use Klotho to transform an application to be run in multiple different deployment environments.
We will show how to standardize defaults for development and production environments as well as generate multiple instances of your application for their various stages, like development vs production.
Why?
In many cases, one application needs to be deployed to different environments and these environments may need different setups. For example, within a development environment we may need less storage within our databases then we would in production.
Getting started with deployment environments
Deployment environments can be defined through a combination of Klotho config definition and command line arguements during compilation.
Each generated compiled folder has a one to one link to deployment environments. If we want to have differing configuration of stages and environments, we will want to target new output directories on each compilation.
Prerequisites
- Klotho CLI installed
curl
- Node.js 16.x+ (& NPM)
- TypeScript
Repository
Clone our sample apps git repo and install the npm packages for the ts-orm
app.
git clone https://github.com/klothoplatform/sample-apps.git
cd sample-apps/ts-orm
npm install
Application Overview
The ts-orm
application utilizes the following annotations
- index.ts
- ./typeorm/logic.ts
- ./sequelize/model.ts
/**
* @klotho::execution_unit {
* id = "sequelize-main"
* }
*/
import * as express from "express";
...
/*
* @klotho::expose {
* target = "public"
* id = "sequelizeApp"
* }
*/
app.listen(3000, async () => {
console.log(`App listening locally`);
});
...
/* @klotho::persist {
* id = "typeormDB"
* }
*/
const AppDataSource = new DataSource({
type: "postgres",
entities: [User],
synchronize: true,
logging: false,
})
...
/**
* @klotho::execution_unit {
* id = "sequelize-main"
* }
*/
...
/** @klotho::persist {
* id = "sequelizeDB"
* }
* */
const sequelize = new Sequelize(`sqlite::memory:`, { logging: false });
...
Klotho compile the application
Let start by compiling our TypeScript application into JavaScript.
npx tsc
Then we'll compile the application using Klotho.
klotho . --app myfirstapp --provider aws
██╗ ██╗██╗ ██████╗ ████████╗██╗ ██╗ ██████╗
██║ ██╔╝██║ ██╔═══██╗╚══██╔══╝██║ ██║██╔═══██╗
█████╔╝ ██║ ██║ ██║ ██║ ███████║██║ ██║
██╔═██╗ ██║ ██║ ██║ ██║ ██╔══██║██║ ██║
██║ ██╗███████╗╚██████╔╝ ██║ ██║ ██║╚██████╔╝
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝
Adding resource input_file_dependencies:
Adding resource exec_unit:sequelize-main
Found 4 route(s) on server 'app'
Adding resource gateway:sequelizeApp
Adding resource persist_orm:sequelizeDB
Adding resource persist_orm:typeormDB
Adding resource topology:sample
Adding resource infra_as_code:Pulumi (AWS)
Pulumi.myfirstapp.yaml: Make sure to run `pulumi config set aws:region YOUR_REGION --cwd 'compiled/' -s 'myfirstapp'` to configure the target AWS region.
Initial Config of the Application
This initial Klotho compilation will generate a Klotho configuration file at ./compiled/klotho.yaml
(based on the --outDir
in the CLI) and look similar to:
defaults:
execution_unit:
type: lambda
pulumi_params_by_type:
fargate:
cpu: 256
memory: 512
lambda:
memorySize: 512
timeout: 180
expose:
type: apigateway
persist:
kv:
type: dynamodb
fs:
type: s3
secret:
type: s3
orm:
type: rds_postgres
pulumi_params_by_type:
rds_postgres:
allocatedStorage: 20
instanceClass: db.t4g.micro
skipFinalSnapshot: true
redis:
type: elasticache
pulumi_params_by_type:
elasticache:
nodeType: cache.t3.micro
numCacheNodes: 1
execution_units:
sequelize-main:
type: lambda
pulumi_params:
memorySize: 512
timeout: 180
exposed:
sequelizeApp:
type: apigateway
persisted:
sequelizeDB:
type: rds_postgres
pulumi_params:
allocatedStorage: 20
instanceClass: db.t4g.micro
skipFinalSnapshot: true
typeormDB:
type: rds_postgres
pulumi_params:
allocatedStorage: 20
instanceClass: db.t4g.micro
skipFinalSnapshot: true
Defining stage configs
We can copy the default Klotho config into 2 new files within our applications root directory (dev_klotho.yaml
and prod_klotho.yaml
) to set a config on a stage basis. This will allow us to define our defaults on a per stage basis as needed and define the CLI flags so the compile is easily repeatable. We'll also remove the parts we don't need to override to keep the file easy to read and to track improvements to the defaults.
For our development environments, we will define our configuration dev_klotho.yaml
as:
app: myfirstapp-dev # Updated app name to use -dev suffix to keep its resources in a different namespace
provider: aws
path: dist
out_dir: dev_compiled # Change the out_dir so different environments compile to different directories
defaults:
orm:
type: rds_postgres
pulumi_params_by_type:
rds_postgres:
allocatedStorage: 20 # Leave the storage of the instance at a minimum for development purposes
instanceClass: db.t4g.micro # Make sure this uses a micro instance, in case the default changes
skipFinalSnapshot: true # We do not need to store data on instance deletion within our development environment
Within production, we will define our configuration to have more storage and bigger instances for our relational databases, thus defining the defaults in prod_klotho.yaml
as:
app: myfirstapp-prod # Updated app name to use -prod suffix to keep its resources in a different namespace
provider: aws
path: dist
out_dir: prod_compiled # Change the out_dir so different environments compile to different directories
defaults:
orm:
type: rds_postgres
pulumi_params_by_type:
rds_postgres:
allocatedStorage: 200 # Increase the storage of the instance for production load
instanceClass: db.t4g.large # Use a large instance for production load
skipFinalSnapshot: false # Ensure we will have a final snapshot of data on instance deletion
Generating stage and environment level applications
At this point in the tutorial, we understand the defaults we would like in our development and production environments. Now we want to generate the application for those environments.
Dev
To generate our development application, we will recompile using the dev config. We will also direct the compilation to go to a dev compiled directory to keep it separate from other compilations of the application.
klotho --config dev_klotho.yaml
██╗ ██╗██╗ ██████╗ ████████╗██╗ ██╗ ██████╗
██║ ██╔╝██║ ██╔═══██╗╚══██╔══╝██║ ██║██╔═══██╗
█████╔╝ ██║ ██║ ██║ ██║ ███████║██║ ██║
██╔═██╗ ██║ ██║ ██║ ██║ ██╔══██║██║ ██║
██║ ██╗███████╗╚██████╔╝ ██║ ██║ ██║╚██████╔╝
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝
Adding resource input_file_dependencies:
Adding resource exec_unit:sequelize-main
Found 4 route(s) on server 'app'
Adding resource gateway:sequelizeApp
Adding resource persist_orm:sequelizeDB
Adding resource persist_orm:typeormDB
Adding resource topology:sample
Adding resource infra_as_code:Pulumi (AWS)
If you look at the klotho.yaml generated within the dev_compiled
directory, you will see the following configuration for our applications persist resources:
persisted:
sequelizeDB:
type: rds_postgres
pulumi_params:
allocatedStorage: 20
instanceClass: db.t4g.micro
skipFinalSnapshot: true
typeormDB:
type: rds_postgres
pulumi_params:
allocatedStorage: 20
instanceClass: db.t4g.micro
skipFinalSnapshot: true
Prod
To generate our production application, we will recompile using the prod config. We will also direct the compilation to go to a prod compiled directory to keep it separate from other compilations of the application.
klotho --config prod_klotho.yaml
██╗ ██╗██╗ ██████╗ ████████╗██╗ ██╗ ██████╗
██║ ██╔╝██║ ██╔═══██╗╚══██╔══╝██║ ██║██╔═══██╗
█████╔╝ ██║ ██║ ██║ ██║ ███████║██║ ██║
██╔═██╗ ██║ ██║ ██║ ██║ ██╔══██║██║ ██║
██║ ██╗███████╗╚██████╔╝ ██║ ██║ ██║╚██████╔╝
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝
Adding resource input_file_dependencies:
Adding resource exec_unit:sequelize-main
Found 4 route(s) on server 'app'
Adding resource gateway:sequelizeApp
Adding resource persist_orm:sequelizeDB
Adding resource persist_orm:typeormDB
Adding resource topology:sample
Adding resource infra_as_code:Pulumi (AWS)
If you look at the klotho.yaml generated within the prod_compiled
directory, you will see the following configuration for our applications persist resources:
persisted:
sequelizeDB:
type: rds_postgres
pulumi_params:
allocatedStorage: 200
instanceClass: db.t4g.large
skipFinalSnapshot: false
typeormDB:
type: rds_postgres
pulumi_params:
allocatedStorage: 200
instanceClass: db.t4g.large
skipFinalSnapshot: false
If you need to deploy your production application to more than one environment, you can follow the same steps as above, but break it out into multple compiled directories. The nyou will have granular control of each production environments infrastructure, while using the same defaults throughout production.
Deploying the updated application
After following the steps in the Deploying tutorial you know have your dev and prod environments up and running!