Posted on 07 Oct 18 by Andrea Bedini - Data Scientist
Hey! We made a thing!
At KZN Group, we are big fan of Jsonnet. Jsonnet is a small (functional) programming language developed as a simple extension of Json. Basically Jsonnet is Json plus variables, functions and few syntax improvements. You’re invited to check its website, but, as a taster, let me copy an example from it:
This:
local Person(name='Alice') = {
name: name,
welcome: 'Hello ' + name + '!',
};
{
person1: Person(),
person2: Person('Bob'),
}
becomes this:
{
"person1": {
"name": "Alice",
"welcome": "Hello Alice!"
},
"person2": {
"name": "Bob",
"welcome": "Hello Bob!"
}
}
You might think, Jsonnet can be helpful if you had to generate large Json documents, and, if you are familiar with AWS CloudFormation, you might be scratching your head already.
Indeed, since the beginning of this year, Jsonnet has been our card up our sleeve to efficiently compose large CloudFormation templates for our customers. If you want to know more about CloudFormation and Jsonnet, here is a presentation I recently gave at the Perth AWS User Group.
When we read about CloudFormation Macros, things got even more exciting.
Previously, our workflow consisted in creating a CloudFormation template in Jsonnet and render it to Json just before creating a stack. Now, exploiting CloudFormation macros, we can delegate this preprocessing step to CloudFormation itself. Moreover we can start with a YAML template and add little bits of Jsonnet here.
As an impromptu experiment, we created
cf-macro-jsonnet
,
admittedly inspired by Jay McConnell’s
PyPlate.
cf-macro-jsonnet
is a CloudFormation macro that allows you to embed
arbitrary Jsonnet code into your CloudFormation templates.
Here is a simple example. There are few things in life that bother me more that having to list tags in a CloudFormation resource as an arrary of keys and values 😂. So I can do this instead:
AWSTemplateFormatVersion: "2010-09-09"
Transform: [Jsonnet]
Resources:
S3Bucket:
Type: "AWS::S3::Bucket"
Properties:
Tags: |
#!jsonnet
local tags = {
Project: 'take-over-the-world',
Stage: 'testing'
};
[{ Key: k, Value: tags[k] } for k in std.objectFields(tags)]
The macro will traverse the template looking for strings that start with
#!jsonnet
. Such strings will be evaluated as Jsonnet code and the result
will be put back in place of the original string. So the above will be
rendered (and executed!) as I had written this.
AWSTemplateFormatVersion: "2010-09-09"
Resources:
S3Bucket:
Type: "AWS::S3::Bucket"
Properties:
Tags:
- Key: Project
Value: take-over-the-world
- Key: Stage
Value: testing
This has good potential but if you look carefully we actually had to write two extra lines in our Jsonnet version.
So we went a step further.
Jsonnet suports importing code from a separate file. This allows us to
create a library of reusable snippets. Following from the previous example,
we can create a file utils.jsonnet
with content:
{
toEntries(obj): [{ Key: k, Value: obj[k] } for k in std.objectFields(obj)],
}
and call it this way:
local utils = import "utils.jsonnet";
utils.toEntries({
Project: 'take-over-the-world',
Stage: 'testing'
})
cf-macro-jsonnet
hooks into this mechanism: all you have to do is zip up
your Jsonnet library and upload it to S3. Then we can use the library from
a templates.
AWSTemplateFormatVersion: "2010-09-09"
Transform: [Jsonnet]
JsonnetLibraryUri: s3://path/to/zip/file/above
Resources:
S3Bucket:
Type: "AWS::S3::Bucket"
Properties:
Tags: |
#!jsonnet
local utils = import "utils.jsonnet";
utils.toEntries({
Project: 'take-over-the-world',
Stage: 'testing'
})
Which still has the same number of lines but it’s way cooler 😂. Jokes
aside, there is no limit to the amount of complexity you can hide inside
library function, so this is cleary an improvement. I can think of few ways
to add things to the scope, avoding the explicit local
binding; but I
need to mull over it a bit longer.
To give an idea of how we are using Jsonnet to write CloudFormation template more efficiently, let me show an example. Writing a IAM role can be tedious:
{
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [ {
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
} ]
},
"Path": "/",
"Policies": [ {
"PolicyName": "InvokePolicy",
"PolicyDocument": {
"Statement": [ {
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
"*"
]
} ]
}
} ]
}
}
How about the following
#!jsonnet
local utils = import 'utils.jsonnet';
utils.assumeRole('lambda.amazonaws.com', [{
PolicyName: 'InvokePolicy',
PolicyDocument: {
Statement: [{
Effect: 'Allow',
Action: [
'lambda:InvokeFunction',
],
Resource: [
'*',
],
}],
},
}])
or even
#!jsonnet
local utils = import 'utils.jsonnet';
utils.assumeRole('lambda.amazonaws.com', [{
'Allow:*': 'lambda:InvokeFunction',
}])
where we write togheter the policy’s effect and resource. Of course this can’t express all possible policies but it does cover the most common cases.
The implementation of assumeRole
is left to the reader, pull-requests are
welcome :)
Thanks to Simon T, Eric H, and Ric H. from AWS who encouraged me to jot down this blog post.
For questions, suggestions or good jokes, don’t hesitate to ping me on
Twitter or by opening an issue on the
cf-macro-jsonnet
repository.