* article: extending Pulumi language support via YAML * fix: add meta image * fix: add meta desc and fix font in image * fix: schedule article for today * Apply suggestions from code review Co-authored-by: Laura Santamaria <laura@nimbinatus.com>
245 lines
7.2 KiB
Markdown
245 lines
7.2 KiB
Markdown
---
|
|
title: "Extending Pulumi's Language Support via YAML"
|
|
|
|
# The date represents the post's publish date, and by default corresponds with
|
|
# the date this file was generated. Posts with future dates are visible in development,
|
|
# but excluded from production builds. Use the time and timezone-offset portions of
|
|
# of this value to schedule posts for publishing later.
|
|
date: 2022-06-08T10:56:34-05:00
|
|
|
|
# Use the meta_desc property to provide a brief summary (one or two sentences)
|
|
# of the content of the post, which is useful for targeting search results or social-media
|
|
# previews. This field is required or the build will fail the linter test.
|
|
# Max length is 160 characters.
|
|
meta_desc: Learn how to use Pulumi YAML as a bridge for CUE, JSONNET, and Rust. This open interface provides support to many other programming languages for Pulumi.
|
|
|
|
# The meta_image appears in social-media previews and on the blog home page.
|
|
# A placeholder image representing the recommended format, dimensions and aspect
|
|
# ratio has been provided for you.
|
|
meta_image: meta.png
|
|
|
|
# At least one author is required. The values in this list correspond with the `id`
|
|
# properties of the team member files at /data/team/team. Create a file for yourself
|
|
# if you don't already have one.
|
|
authors:
|
|
- david-flanagan
|
|
|
|
# At least one tag is required. Lowercase, hyphen-delimited is recommended.
|
|
tags:
|
|
- languages
|
|
|
|
# See the blogging docs at https://github.com/pulumi/pulumi-hugo/blob/master/BLOGGING.md.
|
|
# for additional details, and please remove these comments before submitting for review.
|
|
---
|
|
|
|
It's a surprise to nobody that Pulumi's YAML support has me rather excited, even though I'm unlikely to use YAML itself for Pulumi. So why do I find it exciting? Well, because it's an open interface to provide support to many other programming languages for Pulumi.
|
|
|
|
Let's take a look at using YAML as a bridge for CUE, JSONNET, and Rust.
|
|
|
|
<!--more-->
|
|
|
|
## CUE
|
|
|
|
It's no surprised to anybody that I'm a rather large supporter of the CUE project. I worked hard on the YAML launch to ensure CUE was available, with our CTO Luke covering one of the examples in our release blog. More recently, I also dedicated a Modern Infrastructure to showing how to manage DNS records on Cloudflare with a CUE interface.
|
|
|
|
{{< youtube "YATPtKehD4o?rel=0" >}}
|
|
|
|
I believe that CUE could open up a whole world of possibilities for Pulumi and I'm excited to see what myself and others can come up with in this space in 2022.
|
|
|
|
To use CUE as a compiler for Pulumi YAML, you need to update the `Pulumi.yaml` file with the following compiler options:
|
|
|
|
```yaml
|
|
runtime:
|
|
name: yaml
|
|
options:
|
|
compiler: cue export
|
|
```
|
|
|
|
Next, run `cue mod init` to create a new CUE module. From here, you can create any file with a `.cue` extension and it'll become part of your Pulumi YAML project.
|
|
|
|
Here's a small example:
|
|
|
|
```cue
|
|
package pulumi
|
|
|
|
resources:
|
|
randomPassword: {
|
|
type: "random:RandomPassword"
|
|
properties: {
|
|
length: 16
|
|
special: true
|
|
overrideSpecial: "_%@"
|
|
}
|
|
}
|
|
|
|
outputs:
|
|
password: "${randomPassword.result}"
|
|
```
|
|
|
|
## JSONNET
|
|
|
|
In much the same view as CUE, Pulumi YAML can actually be a compilation target for any YAML superset. Let's take a look at another popular option: JSONNET.
|
|
|
|
First, we need to configure the compiler options:
|
|
|
|
```yaml
|
|
runtime:
|
|
name: yaml
|
|
options:
|
|
compiler: jsonnet index.jsonnet
|
|
```
|
|
|
|
Then we can provide the `index.jsonnet` file. Here's the same example as above, but in JSONNET.
|
|
|
|
```jsonnet
|
|
{
|
|
resources: {
|
|
randomPassword: {
|
|
type: "random:RandomPassword",
|
|
properties: {
|
|
length: 16,
|
|
special: true,
|
|
overrideSpecial: "_%@"
|
|
}
|
|
}
|
|
}
|
|
|
|
outputs: {
|
|
password: "${randomPassword.result}"
|
|
}
|
|
}
|
|
```
|
|
|
|
Now, this is actually just a plain JSON object and we've not taken advantage of any JSONNET features yet. So let's create a function for creating this random resource.
|
|
|
|
```jsonnet
|
|
local random_password(length=16, special=true, overrideSpecial="@") = {
|
|
type: "random:RandomPassword",
|
|
properties: {
|
|
length: length,
|
|
special: special,
|
|
overrideSpecial: overrideSpecial,
|
|
},
|
|
};
|
|
|
|
|
|
local first = random_password();
|
|
local second = random_password();
|
|
|
|
{
|
|
resources: {
|
|
first: first,
|
|
second: second,
|
|
},
|
|
|
|
outputs: {
|
|
"firstPassword": "first.result"
|
|
}
|
|
}
|
|
```
|
|
|
|
Now we have a function for creating a resource, with default values. Nice.
|
|
|
|
## Rust
|
|
|
|
So, what if we wanted to do the same ... but with Rust? Let's see! First, our compiler options:
|
|
|
|
```yaml
|
|
runtime:
|
|
name: yaml
|
|
options:
|
|
compiler: cargo run
|
|
```
|
|
|
|
Next, we need to generate some Rust types to represent our Pulumi resources. Instead of doing the random resource, like above, let's go with the Cloudflare resources from the YouTube video I recorded.
|
|
|
|
```rust
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct Zone {
|
|
pub zone: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct Record {
|
|
pub name: String,
|
|
pub typ: String,
|
|
pub value: String,
|
|
}
|
|
```
|
|
|
|
Rust has great meta programming capabilities, meaning our types can all be enriched by [Serde](https://serde.rs/) to handle serialization through simple [derive macros](https://serde.rs/derive.html).
|
|
|
|
Next, we spec out some skeleton Pulumi types too:
|
|
|
|
```rust
|
|
use crate::cloudflare::{Record, Zone};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct Pulumi {
|
|
pub resources: HashMap<String, Resource>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(tag = "type")]
|
|
pub enum Resource {
|
|
#[serde(rename = "cloudflare:Zone")]
|
|
Zone { properties: Zone },
|
|
#[serde(rename = "cloudflare:Record")]
|
|
Record { properties: Record },
|
|
}
|
|
```
|
|
|
|
We represent all the "available" resources via the `Resource` enum, which has a special annotation to indicate the "type" for Pulumi. We also represent the top level YAML that Pulumi expects as the `Pulumi` type.
|
|
|
|
Lastly, we implement our `fn main` and create our resources, finishing by printing the YAML to `stdout`.
|
|
|
|
```rust
|
|
use serde_yaml;
|
|
use std::collections::HashMap;
|
|
|
|
mod cloudflare;
|
|
mod pulumi;
|
|
|
|
use cloudflare::Zone;
|
|
use pulumi::{Pulumi, Resource};
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut pulumi: Pulumi = Pulumi {
|
|
resources: HashMap::new(),
|
|
};
|
|
|
|
pulumi.resources.insert(
|
|
"rawkode-dev".into(),
|
|
Resource::Zone {
|
|
properties: Zone {
|
|
zone: "rawkode-dev.com".into(),
|
|
},
|
|
},
|
|
);
|
|
|
|
println!("{}", serde_yaml::to_string(&pulumi)?);
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
## What's Next?
|
|
|
|
Obviously, this isn't entirely practical and we can make this 100 times better. So what am I working on next? Well, instead of manually creating all the types that we need for Pulumi to function - what if we could pull in the `schema.json` that all Pulumi providers have and automatically generate the types? Continuing with Rust's meta programming, we could achieve this with a procedural macro.
|
|
|
|
What might that look like?
|
|
|
|
```rust
|
|
use pulumi::import_schema;
|
|
|
|
import_schema!("pulumi/pulumi-cloudflare");
|
|
```
|
|
|
|
This would generate the types, add functions for requesting outputs, and work with any Pulumi provider.
|
|
|
|
Stay tuned, I'll be back with updates as soon as I can.
|