SlackApi Bolt-python App With AWS Lambda And AWS CDK

SlackApi Bolt-python App With AWS Lambda And AWS CDK

ยท

5 min read

Abstract

  • The story: Since the retirement of rtm.start and Slack recommends to not use the custom integration any more, so we need to move to new slack apps
  • With new Slack App feature and The Bolt family of SDKs, we can implement Slack bot with AWS serverless using Slash command or at-mention.
  • This post will follow and extend the example from slackapi/bolt-python and manage infrasture as code it using AWS CDK typescript.

Table Of Contents


๐Ÿš€ Pre-requisite

  • You need slack workspace (free) to create slack app
  • Getting started with AWS CDK

๐Ÿš€ Create Slack App

  • First, create slack app, provide permission for the bot user through OAuth & Permissions and then install to workspace. You can follow the guide from slack page

  • Get Bot User OAuth Token from OAuth Tokens for Your Workspace under OAuth & Permissions

  • Get Signing secret from App Credentials under Basic Information.
  • Store Bot token and signing secret to .env file and ensure you already set ignore .env in .gitignore file. For better security we can let lambda function to get the credential from SSM parameter store or secret manager but it will slow down lambda operation a bit.

๐Ÿš€ Create lambda function with function URL

  • Why do we need lambda function URL here? let's have a look at following overview diagram

  • The lambda function URL is used as Request URL of Slash Command.

  • Not sure about the flow, but the lambda function requires permission to invoke itself, otherwise we will get following error
    2022-09-30 10:47:35,222 Failed to run a middleware middleware (error: An error occurred (AccessDeniedException) when calling the Invoke operation: User: arn:aws:sts::123456789012:assumed-role/sin-d1-slack-app-lambda-role/sin-d1-slack-app-lambda-handler is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:ap-southeast-1:123456789012:function:sin-d1-slack-app-lambda-handler because no identity-based policy allows the lambda:InvokeFunction action)
    
  • The CDK construct here is simple since we do not run any real process or use other AWS services. The stack includes IAM role attached to the lambda function with enabling function URL. Note that the default timeout of lambda function 3s is not enough to run process_request so we need to set timeout: Duration.seconds(10)

    import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha';
    import { App, Duration, Stack, StackProps } from 'aws-cdk-lib';
    import { Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
    import { FunctionUrlAuthType, Runtime } from 'aws-cdk-lib/aws-lambda';
    import { RetentionDays } from 'aws-cdk-lib/aws-logs';
    import { Construct } from 'constructs';
    import { join } from 'path';
    import { SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET } from './shared/configs';
    import { devEnv, EnvironmentConfig } from './shared/environment';
    
    export class SlackAppTest extends Stack {
      constructor(scope: Construct, id: string, reg: EnvironmentConfig, props: StackProps = {}) {
        super(scope, id, props);
    
        const prefix = `${reg.pattern}-${reg.stage}-slack-app`;
    
        const role = new Role(this, `${prefix}-lambda-role`, {
          roleName: `${prefix}-lambda-role`,
          assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
          managedPolicies: [
            {managedPolicyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'},
            {managedPolicyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaRole'}
          ]
        });
    
        const lambda = new PythonFunction(this, `${prefix}-lambda-handler`, {
          functionName: `${prefix}-lambda-handler`,
          runtime: Runtime.PYTHON_3_9,
          environment: {
            SLACK_BOT_TOKEN: SLACK_BOT_TOKEN,
            SLACK_SIGNING_SECRET: SLACK_SIGNING_SECRET
          },
          entry: join(__dirname, 'lambda-handler'),
          logRetention: RetentionDays.ONE_DAY,
          role: role,
          timeout: Duration.seconds(10)
        });
        lambda.addFunctionUrl({authType: FunctionUrlAuthType.NONE});
      }
    }
    
    const app = new App();
    
    new SlackAppTest(app, 'SlackAppLambda', devEnv, {
      description: 'Create Slack App with lambda function',
      env: devEnv
    });
    
    app.synth();
    
  • Now, deploy the stack with following command

    cdk deploy --method=direct
    SlackAppLambda: updating stack...
    
    โœ…  SlackAppLambda
    
    โœจ  Deployment time: 14s
    
    Outputs:
    SlackAppLambda.sind1slackappfunctionUrl = https://ehvkewqbgl7ukplwavdbpycyiy0nicsk.lambda-url.ap-southeast-1.on.aws/
    Stack ARN:
    arn:aws:cloudformation:ap-southeast-1:123456789012:stack/SlackAppLambda/7c775980-3ff4-11ed-bec3-024e925623dc
    
    โœจ  Total time: 18.09s
    

๐Ÿš€ Lambda handler

  • Lambda handler here index.py has two parts
    • Processes belong to slack bolt-python: The important one is verifying signing request to clarify the request is sent from Slack app, it is implemented as a middleware, enabled by default when you instantiate the app (see attribute request_verification_enabled).
    • Process event: Handle main operations

๐Ÿš€ Lazy listeners (FaaS)

  • In the lambda handler, you see the lazy function, what is it?
  • Lazy Listeners are a feature (at the time of this post, python support only) which make it easier to deploy Slack apps to FaaS (Function-as-a-Service) environments.
  • Calling ack() is responsible for returning an immediate HTTP response to Slack API servers within 3 seconds. By contrast, lazy functions are not supposed to return any response. They can do anything by leveraging all the listener args apart from ack() utility. Also, they are completely free from 3-second timeouts.
  • As the lazy is a list, you can set multiple lazy functions to a single listener. The lazy functions will be executed in parallel.

๐Ÿš€ Test the Slash command

  • Create slash command with Command name is the exact command we set in the lambda handler, get the output of lambda function URL after running cdk deploy and add to Request URL

  • Once installed to a Slack workspace, try typing /hello-bolt-python-lambda hello in any channel

๐Ÿš€ Test the app at-mention

  • We need to subscribe to the app_mention bot event (Your Apps > Event Subscriptions > Subscribe to bot events) and provide lambda function url at Request URL

  • With this event subscribed to, you can check if things are working by sending a message such as "hello @yourbotname" to a channel that your bot is a member of (sending this message will give you the option to add your bot to the channel too). If everything is working, you should get a response in channel from your bot! Hope this helps!

๐Ÿš€ Conclusion

  • With lambda function url, we don't need to host slackbot in an instance/server, all is serverless. And with AWS CDK, all is managed through code and deploy by cdk-pipeline
  • There're many feature from new slack app that you can find out more in the slack API page.

References: