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

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:

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 Developers 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). Google Container Engine pricing is documented here.

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):

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>

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:

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

Add a new bot user to the app.

Get the bot user OAuth access token

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.

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:') })
node kittenbot.js

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

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.

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

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"]

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.

export PROJECT_ID=$(gcloud config list --format 'value(core.project)')
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.

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

Testing a Docker image locally

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.

docker ps

Command output

CONTAINER ID   IMAGE                               COMMAND
fab8b7a0d6ee   gcr.io/your-proj/slack-codelab:v1   "node /src/kittenbot."
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).

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.

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.

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.

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

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

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).

kubectl delete deployment slack-codelab

Command output

deployment "slack-codelab" deleted
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).

gsutil ls

Command output

gs://artifacts.<PROJECT_ID>.appspot.com/
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

Next Steps

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.

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

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.

docker build -t gcr.io/${PROJECT_ID}/slack-codelab:v2 .
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.

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

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

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).

kubectl delete deployment slack-codelab

Command output

deployment "slack-codelab" deleted
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).

gsutil ls

Command output

gs://artifacts.<PROJECT_ID>.appspot.com/
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.