Create a Model
Introduction
In CDK, a Model definition is a customized extension of one of the base Octo models.
A new model consists of a few core components,
- Model is the base model extension.
- Model Schema defines the structure and validation rules for model data.
- Model Actions defines the actions that transforms the model into a set of resources.
In this tutorial we will define a custom region model for AWS from scratch.
A region in Octo is defined as a physical location where resources are deployed.
In AWS, this translates to an AWS region like us-east-1
or eu-west-1
.
For this example, we will assume that an entire AWS region is a single shard. Naturally, the total number of shards our users can then create is limited by the number of AWS regions. Further, we will restrict the total number of shards to be 2.
In real world this is rarely the case, and you will typically want to design your region to have multiple shards to maximize the total number of shards your users can create. But for demonstration purposes, this is a fair assumption.
Create a Model
To create an empty model, Octo provides you with a simple template.
Run this command from the root of your project,
npx @quadnix/octo-build create-model -n simple-region -t region --package @example
It should produce a directory structure like this,
src/modules/region/simple-region
└── models
└── region
├── actions
│ └── add-simple-region.model.action.ts
├── index.ts
├── simple-region.model.ts
└── simple-region.schema.ts
This model is currently empty, and we will write custom logic in these files.
Model Schema
A model schema defines the structure and validation rules for model data,
and will always extend from the base model schema.
For region, the base model schema is RegionSchema.
RegionSchema
definesregionId
which is the unique identifier for the region.- Additionally, we define
awsRegionAZs
as a list of AWS availability zones in the region. - And,
awsRegionId
as the AWS region identifier. - We also validate our inputs using the built-in @Validate decorator.
export class SimpleRegionSchema extends RegionSchema {
@Validate({
destruct: (value: SimpleRegionSchema['awsRegionAZs']): string[] => value,
options: { minLength: 1 },
})
awsRegionAZs = Schema<string[]>();
@Validate({ options: { minLength: 1 } })
awsRegionId = Schema<string>();
}
Model
A model class, like schema, extends from one of the base models.
For region, the base model is Region.
- We redeclared the same properties as defined in the schema.
- We then wrote a constructor to initialize the model data based on user inputs.
We have restricted the
awsRegionId
to only 2 AWS regions, and we automatically populate the availability zones.
In a production environment, you would want to allow more AWS regions, and set the AWS availability zones accordingly. - Calling
super()
ensures the base model initializesregionId
. Here, regionId is same asawsRegionId
. - We also marked our model with the @Model decorator.
export class SimpleRegion extends Region {
readonly awsRegionAZs: string[];
readonly awsRegionId: string;
override readonly regionId: string;
constructor (awsRegionId: ['us-east-1', 'us-west-2']) {
super(awsRegionId);
// E.g. for us-east-1, the AZs will be us-east-1a and us-east-1b.
this.awsRegionAZs = [`${awsRegionId}a`, `${awsRegionId}b`];
this.awsRegionId = awsRegionId;
this.regionId = awsRegionId;
}
}
Then we complete our model definition by implementing the required
synth()
and unSynth()
methods to correctly serialize and deserialize the model.
override synth (): SimpleRegionSchema {
return {
awsRegionIds: [...this.awsRegionIds],
awsRegionId: this.awsRegionId,
regionId: this.regionId,
};
}
static override async unSynth (region: SimpleRegionSchema): Promise<SimpleRegion> {
return new SimpleRegion(region.awsRegionId);
}
Model Action
In the model action, we can transform our model into a set of resources.
In this example we will reuse the SimpleVpc
resource we created in the previous section.
The filter()
method is used by Octo to determine which model actions to execute.
When Octo attempts to add this model,
this method is used to determine which model actions qualify for the given model and diff action.
filter(diff: Diff): boolean {
return (
diff.action === DiffAction.ADD &&
diff.node instanceof SimpleRegion &&
hasNodeName(diff.node, 'region') &&
diff.field === 'regionId'
);
}
The handle()
method is where you can define the actual logic to generate resources.
actionInputs.inputs
are module inputs provided by the users. Follow the link to learn more on how to create a module.
async handle(
diff: Diff<SimpleRegion>,
actionInputs: EnhancedModuleSchema<SimpleRegionModule>,
actionOutputs: ActionOutputs,
): Promise<ActionOutputs> {
const region = diff.node;
// Create a VPC.
const vpc = new SimpleVpc(`vpc-${region.regionId}`, {
awsAccountId: actionInputs.inputs.account.accountId,
awsAvailabilityZones: [...region.awsRegionAZs],
awsRegionId: region.awsRegionId,
CidrBlock: actionInputs.inputs.vpcCidrBlock,
InstanceTenancy: 'default',
});
actionOutputs[vpc.resourceId] = vpc;
return actionOutputs;
}
Export a Model
Once you have created a model, it needs to be exported for other parts of your module to consume it.
import './actions/add-simple-region.model.action.js';
export { SimpleRegion } from './simple-region.model.js';
The index.ts
file serves 2 main purpose,
- It creates a single point of entry for your model, thus also controlling what you really want to expose vs internal implementation details.
- It automatically wires up the model actions. Any time your model is imported anywhere,
the model actions will be imported and the
@Action
decorator inside will be registered.
If you miss importing any model actions in the index.ts
,
those actions won't get registered or triggered, since the @Action
decorator never ran,
and thus Octo will never know about this action.