Google is committed to advancing racial equity for Black communities. See how.

Build a Slack Bot with Node.js on Kubernetes

In this codelab, you'll learn how to build a Slack bot using the Botkit toolkit and run it on Google Cloud Platform. You'll be able to interact with the bot in a live Slack channel.

What you'll learn

What you'll need

  • A Google Cloud Platform Project
  • A Browser, such Chrome or Firefox

How will you use this tutorial?

Read it through only Read it and complete the exercises

How would you rate your experience with using Google Cloud Platform?

Novice Intermediate Proficient

Self-paced environment setup

If you don't already have a Google Account (Gmail or Google Apps), you must create one. Sign-in to Google Cloud Platform console (console.cloud.google.com) and create a new project:

Screenshot from 2016-02-10 12:45:26.png

Remember the project ID, a unique name across all Google Cloud projects (the name above has already been taken and will not work for you, sorry!). It will be referred to later in this codelab as PROJECT_ID.

Next, you'll need to enable billing in the Cloud Console in order to use Google Cloud resources.

Running through this codelab shouldn't cost you more than a few dollars, but it could be more if you decide to use more resources or if you leave them running (see "cleanup" section at the end of this document).

New users of Google Cloud Platform are eligible for a $300 free trial.

Using Google Cloud Shell

While Google Cloud Platform and Node.js can be operated remotely from your laptop, in this codelab you will use Google Cloud Shell, a command line environment running in the Cloud.

This Debian-based virtual machine is loaded with all the development tools you'll need. It offers a persistent 5GB home directory, and runs on the Google Cloud, greatly enhancing network performance and authentication. This means that all you will need for this codelab is a browser (yes, it works on a Chromebook).

To activate Google Cloud Shell, from the developer console simply click the button on the top right-hand side (it should only take a few moments to provision and connect to the environment):

activateCloudShell.png

Click the "Start Cloud Shell" button:

Screen Shot 2017-06-14 at 10.13.43 PM.png

Once connected to the cloud shell, you should see that you are already authenticated and that the project is already set to your PROJECT_ID :

gcloud auth list

Command output

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

Command output

[core]
project = <PROJECT_ID>

Cloud Shell also sets some environment variables by default which may be useful as you run future commands.

echo $GOOGLE_CLOUD_PROJECT

Command output

<PROJECT_ID>

If for some reason the project is not set, simply issue the following command :

gcloud config set project <PROJECT_ID>

Looking for your PROJECT_ID? Check out what ID you used in the setup steps or look it up in the console dashboard:

Project_ID.png

IMPORTANT: Finally, set the default zone and project configuration:

gcloud config set compute/zone us-central1-f

You can choose a variety of different zones. Learn more in the Regions & Zones documentation.

You will need a Slack team where you are allowed to create custom integrations. You can create a team for free if you do not already have one that you wish to use for this tutorial.

In Cloud Shell on the command-line, run the following command to clone the GitHub repository:

git clone https://github.com/googlecodelabs/cloud-slack-bot.git

Change directory into cloud-slack-bot/start.

cd cloud-slack-bot/start

Install the Node.js dependencies, including Botkit.

npm install

A bot user can listen to messages on Slack, post messages, and upload files. In this codelab, you will create a bot post a simple greeting message.

Create a new Slack app

  • Go to the Slack apps management page.
  • Click the Create new app button in the upper-right corner.
  • Give the app a name, such as "Kittenbot".
  • Choose the Slack team where you want it installed.

Add a new bot user to the app.

  • Select Bot users under the features heading on the left-hand side navigation of the app configuration page.
  • Click the Add a bot user button.
  • Give it a nice username, like @kittenbot.
  • This tutorial uses the Realtime Messaging (RTM) API, so keep the Always show the bot as online option selected as Off. The bot user will show as online only when there is a connection from the bot.
  • Click the Add bot user button.

Get the bot user OAuth access token

  • Select OAuth & Permissions under the features heading on the left-hand side navigation of the app configuration page.
  • Click the Reinstall app button. This will reinstall the app to your team and add the bot user you just created.
  • Click the Copy button to copy the Bot user OAuth access token text into your clipboard.
  • You'll use the token in the next step. Don't worry. You can come back this configuration page from the apps management page if you need to get this token again.

Edit the kittenbot.js file and enter your Slack bot token. If it is no longer in your clipboard, you can get it from the bot custom integration configuration page. You can use any editor of your choice, such as emacs or vim. This tutorial uses the code editor feature of Cloud Shell for simplicity.

  • Open the code editor.
  • Select the cloud-slack-bot/start/kittenbot.js file.
  • Paste your Slack token to replace your-slack-token in the file and save the file with Ctrl-S or the File -> Save menu item.

kittenbot.js

var Botkit = require('botkit')

var controller = Botkit.slackbot({debug: false})
controller
  .spawn({
    token: 'your-slack-token' // Edit this line!
  })
  .startRTM(function (err) {
    if (err) {
      throw new Error(err)
    }
  })

controller.hears(
  ['hello', 'hi'], ['direct_message', 'direct_mention', 'mention'],
  function (bot, message) { bot.reply(message, 'Meow. :smile_cat:') })
  • Switch back to Cloud Shell and run your bot.
node kittenbot.js

In your Slack team, you should now see that @kittenbot is online.

  • If you do not see kittenbot in your direct messages list, you may need to open a direct message to the bot.

  • Say hello to @kittenbot in a direct message (or if you added @kittenbot to a channel, the bot will respond to mentions there, too). It should meow back at you.

  • Go back to the Cloud Shell and press Control-C to stop the bot server.

Hard-coding the Slack token in the source code makes it likely to accidentally expose your token by publishing it to version control or embedding it in a docker image. Instead, use Kubernetes Secrets to store tokens.

Write your token to a file called slack-token. This filename is in the .gitignore to prevent accidentally checking it into version control.

  • Create a new file in the web editor by right-clicking the start directory name. (To right-click on a Chromebook: press the touchpad with two fingers, or press Alt while clicking with one finger.)
  • Enter the name slack-token (no file extension).
  • Copy your token from kittenbot.js or the bot configuration page, paste it into the slack-token file and save.
  • Edit the kittenbot.js file to load the Slack token specified by the slack_token_path environment variable.

kittenbot.js

var Botkit = require('botkit')
var fs = require('fs') // NEW: Add this require (for loading from files).

var controller = Botkit.slackbot({debug: false})

// START: Load Slack token from file.
if (!process.env.slack_token_path) {
  console.log('Error: Specify slack_token_path in environment')
  process.exit(1)
}

fs.readFile(process.env.slack_token_path, function (err, data) {
  if (err) {
    console.log('Error: Specify token in slack_token_path file')
    process.exit(1)
  }
  data = String(data)
  data = data.replace(/\s/g, '')
  controller
    .spawn({token: data})
    .startRTM(function (err) {
      if (err) {
        throw new Error(err)
      }
    })
})
// END: Load Slack token from file.

controller.hears(
  ['hello', 'hi'], ['direct_message', 'direct_mention', 'mention'],
  function (bot, message) { bot.reply(message, 'Meow. :smile_cat:') })

Go back to the Cloud Console and run your bot.

slack_token_path=./slack-token node kittenbot.js

You should see the bot online again in Slack and be able to chat with it. After testing it out, press Ctrl-C to shut down the bot.

Docker provides a way to containerize your bot. A Docker image bundles all of your dependencies (even the compiled ones) so that it can run in a lightweight sandbox.

Building a Docker image

  • Create a file called Dockerfile.
  • Enter the following definition, which describes how to build your Docker image.

Dockerfile

FROM node:5.4
COPY package.json /src/package.json
WORKDIR /src
RUN npm install
COPY kittenbot.js /src
CMD ["node", "/src/kittenbot.js"]
  • Save this file.

A Dockerfile is a recipe for a Docker image. This one layers on top of the Node.js base image found on the Docker hub, copies package.json to the image and installs the dependencies listed in it, copies the kittenbot.js file to the image, and tells Docker to that it should run the Node.js server when the image starts.

  • Go back to the Cloud Shell.
  • Run this command to save your project ID to the environment variable PROJECT_ID. Commands in this tutorial will use this variable as $PROJECT_ID.
export PROJECT_ID=$(gcloud config list --format 'value(core.project)')
  • Build the image by running the docker build command. (This command takes about 4 minutes to complete. It has to download the base image and Node.js dependencies.)
docker build -t gcr.io/${PROJECT_ID}/slack-codelab:v1 .

Extra Credit: Create a incoming webhook to Slack

While the Docker image is building, you can try out some of the other Slack APIs. For example, an incoming webhook is an easy way to send Slack notifications from another service or app without having to worry about a persistent connection for two-way communication like we do with a bot user.

  • Go to the Slack Custom Integrations management console.
  • Click Incoming WebHooks, then the Add configuration button.
  • Select a Slack channel for messages to post to (or have them go privately to your Slack user).
  • Click the Add incoming webhooks integration button.

By now your Docker build should be complete. We will come back to this integration in a later step.

Testing a Docker image locally

  • Go back to Cloud Shell.
  • Test the image locally with the following command which will run a Docker container as a daemon from our newly-created container image:
docker run -d \
    -v $(pwd)/:/config \
    -e slack_token_path=/config/slack-token \
    gcr.io/${PROJECT_ID}/slack-codelab:v1

This command also mounts the current directory as a volume inside the container to give it access to the slack-token file. You should see that @kittenbot is online again.

Let's now stop the running container.

  • Get the ID of the running container with the docker ps command:
docker ps

Command output

CONTAINER ID   IMAGE                               COMMAND
fab8b7a0d6ee   gcr.io/your-proj/slack-codelab:v1   "node /src/kittenbot."
  • Stop the container. Replace the docker container ID (fab8b7a0d6ee in the example) with the ID of your container:
docker stop fab8b7a0d6ee

Frequently Asked Questions

Now that the image works as intended we can push it to the Google Container Registry, a private repository for your Docker images accessible from every Google Cloud project (but also from outside Google Cloud Platform).

  • Push the Docker image to Google Container Registry from Cloud Shell. (This command takes about 5 minutes to complete.)
gcloud docker -- push gcr.io/${PROJECT_ID}/slack-codelab:v1

Extra Credit: Testing an incoming webhook

While waiting for the image to upload, use the incoming webhook to send a notification to Slack.

  • In Cloud Shell, click the + button to add a new Cloud Shell session.
  • Go back to the incoming webhook you created. If you closed that browser tab, you can get back to there from the Slack Custom Integrations management console.
  • Copy the webhook URL from the configuration page.
  • In the new Cloud Shell session, run curl to send an HTTP request with your message to Slack. (Replace the URL with your webhook URL.)
curl -X POST --data-urlencode \
    'payload={"text": "Hello from Cloud Shell."}' \
    https://hooks.slack.com/services/YOUR/WEBHOOK/URL

This demonstrates that anywhere that you can send an HTTP request, you can send a message to Slack. This is a really easy way to integrate your own apps and services with Slack notifications.

For more complicated messages, test out the JSON request first in the Slack message builder.

Viewing images in Google Container Registry

When the image upload completes, you can see the container image listed in the Google Cloud Console: Container Engine > Container Registry. We now have a project-wide Docker image available which Kubernetes can access and orchestrate as we'll see in a few minutes.

If you're curious, you can navigate through the container images as they are stored in Google Cloud Storage by following this link: https://console.cloud.google.com/storage/browser/.

Frequently Asked Questions

Now that the Docker image is in Google Container Registry, you can run the gcloud docker -- pull command to save this image on any machine and run it with the Docker command-line tool.

If you want to make sure your bot keeps running after it is started, you'll have to run another service to monitor your Docker container and restarts it if it stops. This gets even harder if you want to make sure the bot keeps running even if the machine it is running on fails.

Kubernetes solves these problems. You tell it that you want there to always be a replica of your bot running, and the Kubernetes master will keep that target state. It starts the bot up when there aren't enough running, and shuts bot replicas down when there are too many.

A Container Engine cluster is a managed Kubernetes cluster. It consists of a Kubernetes master API server hosted by Google and a set of worker nodes. The worker nodes are Compute Engine virtual machines.

  • Create a cluster with two n1-standard-1 nodes (this will take a few minutes to complete):
gcloud container clusters create my-cluster \
      --num-nodes=2 \
      --zone=us-central1-f \
      --machine-type n1-standard-1

Command output

Creating cluster my-cluster...done.
Created [https://container.googleapis.com/v1/projects/PROJECT_ID/zones/us-central1-f/clusters/my-cluster].
kubeconfig entry generated for my-cluster.
NAME        ZONE           MACHINE_TYPE   NUM_NODES  STATUS
my-cluster  us-central1-f  n1-standard-1  2          RUNNING

This command creates the cluster and authenticates the Kubernetes command-line tool, kubectl, with the new cluster's credentials.

You should now have a fully-functioning Kubernetes cluster powered by Google Container Engine. View it in the Cloud Console at Container Engine > Container Clusters.

Each node in the cluster is a Compute Engine instance provisioned with Kubernetes and Docker binaries. If you are curious, you can list all Compute Engine instances in the project:

gcloud compute instances list

Command output

NAME           ZONE          MACHINE_TYPE  INTERNAL_IP EXTERNAL_IP     STATUS
gke-my-cl...16 us-central1-f n1-standard-1 10.240.0.2  146.148.100.240 RUNNING
gke-my-cl...34 us-central1-f n1-standard-1 10.240.0.3  104.154.36.108  RUNNING

You won't use anything Compute Engine-specific in the rest of the tutorial. Instead, you'll use kubectl, the Kubernetes command-line tool, to configure and run your bot.

First, create a Secret in Kubernetes to store the Slack token and make it available to the container.

kubectl create secret generic slack-token --from-file=./slack-token

Command output

secret "slack-token" created

It's now time to deploy your own containerized application to the Kubernetes cluster. You need to configure a Deployment, which describes how to configure the container and provide a replication controller to keep the bot running.

  • In the Cloud Shell Code Editor, create a file called slack-codelab-deployment.yaml.
  • Enter the following deployment definition.

slack-codelab-deployment.yaml

apiVersion: extensions/v1beta1 
kind: Deployment 
metadata:
  name: slack-codelab
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: slack-codelab
    spec:
      containers:
      - name: master
        image: gcr.io/PROJECT_ID/slack-codelab:v1  # Replace PROJECT_ID
                                                   # with your project ID.
        volumeMounts:
        - name: slack-token
          mountPath: /etc/slack-token
        env:
        - name: slack_token_path
          value: /etc/slack-token/slack-token
      volumes:
      - name: slack-token
        secret:
          secretName: slack-token
  • Save the file.

Now, you can create the Deployment by running kubectl create in the Cloud Console.

kubectl create -f slack-codelab-deployment.yaml --record

Command output

deployment "slack-codelab" created

Since you used the --record option, you can view the commands applied to this deployment as the "change-cause" in the rollout history.

kubectl rollout history deployment/slack-codelab

Command output

deployments "slack-codelab":
REVISION        CHANGE-CAUSE
1               kubectl create -f slack-codelab-deployment.yaml --record

See what the kubectl create command made. Re-run this command until the status shows Running. This should take about 30 seconds to 1 minute.

kubectl get pods

Command output

NAME                             READY     STATUS    RESTARTS   AGE
slack-codelab-2890463383-1ss4a   1/1       Running   0          3m
  • Rerun kubectl get pods until the status for the pod shows Running.
  • Go to Slack, and see that @kittenbot is back online.
  • Say hello to @kittenbot one last time.

Frequently Asked Questions

Congratulations, you now have a Slack bot running on Google Container Engine. Time for some cleaning of the resources used (to save on cost and to be a good cloud citizen).

  • Delete the Deployment (which also deletes the running pods).
kubectl delete deployment slack-codelab

Command output

deployment "slack-codelab" deleted
  • Delete your cluster.
gcloud container clusters delete my-cluster

Command output

The following clusters will be deleted.
 - [my-cluster] in [us-central1-f]
Do you want to continue (Y/n)?  y
Deleting cluster my-cluster...done.
Deleted [https://container.googleapis.com/v1/proj...l1-f/clusters/my-cluster].

This deletes all the Google Compute Engine instances that are running the cluster.

Finally delete the Docker registry storage bucket hosting your image(s).

  • First, list the Google Cloud Storage buckets to get the bucket path.
gsutil ls

Command output

gs://artifacts.<PROJECT_ID>.appspot.com/
  • Now, delete the bucket and all the images it contains.
gsutil rm -r gs://artifacts.${PROJECT_ID}.appspot.com/

Command output

Removing gs://artifacts.PROJECT_ID.appspot.com/...

Of course, you can also delete the entire project but you would lose any billing setup you have done (disabling project billing first is required). Additionally, deleting a project will only happen after the current billing cycle ends.

You now know how to run a Slack bot on Google Container Engine!

We've only scratched the surface of this technology and we encourage you to explore further with your own Kubernetes deployments. When developing a bot to become a Slack app, the bot will likely have multiple replicas. With a replicated bot, start to check out liveness probes (health checks) and consider using the Kubernetes API directly.

What we've covered

  • Building a Node.js Slack bot Docker image.
  • Running a Docker image on Google Container Engine.

Next Steps

  • Develop a bot of your own.
  • Try API.AI for Slack as a way to build conversational bots without code.
  • Learn more about Kubernetes by trying the more advanced "Compute Engine & Kubernetes" codelab.
  • Check out Beep Boop: a third-party offering an even easier way to host Slack bots, powered by Google Cloud Platform.

Learn More

This extra credit section should take you about 10 minutes to complete.

We'd like for the bot to do more than just say "meow". But how do you deploy a new version of something that is running on Kubernetes?

First, let's modify the application. Botkit offers the ability to handle conversations. With these, the bot can request more information and react to messages beyond a one word reply.

  • Edit the kittenbot.js file in the web editor. Add this new kitten emoji delivery code to the bottom of the file.

kittenbot.js

// ...

// START: listen for cat emoji delivery
var maxCats = 20
var catEmojis = [
  ':smile_cat:',
  ':smiley_cat:',
  ':joy_cat:',
  ':heart_eyes_cat:',
  ':smirk_cat:',
  ':kissing_cat:',
  ':scream_cat:',
  ':crying_cat_face:',
  ':pouting_cat:',
  ':cat:',
  ':cat2:',
  ':leopard:',
  ':lion_face:',
  ':tiger:',
  ':tiger2:'
]

controller.hears(
  ['cat', 'cats', 'kitten', 'kittens'],
  ['ambient', 'direct_message', 'direct_mention', 'mention'],
  function (bot, message) {
    bot.startConversation(message, function (err, convo) {
      if (err) {
        console.log(err)
        return
      }
      convo.ask('Does someone need a kitten delivery? Say YES or NO.', [
        {
          pattern: bot.utterances.yes,
          callback: function (response, convo) {
            convo.say('Great!')
            convo.ask('How many?', [
              {
                pattern: '[0-9]+',
                callback: function (response, convo) {
                  var numCats =
                  parseInt(response.text.replace(/[^0-9]/g, ''), 10)
                  if (numCats === 0) {
                    convo.say({
                      'text': 'Sorry to hear you want zero kittens. ' +
                        'Here is a dog, instead. :dog:',
                      'attachments': [
                        {
                          'fallback': 'Chihuahua Bubbles - https://youtu.be/s84dBopsIe4',
                          'text': '<https://youtu.be/s84dBopsIe4|' +
                            'Chihuahua Bubbles>!'
                        }
                      ]
                    })
                  } else if (numCats > maxCats) {
                    convo.say('Sorry, ' + numCats + ' is too many cats.')
                  } else {
                    var catMessage = ''
                    for (var i = 0; i < numCats; i++) {
                      catMessage = catMessage +
                      catEmojis[Math.floor(Math.random() * catEmojis.length)]
                    }
                    convo.say(catMessage)
                  }
                  convo.next()
                }
              },
              {
                default: true,
                callback: function (response, convo) {
                  convo.say(
                    "Sorry, I didn't understand that. Enter a number, please.")
                  convo.repeat()
                  convo.next()
                }
              }
            ])
            convo.next()
          }
        },
        {
          pattern: bot.utterances.no,
          callback: function (response, convo) {
            convo.say('Perhaps later.')
            convo.next()
          }
        },
        {
          default: true,
          callback: function (response, convo) {
            // Repeat the question.
            convo.repeat()
            convo.next()
          }
        }
      ])
    })
  })
  // END: listen for cat emoji delivery
  • Save the file.

Optionally, you can test this out in Cloud Shell using the same command as before, but note that you'll see two responses since the bot is still running in your Kubernetes cluster.

  • Next, build a new container image with an incremented tag (v2 in this case):
docker build -t gcr.io/${PROJECT_ID}/slack-codelab:v2 .
  • Push the image to Google Container Registry.
gcloud docker -- push gcr.io/${PROJECT_ID}/slack-codelab:v2

As you did with the first version of the kitten bot, you can test locally using the node command and the docker command. Here we'll skip those steps and push the new version to the cluster.

We're now ready for kubernetes to update our deployment to the new version of the application.

  • Edit the line in the slack-codelab-deployment.yaml file defining which image to use.

slack-codelab-deployment.yaml

apiVersion: extensions/v1beta1 
kind: Deployment 
metadata:
  name: slack-codelab
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: slack-codelab
    spec:
      containers:
      - name: master
        image: gcr.io/PROJECT_ID/slack-codelab:v2  # Update this to v2.
                                                   # Replace PROJECT_ID
                                                   # with your project ID.
        volumeMounts:
        - name: slack-token
          mountPath: /etc/slack-token
        env:
        - name: slack_token_path
          value: /etc/slack-token/slack-token
      volumes:
      - name: slack-token
        secret:
          secretName: slack-token
  • Save the file.

Now, you can apply this change to the running Deployment.

  • Run this command to update the deployment to use the v2 image.
kubectl apply -f slack-codelab-deployment.yaml

Command output

deployment "slack-codelab" configured

You should see that Kubernetes has shut down the pod running the previous version and started a new pod that is running the new image.

kubectl get pods

Command output

NAME                             READY     STATUS        RESTARTS   AGE
slack-codelab-2890463383-mqy5l   1/1       Terminating   0          17m
slack-codelab-3059677337-b41r0   1/1       Running       0          7s

We can see also that we changed the deployment:

kubectl rollout history deployment/slack-codelab

Command output

deployments "slack-codelab":
REVISION        CHANGE-CAUSE
1               kubectl create -f slack-codelab-deployment.yaml --record
2               kubectl apply -f slack-codelab-deployment.yaml

Go back to Slack and type a message to kittenbot that mentions "kitten" and see it helpfully join the conversation.

Congratulations! You just updated a Slack bot running on Kubernetes to a new version.

You have just written a Slack bot, tested locally, deployed it, made some changes, and deployed an update with minimal downtime.

Time for some cleaning of the resources used (to save on cost and to be a good cloud citizen).

  • Delete the Deployment (which also deletes the running pods).
kubectl delete deployment slack-codelab

Command output

deployment "slack-codelab" deleted
  • Delete your cluster.
gcloud container clusters delete my-cluster

Command output

The following clusters will be deleted.
 - [my-cluster] in [us-central1-f]
Do you want to continue (Y/n)?  y
Deleting cluster my-cluster...done.
Deleted [https://container.googleapis.com/v1/proj...l1-f/clusters/my-cluster].

This deletes all the Google Compute Engine instances that are running the cluster.

Finally delete the Docker registry storage bucket hosting your image(s).

  • First, list the Google Cloud Storage buckets to get the bucket path.
gsutil ls

Command output

gs://artifacts.<PROJECT_ID>.appspot.com/
  • Now, delete the bucket and all the images it contains.
gsutil rm -r gs://artifacts.${PROJECT_ID}.appspot.com/

Command output

Removing gs://artifacts.PROJECT_ID.appspot.com/...

Of course, you can also delete the entire project but you would lose any billing setup you have done (disabling project billing first is required). Additionally, deleting a project will only happen after the current billing cycle ends.