Skip to content

pollenjp/cdk-ansible

Repository files navigation

CDK Ansible by Rust

This project is in an alpha state.

  • cdk-ansible Crates.io
  • cdk-ansible-cli Crates.io

CDK-Ansible is a CDK (Cloud Development Kit) for Ansible, and inspired by AWS CDK.

While Ansible's playbook and inventory files are written in YAML format, managing YAML templating can be challenging. CDK-Ansible enables you to generate Ansible files using Rust as a type-safe programming language.

WARNING: This project does not use JSii.

cdk-ansible

Features

  • cdk-ansible can define an abstract Ansible Playbook using Rust.
  • cdk-ansible-cli (cdk-ansible command) generates Rust code compatible with existing Ansible modules.

Version Compatibility

{cdk-ansible,cdk-ansible-cli}>=0.2.0 compatibility is shown in the table below.

cdk-ansible cdk-ansible-cli
>=0.2.0 >=0.2.0

{cdk-ansible,cdk-ansible-cli}<v0.2.0 are guaranteed to work only with matching versions.

Quick Start

Template project can see in cdk-ansible-examples and support cargo-generate.

  1. Install mise (not required but recommended)

  2. Install cargo-generate, for example:

    mise use -g rust@latest
    mise use -g cargo-binstall
    mise use -g cargo-generate
  3. Init your project from template and 'Enter' (select default template)

    cargo generate gh:pollenjp/cdk-ansible-examples
  4. Execute the sample command in your local environment.

    # For the command details, refer to `[tasks.run]` in `mise.toml`.
    mise run run
  5. Edit src/stack/*.rs and src/inventory/*.rs (or src/inventory.rs) as you like.

What can cdk-ansible do?

Define Plays

Define Ansible Play in Rust.

let play = Box::new(Play {
    name: "sample-play".into(),
    hosts: hosts.into(),
    options: PlayOptions::default(),
    tasks: vec![Task {
        name: "Debug".into(),
        options: TaskOptions::default(),
        command: Box::new(::cdkam::ansible::builtin::debug::Module {
            module: ::cdkam::ansible::builtin::debug::Args {
                options: ::cdkam::ansible::builtin::debug::Opt {
                    msg: OptU::Some("Hello, world!".into()),
                    ..Default::default()
                },
            },
        }),
    }],
}),

Define Stacks (L1 Stack)

Create cdk-ansible's Stack and define the relationship between Play (Sequential, Parallel, etc.).

// let play1, play2, play3, ...

pub struct SampleStack {
    exe_play: ExePlay,
}

impl SampleStack {
    pub fn new(hp: &HostPool) -> Self {
        let hosts = hp.localhost.name.as_str();

        Self {
            exe_play: ExeSequential(vec![
                ExeSingle(Box::new(play1)),
                ExeSingle(Box::new(play2)),
                ExeParallel(vec![
                    ExeParallel(vec![
                        ExeSingle(Box::new(play3)),
                        ExeSingle(Box::new(play4)),
                        ExeSingle(Box::new(play5)),
                    ]),
                    ExeSequential(vec![
                        ExeSingle(Box::new(play6)),
                        ExeSingle(Box::new(play7)),
                    ]),
                    ExeSingle(Box::new(play8)),
                ]),
                ExeSingle(Box::new(play9)),
            ]),
        }
    }
}
%%{init: {'theme': 'redux', 'themeVariables': { 'fontSize': '30pt'}}}%%
stateDiagram
  direction LR
  state ForkExeParallel1 <<fork>>
  state ForkExeParallel2 <<fork>>
  state JoinExeParallel2 <<join>>
  state JoinExeParallel1 <<join>>
  [*] --> play1
  play1 --> play2
  play2 --> ForkExeParallel1
  ForkExeParallel1 --> ForkExeParallel2
  ForkExeParallel2 --> play3
  ForkExeParallel2 --> play4
  ForkExeParallel2 --> play5
  play3 --> JoinExeParallel2
  play4 --> JoinExeParallel2
  play5 --> JoinExeParallel2
  ForkExeParallel1 --> play6
  play6 --> play7
  ForkExeParallel1 --> play8
  JoinExeParallel2 --> JoinExeParallel1
  play7 --> JoinExeParallel1
  play8 --> JoinExeParallel1
  JoinExeParallel1 --> play9
  play9 --> [*]
Loading

Instantiate an App

Instantiate CDK-Ansible's App and add Inventory and Stack to it.

pub fn run() -> Result<()> {
    let host_pool = HostPool {
        localhost: LocalHost {
            name: "localhost".into(),
        },
        host_a: Rc::new(HostA {
            name: "host-a".into(),
            fqdn: "host-a.example.com".into(),
        }),
        host_b: RefCell::new(HostB {
            name: "host-b".into(),
            fqdn: "host-b.example.com".into(),
        }),
    };

    let mut app = App::new(std::env::args().collect());
    app.add_inventory(host_pool.to_inventory()?)?;
    app.add_stack(Box::new(SampleStack::new(&host_pool)))?;
    app.run()
}

Run the App

Run your app.

cargo run --package my-app -- deploy -P 3 -i dev SampleStack

If your ansible command is installed through uv, pass --playbook-command option like below.

https://github.com/pollenjp/cdk-ansible-examples/blob/a5d5568fa170047fae4b7327b26c5ba16a37f88f/cli-init/xtasks/test/cdk-ansible-cli-init#L33-L40

L2 Stack (Recommended)

CDK-Ansible can also define higher-level L2 (Layer 2) stacks. In comparison, the previous Stack is called an L1 Stack.

simple-sample for L2 Stack

  • L1 Stack first statically defines all Inventory and Play objects before moving to the execution phase.
  • In contrast, L2 Stack generates Inventory and Play objects just before executing each Play.
  • This allows generating Inventory and Play objects based on the execution state up to that point. This functionality can serve as an alternative to Ansible's Dynamic Inventory.
%%{init: {'theme': 'redux', 'themeVariables': { 'fontSize': '30pt'}}}%%
stateDiagram
  direction LR
  state ForkExeParallel2 <<fork>>
  state JoinExeParallel2 <<join>>
  [*] --> LazyPlayL2_1
  LazyPlayL2_1 --> ForkExeParallel2
  ForkExeParallel2 --> LazyPlayL2_2
  ForkExeParallel2 --> LazyPlayL2_3
  LazyPlayL2_2 --> JoinExeParallel2
  LazyPlayL2_3 --> JoinExeParallel2
  JoinExeParallel2 --> LazyPlayL2_4
  LazyPlayL2_4 --> [*]
Loading
%%{init: {'theme': 'redux', 'themeVariables': { 'fontSize': '10pt'}}}%%
stateDiagram
  direction LR
  classDef Rose stroke-width:1px,stroke-dasharray:none,stroke:#FF5978,fill:#FFDFE5,color:#8E2236;
  state LazyPlayL2_1 {
    direction LR
    [*] --> s2
    s2 --> s4
    s4 --> [*]
  }
  [*] --> LazyPlayL2_1
  LazyPlayL2_1 --> [*]
  s2:Generate Inventory<br>and Play (Playbook)
  s4:Run Playbook
  class s2 Rose
Loading

The definition of L2 Stack is similar to L1, but it implements the StackL2 trait. The difference is that fn exe_play returns &LazyExePlayL2.

struct SampleStack {
    exe_play: LazyExePlayL2,
}

impl SampleStack {
    fn new() -> Self {
        Self {
            exe_play: LazyExePlayL2::Single(Arc::new(SampleLazyPlayL2Helper::new("sample"))),
        }
    }
}

impl StackL2 for SampleStack {
    fn name(&self) -> &str {
        std::any::type_name::<Self>()
            .split("::")
            .last()
            .expect("Failed to get a stack name")
    }
    fn exe_play(&self) -> &LazyExePlayL2 {
        &self.exe_play
    }
}

pub fn main2() -> Result<()> {
    // AppL2 can define stacks with chainable method calls.
    AppL2::new(std::env::args().collect())
        .stack(Arc::new(SampleStack::new()))
        .expect("Failed to add sample stack")
        .run()
}

LazyExePlayL2 corresponds to ExePlay, but it can accept objects that implement the LazyPlayL2 trait instead of Play objects. The LazyPlayL2 trait implements an async function fn lazy_play_l2 which can generate information equivalent to an Ansible Play within this function.

struct SampleLazyPlayL2Helper {
    name: String,
}

impl SampleLazyPlayL2Helper {
    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
        }
    }
}

impl LazyPlayL2 for SampleLazyPlayL2Helper {
    fn lazy_play_l2(&self) -> BoxFuture<'static, Result<ExePlayL2>> {
        let name = self.name.clone();
        async move {
            Ok(PlayL2 {
                name,
                hosts: HostsL2::new(vec![Arc::new(HostA {
                    name: "localhost".to_string(),
                })]),
                options: PlayOptions::default(),
                tasks: create_tasks_helper(2)?,
            }
            .into())
        }
        .boxed()
    }
}

PlayL2::hosts is not a simple string, but an object that implements the HostInventoryVarsGenerator trait. This allows you to generate Inventory dynamically based on the execution state up to that point.

struct HostA {
    name: String,
}

impl HostInventoryVarsGenerator for HostA {
    fn gen_host_vars(&self) -> Result<HostInventoryVars> {
        Ok(HostInventoryVars {
            ansible_host: self.name.clone(),
            inventory_vars: vec![("ansible_connection".to_string(), "local".into())],
        })
    }
}

cdk-ansible-cli (cdk-ansible command)

Install

mise

MISE is recommended as it allows you to keep the versions of the cdk-ansible crate and CLI in sync.

mise use cargo:cdk-ansible-cli

binstall

binstall

cargo binstall cdk-ansible-cli

shell

See the latest release page.

cargo install

cargo install cdk-ansible-cli

Requirements

  • cdk-ansible-cli
    • rustfmt
      • rustup component add rustfmt

Tutorial

Create Ansible Module package for a workspace

Running cdk-ansible module command generates a Rust package for the specified Ansible module.

# '<namespace>.<collection>.<module>' only generates the specified module.
cdk-ansible module --output-dir crates/ --module-name ansible.builtin.debug

cdkam_ansible in below example is auto-generated by cdk-ansible module command.

your-cdk-ansible-app/
|-- Cargo.toml
`-- crates/
    `-- my-app/         ... your app (run `cdk_ansible::App`)
    `-- cdkam_ansible/  ... auto-generated by `cdk-ansible module` command
        |-- Cargo.toml
        `-- src/
            |-- lib.rs
            |-- m/ansible/builtin/debug.rs
            `-- ...

cdk-ansible module command has other options.

# '<namespace>.<collection>' generates all modules in the collection.
cdk-ansible module --output-dir crates/ --module-name-regex 'ansible\.builtin\..*'
# '<namespace>' generates all modules in the namespace.
cdk-ansible module --output-dir crates/ --module-name-regex 'ansible\..*'
# If you don't specify `--module-name` or `--module-name-regex`,
# all modules accessible from your ansible environment will be generated.
# (This is the same as `--module-name-regex '*'`)
cdk-ansible module --output-dir crates/

# If you are using uv to manage your ansible project, move to the directory or specify the `--project` option.
uv --project /path/to/your/ansible-project run \
  cdk-ansible module --output-dir crates/ --module-name ansible.builtin.debug

About

Cloud Development Kit for Ansible

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 5