Skip to content

ninjarobot/FsCloudInit

Repository files navigation

FsCloudInit

Create cloud-init virtual machine configuration files in F#.

Build and Test FsCloudInit on Nuget

Examples

FsCloudInit includes configuration builders for many common tasks to simplify building the cloud-config file while also providing type checking around configuration code.

#r "nuget: FsCloudInit"

open FsCloudInit
open FsCloudInit.Builders

cloudConfig {
    package_upgrade true
    add_packages [
        "curl"
        "httpd"
    ]
}
|> Writer.write
|> System.Console.WriteLine

This code outputs a simple cloud-config file.

#cloud-config
package_upgrade: true
packages:
- curl
- httpd

Builders or records

Builders can reduce some of the complexity by handling things like type casting and using sane defaults, but it's also possible without the builder.

#r "nuget: FsCloudInit"

open FsCloudInit

{
    CloudConfig.Default with
        Packages = [ Package "httpd" ]
        PackageUpgrade = Some true
}
|> Writer.write
|> System.Console.WriteLine

Pull external data for the configuration

Often the configuration is more complex or even has external dependencies, and we get the benefits of a full language and framework.

This configuration will install the dotnet 6.0 SDK on a new VM. This pulls the Microsoft package source and signing key when building the cloud-init configuration.

async {
    use http = System.Net.Http.HttpClient ()
    let! aptSourceRes = http.GetAsync "https://packages.microsoft.com/config/ubuntu/20.04/prod.list" |> Async.AwaitTask
    let! aptSourceVal = aptSourceRes.Content.ReadAsStringAsync () |> Async.AwaitTask
    let! gpgKeyRes = http.GetAsync "https://packages.microsoft.com/keys/microsoft.asc" |> Async.AwaitTask
    let! gpgKey = gpgKeyRes.Content.ReadAsStringAsync () |> Async.AwaitTask
    cloudConfig {
        add_apt_sources [
            aptSource {
                name "microsoft-prod"
                key gpgKey
                source aptSourceVal
            }
        ]
        package_update true
        add_packages [
            Package "apt-transport-https"
            PackageVersion (PackageName="dotnet-sdk-6.0", PackageVersion="6.0.100-1")
        ]
    }
    |> Writer.write
    |> Console.WriteLine
}

The above snippet writes a cloud-init configuration file to the console, resulting in the following configuration file that can be used to install the SDK on a server:

#cloud-config
apt:
  sources:
    microsoft-prod:
      key: >
        -----BEGIN PGP PUBLIC KEY BLOCK-----

        Version: GnuPG v1.4.7 (GNU/Linux)


        mQENBFYxWIwBCADAKoZhZlJxGNGWzqV+1OG1xiQeoowKhssGAKvd+buXCGISZJwT

        LXZqIcIiLP7pqdcZWtE9bSc7yBY2MalDp9Liu0KekywQ6VVX1T72NPf5Ev6x6DLV

        7aVWsCzUAF+eb7DC9fPuFLEdxmOEYoPjzrQ7cCnSV4JQxAqhU4T6OjbvRazGl3ag

        OeizPXmRljMtUUttHQZnRhtlzkmwIrUivbfFPD+fEoHJ1+uIdfOzZX8/oKHKLe2j

        H632kvsNzJFlROVvGLYAk2WRcLu+RjjggixhwiB+Mu/A8Tf4V6b+YppS44q8EvVr

        M+QvY7LNSOffSO6Slsy9oisGTdfE39nC7pVRABEBAAG0N01pY3Jvc29mdCAoUmVs

        ZWFzZSBzaWduaW5nKSA8Z3Bnc2VjdXJpdHlAbWljcm9zb2Z0LmNvbT6JATUEEwEC

        AB8FAlYxWIwCGwMGCwkIBwMCBBUCCAMDFgIBAh4BAheAAAoJEOs+lK2+EinPGpsH

        /32vKy29Hg51H9dfFJMx0/a/F+5vKeCeVqimvyTM04C+XENNuSbYZ3eRPHGHFLqe

        MNGxsfb7C7ZxEeW7J/vSzRgHxm7ZvESisUYRFq2sgkJ+HFERNrqfci45bdhmrUsy

        7SWw9ybxdFOkuQoyKD3tBmiGfONQMlBaOMWdAsic965rvJsd5zYaZZFI1UwTkFXV

        KJt3bp3Ngn1vEYXwijGTa+FXz6GLHueJwF0I7ug34DgUkAFvAs8Hacr2DRYxL5RJ

        XdNgj4Jd2/g6T9InmWT0hASljur+dJnzNiNCkbn9KbX7J/qK1IbR8y560yRmFsU+

        NdCFTW7wY0Fb1fWJ+/KTsC4=

        =J6gs

        -----END PGP PUBLIC KEY BLOCK-----
      source: >
        deb [arch=amd64,armhf,arm64] https://packages.microsoft.com/ubuntu/20.04/prod focal main
package_update: true
packages:
  - apt-transport-https
  - - dotnet-sdk-6.0
    - 6.0.100-1

Write files

Write some arbitrary data to a file. It will be base64 encoded automatically so there won't be any character escaping issues.

cloudConfig {
    write_files [
        writeFile {
            path "/var/lib/data/hello"
            content "hello world"
            owner "root:root"
            permissions "400"
        }
    ]
}
|> Writer.write

If you are writing files that should be owned by users other than root, you often need to use defer so they are not written until after that user exists. When not deferred, depending on the version of cloud-init, the file may not be written or it may end up owned by root.

cloudConfig {
    write_files [
        writeFile {
            path "/var/lib/data/hello"
            content "hello world"
            owner "myuser:myuser"
            defer true
        }
    ]
}
|> Writer.write

Install packages

    cloudConfig {
        package_upgrade
        add_packages [
            "curl"
            "screen"
            "httpd"
        ]
    }
    |> Writer.write

Run commands

cloudConfig {
    run_commands [
        [ "ls"; "-l"; "/" ]
        [ "sh"; "-c"; "date >> whatsthetime.txt && cat whatsthetime.txt" ]
        "apt update".Split null
    ]
}
|> Writer.write

Print a final message when done

cloudConfig {
    final_message "#### Cloud-init is done! ####"
}
|> Writer.write

Reboot after initialized

cloudConfig { 
    power_state (
        powerState {
            mode PowerState.Mode.Reboot
            message "Done with installation. Rebooting now."
        }
    )
}

Attach to Ubuntu Pro Subscription

cloudConfig {
    attach_ubuntu_pro (
        ubuntuPro {
            token "d6cec6a05314b7c63f251e2c0e238830"
        }
    )
}

Use Ubuntu 22.04 Pro with FIPS and Reboot to Switch Kernel

 cloudConfig {
     attach_ubuntu_pro (
         ubuntuPro {
             token "d6cec6a05314b7c63f251e2c0e238830"
             enable [
                 UbuntuPro.Services.FipsPreview
                 UbuntuPro.Services.EsmApps
                 UbuntuPro.Services.EsmInfra
             ]
         }
     )
     power_state (
         powerState {
             mode PowerState.Mode.Reboot
             message "Rebooting to enable FIPS kernel."
         }
       )
 }

Create additional users

cloudConfig {
    users [
        user {
            name "itme"
            gecos "My Account"
            ssh_import_github_id "mygithubusername"
            groups [ "sudo" ]
            sudo Sudo.AllPermsNoPasswd
        }
    ]
}