Blogs about Jazz

Blogs > Jazz Team Blog >

Using Slack to perform work in an UrbanCode Deploy environment

In this post, we’re going to show you how to build a Node.js application that monitors a Slack channel and responds to commands. These commands result in a command line tool (udclient) being called against our Urbancode Deploy (UCD) server.  This will allow you to tap into a continuous delivery pipeline, and run UCD processes that your build process also invokes when it runs.  You could also execute any arbitrary UCD process not associated with a build pipeline using this method.

This post comes directly from some of the work described in the blog post Continuous delivery practices within Jazz.net development.

Tools:

urbancode_img UrbanCode Deploy (UCD), for deployment automation

slack Slack, for team awareness and operational control

The capability to affect an operational environment can be extremely useful to Slack users who aren’t in front of their computer.  Giving them the ability to promote a build, for example, and then deploy that build to a UCD environment from Slack can help make remote operations easier and allow for organizational processes to be more centralized and concrete.

The flow of things boils down to this…

  1. A user issues a command on a certain Slack channel.  The user might type something like “jazznetdeploy: showEnvironments” into their channel. Here’s what it might look like in Slack
    simple_slack_command
  2. At this point, the Node.js application we’re implementing “hears” what the user typed into the channel.  If that meets what the application is currently matching on, it will fall into a command processing loop.
  3. The Node.js application parses the command, ensuring it’s valid and known.  On the channel, it will look as if the bot is typing while it processes.
  4. If the command is valid, our implementation winds up using the udclient command line utility to invoke a process on the Urbancode Deploy server.  In our example above, we chose showEnvironments to run the following udclient command:  udclientusername <user> -password <pass> –weburl <udeploy url> getEnvironmentsInApplication -application <appname>.  We pass appname in statically with the UCD name of the application we want information about, but you could make this a parameter, depending on how you choose to implement.
  5. The output of the command is displayed on the Slack channel, which you can see in the above screenshot, as json.  We chose this because it felt natural, but again, your implementation is free to return whatever you like.

Prerequisites:

  • An Urbancode Deploy server, configured with an agent to perform the work of deploying an application for you.  Your application could be anything; what’s important is that you use UCD application processes that do something useful for you.  In this case, we’ll be talking about deploying a small Node.js application, but this could really be anything.
  • The udclient cmd line utility, for interacting with Urbancode Deploy remotely. Here’s a link to the setup instructions from the UCD documentation.
  • Slack, and a channel you want to use for “slackops”, “chatops”, or whatever you want to call it.  From an operations perspective, it’s probably best to put these kinds of bots in private channels, which can create its own set of wrinkles with the Slack api, but this way, only certain people can interact with the bot. Since this is a piece of software that can affect the state of your deployed environments, exercising some care here makes sense.
  • A basic understanding of Slack webhooks and how to configure one.  See the Slack API site for more information.
  • slackr-bot, which is open source from github (see the repo at git://github.com/ehues/slackr-bot.git).   wibot is a customized version of slackr-bot  to monitor a channel in Slack for mentions of certain jazz work items.  We changed the way you configure it, but mostly it’s the same as slackr-bot in that it is able to retrieve information from the server and present it to everyone on a channel.  In this article, we use it as an example of something you can have your chat bot execute, effectively providing a switch for an arbitrary application controlled via Slack.  Note that we won’t provide wibot in this post, but as it is a Node.js application, you can drop your own in its stead.
  • A general understanding of Node.js applications, javascript, and continuous delivery/deployment, devops.

Steps:

Below, I describe a pattern that can be reused to enable changes to an UrbanCode Deploy environment, via Slack. Let’s say we have the following:

A UCD Application. Let’s call it sandbox.  The UCD application has a bunch of processes that do useful stuff.  We have a few different environments defined within the application, along with some variables in each to help us track the current state of things.  Let’s say we have TEST, STAGING, and PRODUCTION environments for our sandbox.

  1. You need to decide what UCD processes you want to run.  We have a bunch that help us gather information about our UCD environment, but also some that allow us to affect an environment.  These processes generally wind up modifying/adding environment properties, after they’ve performed their work, to record things like container IDs or test output in the relevant UCD environment.
  2. Ensure you can run your processes via udclient, on the agent machine.
    • We gave the example, earlier, of running a command called showEnvironments via Slack against our UCD application.  In this case, the command we would test manually would be udclient –username <user> -password <pass> –weburl <udeploy url> getEnvironmentsInApplication -application <appname>  
    • Another might be to get the properties for a particular environment.  In that case, the command would look like: udclientusername <user> -password <pass> getComponentEnvironmentProperties -component <component> -environment <environment>
    • For the command to restart wibot, which you can see more about below, you would run something like this: udclient –username <user> -password <pass> –weburl <udeploy url> requestApplicationProcess runRestartWibot.json.  The udclient will read the json file for the parameters to pass when requesting the application process on the UCD server.
  3. Create a Node.js application to listen to your chosen Slack channel.  Set the pattern you want it to listen for. There’s a sample file below with a skeleton implementation.
    • Command to create Node.js application.  Here’s a sample package file.  Before you use this package.json yourself, you might want to change somethings around.  Name, version, description should probably be changed, but the most important thing to change here is the value of main.  This should be whatever javascript file you want to run as part of the Node.js application.  Set environment variable for channel you plan to use prior to running the node application; the environment variable name is DEPLOYBOT_ERROR_CHANNEL.
    • Put udclient in the Node.js application.  You could alternatively choose to implement it so that you didn’t package the client, but relied on having the client at a known path at runtime.  In this case, we wanted the client packaged with our implementation.
    • This sample.js file is explained in more detail below.
    • Before you dig into the implementation, turn on the Node.js app and hopefully you’ll see a startup message on your Slack channel.
  4. Implement udclient command function methods and constants in the Node.js application.  When thinking of useful things to do for our team, we chose a list of published component versions (which lets you get the component version ids, in case you want to retrieve attachments from the published version), a list of environments (allows you to copy env id to other commands), help (list of supported commands), and restartWibot (using a process to execute another application on the agent machine).
    • The following is a low level function to prep the udclient file we need to run things.  The udclient utility expects a json file with certain things in it in order to request application processes be run on the UCD server.  We just have this method customize that file for us by replacing the @@ENVIRONMENT@@ and @@VERSION@@ tokens with environment ID and build version that get passed into the command, using sed for the replacement.

      function prepRestartWibotJson(message, environment, build){
          var exec = require('child_process').execSync;
          var jsonFile = RUNRESTARTWIBOT + JSONEXT;
          //run sed replacement on json before udclient executes
          var sed_cmd = 'sed ' + ' -e \'s/@@VERSION@@/' + build + '/\'' + ' -e \'s/@@ENVIRONMENT@@/' + environment + '/\'' + SPACE + RUNRESTARTWIBOT + JSONEXT + ' > ' + jsonFile;
          var sed_output =  exec(sed_cmd);
          console.log('prepRestartWibotJson: sed command invoked: ' + sed_cmd);
          console.log('sed_output: ' + sed_output);
          var cat_cmd = 'cat ' + jsonFile;
          var cat_output = exec(cat_cmd);
          console.log('prepRestartWibotJson: cat command invoked: ' + cat_cmd);
          console.log('cat_output: ' + cat_output);
          message.reply({
            text: cat_output
          });
          return cat_output;
      }
      • Before modifying the json file, we might have something that looks like this.  As the file is pretty small, we’ll also show you inline…

        {
        "application": "jazz.net",
        "description": "Restarts wibot on the docker host",
        "applicationProcess": "restartWibot",
        "environment": "@@ENVIRONMENT@@",
        "onlyChanged": "false",
        "versions": [
        {
        "version": "@@VERSION@@",
        "component": "jazz.net"
        }
        }

        “application” is the UCD application that this process is going to be run within.
        “applicationProcess” is the process in the UCD application that gets invoked by udclient
        “environment” is the UCD environment ID in which to run the application.  You can think of this as being possibly  TEST, STAGING, or PRODUCTION.
        “version” is the component version you want included in the run of the application process.

    • Here’s a higher level function called by the command loop.  This method uses the json file built above when calling udclient.

      • function restartWibot(message, environment, build){
            var exec = require('child_process').execSync;
            var fileContent = prepRestartWibotJson(message, environment, build);
        //udclient -username <user> -password <pass> -weburl <weburl> requestApplicationProcess runRestartWibot.json
            var udclient_cmd = BASEUDCLIENTCMD + UDCLIENT_REQUESTAPPLICATIONPROCESS + SPACE + RUNRESTARTWIBOT  + JSONEXT;
            console.log('restartWibot: command being invoked: ' + udclient_cmd);
            var output = exec(udclient_cmd);
            return output;
        }
    • Here’s a method to have udclient show environments in an application on the server.

      • function showUCDEnvironments(message, application){
        //run udclient -username <user> -password <pass> -weburl <url> getEnvironmentsInApplication -application <appname>
            var udclient_cmd = BASEUDCLIENTCMD + UDCLIENT_GETENVIRONMENTSINAPPLICATION + SPACE + PARAM_APPLICATION + SPACE + application;
            console.log('showUCDEnvironments invoked');
            var exec = require('child_process').exec,
              inspect = exec(udclient_cmd);
              var output = '';
              inspect.stdout.on('data', function(data){
                  output += data;
                  console.log("Environments found: " + data);
              });
            inspect.on('close', function (code) {
                 message.reply({
                        text: output
                 });
              console.log('showUCDEnvironments child process exited with code ' + code); 
            });
        }
    • Method to show properties on an Urbancode Deploy environment…

      • function showEnvironmentProperties(message, component, environment){
            var udclient_cmd = BASEUDCLIENTCMD + UDCLIENT_GETCOMPONENTENVIRONMENTPROPERTIES + SPACE + PARAM_COMPONENT + SPACE + component + SPACE + PARAM_ENVIRONMENT + SPACE + environment;
            console.log('showEnvironmentProperties invoked');
            var exec = require('child_process').execSync;
            var output = exec(udclient_cmd);
            console.log('showEnvironmentProperties output: ' + output);
            return output;
        }
  5. Implement function to parse Slack message (eg, deploybot: deployToStaging….)  This is just a simple example of an imperfect parser that supports a few commands.
    • function lookForDeployMessages(message, match) {
          console.log("match: " + match);
          message.typing();
          var command = String(match);
          var cmdarray = command.split(SPACE);
          var cmdparmlength = cmdarray.length;
          console.log('cmd parameter length: ' + cmdparmlength);
          for (var i = 0; i < cmdparmlength; i++) {
              console.log('cmdarray['+ i + ']: ' + cmdarray[i]);
              //Do something
          }
          if(command.indexOf(CMD_SHOWENV) > 0)
            showUCDEnvironments(message, "applicationName");
          else if(command.indexOf(CMD_SHOWENVPROPS) > 0){
               if(cmdarray.length >= 3){
                   var environment = cmdarray[2];
                   var envprops = String(showEnvironmentProperties(message, "applicationName", environment));                       
                   console.log('envprops is lookForDeployMessages: ' + envprops);
                   message.reply({
                       text: envprops
                   });
               }else{
                   message.reply({
                       text: 'Please supply an environment id, eg deploybot: ' + CMD_SHOWENV_PROPS +' 6aedfef9-4f53-43c4-9746-8a362ab271d6'
                   });
               }
          } else (command.indexOf(CMD_RESTARTWIBOT) > 0){
               if(cmdarray.length >= 4){
                  var build = cmdarray[2];
                   var environment = cmdarray[3];
                  var output = String(restartWibot(message, environment, build));
                  message.reply({
                       text: output
                  });
               }else{
                   message.reply({
                       text: 'Please supply a build and environment id, eg deploybot: ' + CMD_RESTARTWIBOT + '201604011-2234 6aedfef9-4f53-43c4-9746-8a362ab271d6'
                   });
               }     }else if(command.indexOf(CMD_HELP) > 0){
               message.reply({
                   text: 'currently supported commands are showEnvironments, showEnvironmentProperties <environmentId>, restartWibot <buildId> <environmentId>, and help'         });
           }else{
               message.reply({
                   text: 'Did not understand your request: ' + match
               });
           }
       } //end of lookForDeployMessages
    • There are a lot of ways to create a command parser; this is just one.  In the interest of time, we chose to use something that is more UNIX command-based, than, say, natural language based.  That would be one way to enhance the chat bot in the future.
    • At the end of the sample file, you can see where we tell the bot what to key in on in the Slack channel content.  The key line of code looks like this:
      sess.on(/jazznetdeploy:\s.*/i, lookForDeployMessages);

      This line of code tells the session object that when it encounters the pattern “jazznetdeploy:” it will invoke the lookForDeployMessages method against the message content.  Without this, our bot won’t listen to anything happening in Slack.  There is some code in the sample file, right below the sess.on call, that logs our bot startup results either to Slack or to the error console.