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

Codelab-at-a-conference setup

The instructor will be sharing with you temporary accounts with existing projects that are already setup so you do not need to worry about enabling billing or any cost associated with running this codelab. Note that all these accounts will be disabled soon after the codelab is over.

Once you have received a temporary username / password to login from the instructor, log into Google Cloud Console: https://console.cloud.google.com/.

Here's what you should see once logged in :

Note the project ID you were assigned ( "codelab-test003" in the screenshot above). It will be referred to later in this codelab as PROJECT_ID.

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.

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.

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

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.

After clicking add integration,

The other important information on this page is the API Token.

You'll use it in the next step. You can come back to the bot configuration page from the custom integrations management page if you overwrite your clipboard and need to get this token again.

First, we need to install the dependencies, including Botkit.

npm install

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

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

This recipe for the Docker image layers on top of the node base image found on the Docker hub, installs the dependencies, copies the kittenbot.js file to the image, and starts the node server when the image is run.

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.

docker build -t gcr.io/${PROJECT_ID}/slack-codelab:v1 .

Once this completes (it'll take some time to download and extract everything) you can 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. First, 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."

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

gcloud docker -- push gcr.io/${PROJECT_ID}/slack-codelab:v1

If all goes well, you should be able to see the container image listed in the console: Compute > 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/ (the full resulting link should be of this form: https://console.cloud.google.com/project/PROJECT_ID/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 this. 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 \
      --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

You should now have a fully-functioning Kubernetes cluster powered by Google Container Engine:

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

But you really shouldn't have to use anything Compute Engine-specific and stick to the kubectl Kubernetes command line in the rest of the tutorial.

First, you need to create a Secret in Kubernetes to make the Slack token 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.

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.