RakuOps. Issue Number 2.

RakuOps series – an attempt to show people who write on Raku how to use the language in daily DevOps tasks – automation, configuration management, Docker containers and so on

It’s been two weeks I’ve been playing with Sparrowdo – an automation tool written on Raku and based on Sparrow automation framework. Now it’s a time to share some cool features I’ve added recently. But before to do that let me remind you how it all started.

Multiple hosts management

After publishing an issue number 1, I received a comment from @bobthecimerian in r/rakulang reddit post:

“Assume for the sake of discussion that I want to manage 5 machines with Sparrow6 and run Docker on all of them. Do I have to install Sparrow6 on all of them, and deploy Sparrow6 tasks to all of them? Then I use ssh, or ssh through the Sparrow6 DSL, to run tasks that install Docker and other software? Do I have to manage ssh authorized keys and network addresses for each machine that I am configuring myself, or does Sparrow6 have tasks or other tools to make that management easier?”

So, I thought – “Wait … what a cool use case I can reveal here, I just need to add some features to Sparrowdo and that is it!”

Why?

The idea of managing multiple hosts is quite common. Say, you have a bunch of related VMs in your network, and you want to manage them consistently – installing the same packages, running services, so on. Or you have a multi tier application – frontend/backend/database and you need to manage a configuration of each node specifically, but still need to connect those nodes through different protocols. Of course, in days of immutable infrastructure and Kubernetes these types of tasks could be solved using Docker. But what if I want something lightweight, flexible and not involving industrial scale efforts? Here is where Sparrrowdo could be a good alternative, especially for people writing on Raku.

Dependencies

This what we need for this tutorial. You don’t have to install those tools, unless you want to experiment with given topic in practice, but here we are:

* Terraform to create ec2 instances in amazon aws
* Free tier Amazon account
* Aws cli to launch ec2 instances with Terraform
* Sparrowdo to provision hosts
* Sparky – Sparrowdo backend to asynchronously execute Sparrowdo scenarios

Spin up infrastructure

Creation of bare bone infrastructure is relatively easy with Terraform – multi cloud infrastructure deployment tool. It’s de-facto an industrial standard for infrastructure management. I am not a big fan of Terraform’s declarative style DSL but it works really well when we just need to spin up an infrastructure without provisioning stage (see later).

So let’s create a terraform scenario to create 3 ec2 linux instances with Ubuntu OS, representing frontend, backend and database nodes:

$ mkdir ~/terraform-example
$ cd terrafrom-example
$ nano example.tf

resource "aws_instance" "example" {

  ami           = "ami-2757f631"
  instance_type = "t2.micro"
  key_name = "mylaptop"

  tags = {
    Name = "frontend"
  }
}

resource "aws_instance" "example2" {
  ami           = "ami-2757f631"
  instance_type = "t2.micro"
  key_name = "mylaptop"

  tags = {
    Name = "backend"
  }
}

resource "aws_instance" "example3" {
  ami           = "ami-2757f631"
  instance_type = "t2.micro"
  key_name = "my-key"

  tags = {
   Name = "database"
  }
}

Ssh keys

But before we launch terraform script, we need to enable passwordless ssh setup to allow Sparrowdo provision stage runs from my laptop.

What I need is to generate ssh key and import it’s public part to my amazon account. When terraform creates ec2 instances it will reference to this key, which makes amazon inserts the public part into hosts configurations and finally makes passwordless ssh connect from my laptop to those hosts:

$ ssh-keygen -t rsa -C "my-key" -f ~/.ssh/my-key

$ aws ec2 import-key-pair --key-name "my-key" --public-key-material fileb://~/.ssh/my-key.pub

The clever bit here is we create a key pair named “my-key" and reference to it inside Terraform using key-name attribute.

Run terraform

Now let’s run terraform to create our first infrastructure consisting of 3 hosts.

$ terrafrom apply -auto-approve

aws_instance.example: Creating…
aws_instance.example2: Creating…
aws_instance.example3: Creating…
aws_instance.example: Still creating… [10s elapsed]
aws_instance.example2: Still creating… [10s elapsed]
aws_instance.example3: Still creating… [10s elapsed]
aws_instance.example: Still creating… [20s elapsed]
aws_instance.example2: Still creating… [20s elapsed]
aws_instance.example3: Still creating… [20s elapsed]
aws_instance.example2: Creation complete after 24s [id=i-0af378c47f68a1250]
aws_instance.example3: Creation complete after 24s [id=i-082ad29992e0c83eb]
aws_instance.example: Creation complete after 24s [id=i-0c15a8a728ad71302]


Once we apply terraform configuration to aws, in literally seconds we will get 3 ec2 instances with Ubuntu OS up and running in amazon cloud. Cool!

Sparrowdo

In devops terminology provisioning is a stage when we apply configuration on bare bone infrastructure resources, for example on virtual machines. This where Sparrowdo starts shining because it’s what the tool was designed for.

Let’s install Sparrowdo itself first. Sparrowdo is installed as a zef module:

$ zef install Sparrowdo –/test

Now let’s create a simple Sparrowdo scenario which will define provision logic.

Our first scenario – sparrowfile – will be as simple as that:

mkdir -p ~/sparrowdo-examples
cd ~/sparrow-examples
nano sparrowfile

package-install "nano";

Installing nano editor ( which I am bug fan of ) on all the nodes should be enough to test our first simple Sparrowdo configuration.

Sparky

Because we are going to run Sparrowdo in asynchronous mode, we need to install Sparky – asynchronous Sparrowdo runner. As a benefit it comes with nice web UI where build statuses are tracked and logs are visible:

$ mkdir ~/sparky-git
$ cd ~/sparky-git
$ git clone https://github.com/melezhik/sparky.git
$ zef install .

$ mkdir -p ~/.sparky/projects
$ raku db-init.pl6

$ nohup sparkyd &
$ nohup raku bin/sparky-web.pl6

Last 3 commands initialize Sparky internal database and run Sparky queue dispatcher with Sparky web UI which is accessible at 127.0.0.1:3000 endpoint.

But before we try to run any Sparrowdo provision let’s understand how do we know hosts network addresses bearing in mind we don’t want to hardcode ones into our configuration.

Terrafrom state

What is cool about Terrafrom it keeps infrastructure internal data in a special file which is called state in JSON format:

$ cat ~/terraform-example/terraform.tfstate

So it’s relatively easy to create a simple Raku script that parses the file and fetches all required configuration data:

$ cd ~/sparrowdo-example
$ nano hosts.aws.raku

use JSON::Tiny;

my $data = from-json("/home/melezhik/terraform-example/terraform.tfstate".IO.slurp);

my @aws-instances = $data<resources><>.grep({
  .<type> eq "aws_instance"
}).map({
  %(
    host => .<instances>[0]<attributes><public_dns>
  )
});

@aws-instances;

If we dump @aws-instances array we will see all 3 instances with public DNS address data:

[
  {
    host => "ec2-54-237-6-19.compute-1.amazonaws.com",
  },
  {
    host => "ec2-52-23-177-193.compute-1.amazonaws.com",
  },
  {
    host => "ec2-54-90-19-170.compute-1.amazonaws.com",
  },
]

If we pass a script as host parameter, Sparrowdowill be clever enough to run the one, and because the last script statement is @aws-instances array, take it as an input hosts list:

$ sparrowdo --host=aws.raku --ssh_user=ubuntu --bootstrap

queue build for [ec2-54-237-6-19.compute-1.amazonaws.com] on [worker-3]
queue build for [ec2-52-23-177-193.compute-1.amazonaws.com] on [worker-2]
queue build for [ec2-54-90-19-170.compute-1.amazonaws.com] on [worker-2]

This command will launch nano editor installation on all 3 hosts. A --boostrap flags asks Sparrowdo to install all Sparrow dependencies first, because we run provision for the first time.

As it’s seen through an output, Sparrowdo has triggered 3 builds and they got added to Sparky queue. If we open up a Sparky web UI we could see that 2 builds are already being executed:

And the third one is kept in a queue:

After awhile we could see all 3 instances are provisioned:

So all 3 hosts have been successfully provisioned. If we ssh to any hosts, we will see that nano editor is presented.

Build logs

Sparky UI allows to see builds logs where could find a lot of details of how configuration was provisioned. For example:

rakudo-pkg is already the newest version (2020.06-01).
0 upgraded, 0 newly installed, 0 to remove and 117 not upgraded.
===> Installing: Sparrow6:ver<0.0.25>

1 bin/ script [s6] installed to:
/opt/rakudo-pkg/share/perl6/site/bin
18:37:03 07/16/2020 [repository] index updated from http://rakudist.raku.org/repo//api/v1/index
18:37:07 07/16/2020 [install package(s): nano.perl] trying to install nano ...
18:37:07 07/16/2020 [install package(s): nano.perl] installer - apt-get
18:37:07 07/16/2020 [install package(s): nano.perl] Package: nano
18:37:07 07/16/2020 [install package(s): nano.perl] Version: 2.5.3-2ubuntu2
18:37:07 07/16/2020 [install package(s): nano.perl] Status: install ok installed
[task check] stdout match <Status: install ok installed> True


Now let’s see how we can provision hosts specifically, depending on roles assigned to hosts. Remember we have a frontend, backend and database hosts?

Custom configurations

The latest Sparrowdo release comes with an awesome feature called tags. Tags allow one to assign arbitrary variables per each host, and branch installation logic depending on that variables.

Let’s tweak a host inventory script hosts.aws.raku so that resulted @aws-instances array include elements with tags:

[
  {
    host => "ec2-54-237-6-19.compute-1.amazonaws.com",
    tags => "aws,frontend" 
  },
  {
    host => "ec2-52-23-177-193.compute-1.amazonaws.com",
    tags => "aws,backend"
  },
  {
    host => "ec2-54-90-19-170.compute-1.amazonaws.com",
    tags => "aws,database"
  },
]

As one can see, basically tags are plain strings with comma separated values.

To handle tags within Sparrowdo scenarios one should use tags() function:

$ nano sparrowdo-examples/sparrowfile

if tags()<database> {

  # Database specific code here

  package-install "mysql-server"; 

} elsif tags()<backend> {

  # Install Backend application 
  # And dependencies
 
  package-install "mysql-client";

  user "app";

  directory "/home/app/cro-example", %(
    owner => "app",
    group => "app"
  );

  git-scm "https://github.com/melezhik/cro-example.git", %(
    user => "app",
    to => "/home/app/cro-example"
  );

  zef ".", %(
     user => "app",
     cwd => "/home/app/cro-example"
  );

} elsif tags()<fronted> {

  # Install Nginx server 
  # As a fronted 
 
  package-install "nginx";

}

This simple example shows that we can create a single provision scenario where different nodes are configured differently depending on their roles.

Now we can run Sparrow the same way as we did before and nodes configurations will be updated according their types:

$ cd ~/sparrowdo-examples

$ sparrowdo --host=hosts.aws.raku --ssh_user=ubuntu

Filtering by tags

Another cool thing about tags is one can pass --tags as a command line argument and it will act as a filter to leave only certain types of hosts. Say, we only want to update database host:

$ sparrowdo --host=hosts.aws.raku --ssh_user=ubuntu --tags=database

If we pass multiple tags by using a "," delimiter it will act as an AND condition. For example:

--tags=database,production

Will only process hosts with tag set to database and production.

Hosts attributes

And last but not the least feature of tags is key/value data . If set a tag as name=value format, Sparrowdo will process this as a named attribute:

my $v = tags()<name>

This is how we pass an arbitrary data into Sparrowdo context using the same tag syntax. For example, let’s modify hosts inventory script, to pass IP address of backend node:

$ nano ~/sparrowdo-examples/hosts.aws.raku

use JSON::Tiny;

my $data = from-json("/home/melezhik/terraform-example/terraform.tfstate".IO.slurp);
my $backend-ip;
my @aws-instances = $data<resources><>.grep({
  .<type> eq "aws_instance"
}).map({

   if .<instances>[0]<attributes><tags><Name> eq "backend" {
     $backend-ip = .<instances>[0]<attributes><public_ip>
   }

  %(
    host => .<instances>[0]<attributes><public_dns>,
    tags => "name={.<instances>[0]<attributes><tags><Name>}"
  )
});

for @aws-instances {
  $i<tags> ~= "backend_ip={$backend_ip}"
}

@aws-instances;


Now @aws-instance array has a following structure:

[
  {
    host => "ec2-54-237-6-19.compute-1.amazonaws.com",
    tags => "aws,frontend,backend_ip=54.90.19.170" 
  },
  {
    host => "ec2-52-23-177-193.compute-1.amazonaws.com",
    tags => "aws,backend,backend_ip=54.90.19.170"
  },
  {
    host => "ec2-54-90-19-170.compute-1.amazonaws.com",
    tags => "aws,database,backend_ip=54.90.19.170"
  },
]

So, for database part we might have a following Sparrowdo scenario, to
allow host with backend_ip to connect to a mysql server:

if tags()<database> {

  my %state = task-run "set mysql", "set-mysql", %( 
    user => "test", 
    database => "test", 
    allow_host => tags()<backend_ip>, 
  ); 
 
  if %state<restart> { 
    service-restart "mysql" 
  }

 }

Let’s rerun Sparrowdo to apply changes to a MySQL server:

$ sparrowdo --host=hosts.aws.raku --ssh_user=ubuntu --tags=database

Other hosts formats

Sparrowdo supports different hosts format, including localhost and docker , please read a documentation to get more details.

Conclusion

Sparrowdo and Sparky are flexible tools allow one to asynchronously provision virtual resources. In this tutorial we’ve seen how easy one can spin up a multi tier application consisting of 3 nodes from the scratch.

Moreover, Sparrowdo works nice with some well known tools like Terrafrom that makes it’s even more attractive and practical.

See you soon, on the RakuOps issue number 3, please let me know what do you want to hear next time.

Thank you for reading!


Aleksei Melezhik

One thought on “RakuOps. Issue Number 2.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: