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.
If you see a "request account button" at the top of the main Codelabs window, click it to obtain a temporary account. Otherwise ask one of the staff for a coupon with username/password.
These temporary accounts have existing projects that are set up with billing so that there are no costs associated for you with running this codelab.
Note that all these accounts will be disabled soon after the codelab is over.
Use these credentials to log into the machine or to open a new Google Cloud Console window https://console.cloud.google.com/. Accept the new account Terms of Service and any updates to Terms of Service.
Here's what you should see once logged in:
When presented with this console landing page, please select the only project available. Alternatively, from the console home page, click on "Select a Project" :
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):
Click the "Start Cloud Shell" button:
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
Credentialed accounts: - <myaccount>@<mydomain>.com (active)
gcloud config list project
[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
<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 workspace where you are allowed to create custom integrations. You can create a workspace for free if you do not already have one that you wish to use for this tutorial.
A bot user can listen to messages on Slack, post messages, and upload files. In this codelab, you will create a bot to post a simple greeting message.
chat:write
to "Send messages as Kittenbot"export BOT_TOKEN={YOUR_BOT_TOKEN}
export SIGNING_SECRET={YOUR_SECRET}
Don't worry. You can come back to this configuration page from the apps management page if you need to get these tokens again.
We want to ensure that your Bot Token and Client Signing Secret are stored securely. Hard-coding the Slack token in source code makes it likely to accidentally expose your token by publishing it to version control or embedding it in a docker image.
The Secret Manager provides a secure and convenient method for storing API keys, passwords, certificates, and other sensitive data. Secret Manager provides a central place and single source of truth to manage, access, and audit secrets across Google Cloud.
To use the Secret Manager, we need to enable the API, update permissions, and create the secrets.
Enable the API
gcloud services enable secretmanager.googleapis.com
Set Permissions
You'll need to update your project permissions so that you can create and access secrets in the Secret Manager. Here we're using the output of gcloud auth list, which returns the current active account. This should be the currently logged in user account.
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT --member=user:$(gcloud auth list --format 'value(account)') --role=roles/secretmanager.admin
Create your Secrets
Now that your account has permission to create and access the values of your secrets, you'll save your Bot Token and Signing Secret with the following commands.
echo -n $BOT_TOKEN | gcloud secrets create bot-token --replication-policy=automatic --data-file=-
echo -n $SIGNING_SECRET | gcloud secrets create client-signing-secret --replication-policy=automatic --data-file=-
Access your Secrets
Let's confirm that your secrets were created properly and your permissions are working. Access your secrets with the following commands:
gcloud secrets versions access 1 --secret="bot-token" gcloud secrets versions access 1 --secret="client-signing-secret"
You can also view and manage your secrets in the Cloud Console Secret Manager page.
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
Open up the kittenbot.js file with an editor. 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.
cloud-slack-bot/start/kittenbot.js
file.The kittenbot code has two main functions. One is to retrieve the secrets, and the other is to run the bot.
First we import our dependencies:
const {Botkit} = require('botkit');
const {SlackAdapter, SlackEventMiddleware} = require(
'botbuilder-adapter-slack');
const {SecretManagerServiceClient} = require('@google-cloud/secret-manager');
The SlackAdapter and SlackEventMiddleware are packages that extend Botkit and allow the bot to easily translate messages to and from the Slack API. The Secret Manager client will allow you to access the secrets that you saved in an earlier step.
Next we have our function for retrieving the secrets:
/**
* Returns the secret string from Google Cloud Secret Manager
* @param {string} name The name of the secret.
* @return {payload} The string value of the secret.
*/
async function accessSecretVersion(name) {
const client = new SecretManagerServiceClient();
const projectId = process.env.PROJECT_ID;
const [version] = await client.accessSecretVersion({
name: `projects/${projectId}/secrets/${name}/versions/1`,
});
// Extract the payload as a string.
const payload = version.payload.data.toString('utf8');
return payload;
}
This function returns the string values of the secrets that are required to authenticate the bot.
The next function initializes the bot:
/**
* Asynchronous function to initialize kittenbot.
*/
async function kittenbotInit() {
const adapter = new SlackAdapter({
clientSigningSecret: await accessSecretVersion('client-signing-secret'),
botToken: await accessSecretVersion('bot-token'),
});
adapter.use(new SlackEventMiddleware());
const controller = new Botkit({
webhook_uri: '/api/messages',
adapter: adapter,
});
controller.ready(() => {
controller.hears(['hello', 'hi'], ['message', 'direct_message'],
async (bot, message) => {
await bot.reply(message, 'Meow. :smile_cat:');
});
});
}
The first part of the function configures the SlackAdapter with the secrets, and then specifies an endpoint for receiving messages. Then, once the controller is on, the bot will reply to any message containing "hello" or "hi" with "Meow. 😺"
Unfortunately, you cannot run the bot locally and have it interact with Slack. The Slack Events API uses webhooks, which means it requires an HTTP endpoint to send events to. To integrate with Slack, you'll need to host this app on a public URL.
To host our application and create a public URL, we'll containerize our bot with a Dockerfile, and then host it on Cloud Run. A Docker image bundles all of your dependencies (even the compiled ones) so that it can run in a lightweight sandbox.
FROM node:10-slim
# Install app dependencies.
COPY package.json /src/package.json
WORKDIR /src
RUN npm install
# Bundle app source.
COPY kittenbot.js /src
CMD ["node", "kittenbot"]
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.
PROJECT_ID
. Commands in this tutorial will use this variable as $PROJECT_ID
.export PROJECT_ID=$(gcloud config list --format 'value(core.project)')
gcloud services enable cloudbuild.googleapis.com
gcloud builds submit --tag gcr.io/${PROJECT_ID}/slack-codelab .
This command takes about 2 minutes to complete. It has to download the base image and Node.js dependencies, and push it to the registry.
When the image upload completes, you can see the container image listed in the Google Cloud Console: Container Registry.
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 Slack Events API uses webhooks to send outgoing messages about events. When you configure the Slack App, you'll have to provide a publicly accessible URL for the Slack API to ping.
Cloud Run is a good solution for hosting webhook targets. It allows you to use any language or runtime that you like and it provides concurrency, meaning your application will be able to handle much higher volume.
gcloud services enable run.googleapis.com
Cloud Run offers both a fully managed option and an Anthos option, which supports both Google Cloud and on-premise environments. For this tutorial, we'll use Cloud Run fully managed.
gcloud config set run/platform managed
Next, set the compute zone:
gcloud config set run/region us-central1
In order to call the accessSecretVersion
function, the Cloud Run service account will need to have the role: roles/secretmanager.secretAccessor.
First, save the default service account into an environment variable:
export SERVICE_ACCOUNT=$(gcloud iam service-accounts list --format 'value(EMAIL)' --filter 'NAME:Default compute service account')
Confirm you have the email address saved:
echo $SERVICE_ACCOUNT
You can also find your Default compute service account in the Cloud console.
Once you have the email address, enable the Secret Manager role for the service account:
gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$SERVICE_ACCOUNT --role=roles/secretmanager.secretAccessor
A Cloud Run service exposes a unique endpoint and automatically scales the underlying infrastructure to handle incoming requests. To create a service and deploy the kittenbot image, use the following command.
gcloud run deploy kittenbot --image gcr.io/${PROJECT_ID}/slack-codelab --set-env-vars PROJECT_ID=${PROJECT_ID}
This command has three distinct parts:
When prompted, specify the region us-central1.
Allow unauthenticated invocations to kittenbot. Remember, the URL has to be public!
By now, your app will have finished deploying and the terminal should have returned something like:
Service [kittenbot] revision [kittenbot-.......] has been deployed and is serving 100 percent of traffic at https://kittenbot-.......a.run.app
This URL will be the base of the URL you use to enable the Slack Events API. Copy it into your clipboard to use in the next step.
Your service is now live and publicly available! Go to the Cloud Run console to see more information about it.
You can see when the last revision was created, how much traffic it's receiving, and look at the logs. If we click into the logs, we can see that the Botkit controller is on and ready to receive messages.
Now let's start sending messages from our Slack channel!
As we saw earlier, our kittenbot code specifies a relative endpoint for our webhook target.
const controller = new Botkit({
webhook_uri: '/api/messages',
adapter: adapter,
});
This means, our full URL will be the base part from the Cloud Run service, plus "/api/messages."
To retrieve the base part of the URL:
gcloud run services describe kittenbot
In the apps management page, go to the Events Subscriptions section on the sidebar, and toggle Enable Events on. Input your URL like this:
{YOUR_URL}/api/messages
Depending on how fast you type the URL in, it might try to verify before you're finished. If it fails, click "Retry."
Subscribe to all the message bot events.
Click Save Changes at the bottom of the page. You will be prompted to Reinstall Your App. Go through the prompts and click Allow.
At this point, your bot is fully integrated! Messages in the workspace will trigger Slack to send messages to your Cloud Run service, which will in turn respond with a simple greeting.
Now everyone in your channel can interact with Kittenbot!
Each message in Slack triggers an event and sends an HTTP POST message to our Cloud Run service. If you take a look at the Cloud Run service logs, you'll see that each message corresponds to a POST entry in the log.
The kittenbot responds to each message with "Meow. 😺".
This extra credit section should take you about 10 minutes to complete. Feel free to skip directly to Cleanup.
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 Cloud Run?
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. Open the kittenbot.js
file in the web editor to make the following changes.
// ...
//
const maxCats = 20
const 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:'
]
function makeCatMessage(numCats){
let catMessage = "";
for (let i = 0; i < numCats; i++) {
// Append a random cat from the list
catMessage += catEmojis[Math.floor(Math.random() * catEmojis.length)];
}
return catMessage;
}
// Start kitten-delivery convo
function createKittenDialog(controller) {
let convo = new BotkitConversation('kitten-delivery', controller);
convo.ask('Does someone need a kitten delivery?', [
{
pattern: 'yes',
handler: async(response, convo, bot) => {
await convo.gotoThread('yes_kittens');
}
},
{
pattern: 'no',
handler: async(response, convo, bot) => {
await convo.gotoThread('no_kittens');
}
},
{
default: true,
handler: async(response, convo, bot) => {
await convo.gotoThread('default');
}
},
]);
convo.addQuestion('How many would you like?', [
{
pattern: '^[0-9]+?',
handler: async(response, convo, bot, message) => {
let numCats = parseInt(response);
if(numCats > maxCats){
await convo.gotoThread('too_many');
}
else{
convo.setVar('full_cat_message', makeCatMessage(numCats));
await convo.gotoThread('cat_message');
}
}
},
{ default: true,
handler: async(response, convo, bot, message) => {
if (response){
await convo.gotoThread('ask_again');
}
// The response '0' is interpreted as null
else { await convo.gotoThread('zero_kittens');
}
}
}
],'num_kittens', 'yes_kittens');
// If numCats is too large, jump to start of the yes_kittens thread
convo.addMessage(
'Sorry, {{vars.num_kittens}} is too many cats. Pick a smaller number.',
'too_many');
convo.addAction('yes_kittens', 'too_many');
// If response is not a number, jump to start of the yes_kittens thread
convo.addMessage('Sorry I didn\'t understand that', 'ask_again');
convo.addAction('yes_kittens', 'ask_again');
// If numCats is 0, send a dog instead
convo.addMessage(
{'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>!'
}
]}, 'zero_kittens');
// Send cat message
convo.addMessage('{{vars.full_cat_message}}', 'cat_message')
convo.addMessage('Perhaps later.', 'no_kittens');
return (convo);
}
// END: kitten-delivery convo
This new conversation directs the thread based on the responses. For example, if the user responds "no" to the kitten question, it jumps to the message labeled "no_kittens", which is the end of that conversational thread.
Now that the conversation is defined, you need to add it to your controller. In the kittenbotInit function, add the new dialog to the controller, after the controller is configured and before it is ready.
async function kittenbotInit() {
...
const controller = new Botkit({
webhook_uri: '/api/messages',
adapter: adapter,
});
// Add Kitten Dialog
convo = createKittenDialog(controller);
controller.addDialog(convo);
// Controller is ready
controller.ready(() => {
...
}
}
Now that dialog is available for the controller to use, we'll add a trigger for it to start. When the chatbot hears "kitten", "kittens", "cat", or "cats", it will start the conversation.
// ...
// Controller is ready
controller.ready(() => {
controller.hears(['hello', 'hi'], ['message', 'direct_message'],
async (bot, message) => {
return await bot.reply(message, 'Meow. :smile_cat:');
});
// START: listen for cat emoji delivery
controller.hears(['cat','cats','kitten','kittens'],
['message', 'direct_message'],
async (bot, message) => {
// Don't respond to self
if (message.bot_id != message.user){
await bot.startConversationInChannel(message.channel, message.user);
return await bot.beginDialog('kitten-delivery');
}
});
// END: listen for cat emoji delivery
});
Now that the controller is updated, you'll need to re-build and re-deploy the application.
gcloud builds submit --tag gcr.io/${PROJECT_ID}/slack-codelab .
gcloud run deploy kittenbot --image gcr.io/${PROJECT_ID}/slack-codelab
Congratulations! You just updated a Slack bot running on Cloud Run to a new version.
What if you don't want to have a conversation with the user? What if you'd prefer to simply trigger an action with one simple command?
Slack offers this functionality via Slash Commands, which allow users to invoke your application by entering the command into the message box.
'/api/messages'
In kittenbot.js, inside the controller.ready function, add a handler for slash commands.
// ...
// Controller is ready
controller.ready(() => {
...
// START: slash commands
controller.on('slash_command', async(bot, message) => {
let numCats = parseInt(message.text);
response = makeCatMessage(numCats);
bot.httpBody({text: response});
});
// END: slash commands
});
Now that the controller is updated, you'll need to re-build and re-deploy the application.
gcloud builds submit --tag gcr.io/${PROJECT_ID}/slack-codelab .
gcloud run deploy kittenbot --image gcr.io/${PROJECT_ID}/slack-codelab
Enter /cats plus a number to send the slash command. Eg: /cats 8
The bot will respond with 8 cats, only visible to you:
Congratulations, you now have a Slack bot running on Google Cloud Run. Time for some cleaning of the resources used (to save on cost and to be a good cloud citizen).
If you prefer, you can delete the entire project. In the GCP Console, go to the Cloud Resource Manager page:
In the project list, select the project we've been working in and click Delete. You'll be prompted to type in the project ID. Enter it and click Shut Down.
Alternatively, you can delete the entire project directly from Cloud Shell with gcloud:
gcloud projects delete [PROJECT_ID]
If you prefer to delete the different components one by one, proceed to the next section.
gcloud run services delete kittenbot
Service [kittenbot] will be deleted. Do you want to continue (Y/n)? y Deleted service [kittenbot].
gcloud beta secrets delete bot-token
You are about to destroy the secret [bot-token] and its [1] version(s). This action cannot be reversed. Do you want to continue (Y/n)? y Deleted secret [bot-token].
gcloud beta secrets delete client-signing-secret
You are about to destroy the secret [client-signing-secret] and its [1] version(s). This action cannot be reversed. Do you want to continue (Y/n)? y Deleted secret [client-signing-secret].
First, list the Google Cloud Storage buckets to get the bucket path.
gsutil ls
gs://artifacts.<PROJECT_ID>.appspot.com/ gs://<PROJECT_ID>_cloudbuild/
gsutil rm -r gs://artifacts.${PROJECT_ID}.appspot.com/
Removing gs://artifacts.<PROJECT_ID>.appspot.com/...
gsutil rm -r gs://${PROJECT_ID}_cloudbuild/
Removing gs://<PROJECT_ID>_cloudbuild/...
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 Cloud Run!
We've only scratched the surface of this technology and we encourage you to explore further with your own Cloud Run deployments.