API Gateway + Lambda で slack (1) ~ slack app を登録 ~

  • 11
    Like
  • 0
    Comment

API Gateway + Lambda で slack app

slack app

slack appとはbotやcommandsなどの集合体で、これを登録すれば、appに含まれているbotやcommandsが使えるようになるものです。

tokenが一意で済むのと、interactive message

https://api.slack.com/docs/message-buttons

を使うことができるようになります。これを使うことで、interactiveに操作を進めることが可能です。
slack appについて詳しくは下記を参照してください。

https://api.slack.com/slack-apps

作成したslack appは全体に公開することもできます。が、今回は完全にprivate目的なので自チームにだけ登録して利用します。

api gateway + Lambda

API Gateway + Lambdaとするのは、常に起動している必要がないからサーバがいらない構成にしたいためです。

さらにslash commandsを使う場合、botと違ってslash commandsを実行した時だけ呼び出されるため、料金的にも安心です。

必要なもの

  • apex

http://apex.run/

lambdaのデプロイに使います。invokeとlogsが便利です。
terraformとも連携できますが、terraformとapi gateway + lambdaの連携がつらいので今回は見送りました。

インストールはこれだけです。

curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh
  • aws credentials

aws configure でアクセスキーとシークレットアクセスキーを設定しておきます。
apexがデフォルトだとiamも配置するので、lambdaとiam関係の権限がなければなりません。
また、tokenなどの保持にkmsを利用するのでその権限も必要です。

複数credentialを持っている場合は、apexの設定からprofile指定もできます。詳しくは公式ページに。

  • slack

インストール

apexでlambdaの配置

まずは空のディレクトリで

apex init

をすることで、apexのプロジェクトファイル、apexが利用するIAMの作成が行われます。
project nameはここではcommandsとしておきます。

apexに慣れておきたい場合には公式のGetting startedにしたがって、helloを配置してみるといいでしょう。

oauth callbackの作成

slack appの登録にoauthのcallbackが必要です。手でやれないこともないですが、せっかくなのでこれもapi gateway + lambdaで処理してみます。

20170324追記

slack appが開発で使う分にはoauth callbackを使わなくても権限を追加したりtokenをもらったりすることができるようになってました。
lambdaとapi gatewayの配置方法例として以下は残しておきますが、自分たちで使うようなslack appを配置するだけならoauth callbackの配置は不要になってます。

slack appの作成

https://api.slack.com/apps

[Create New App] から新しいAppを作ります。 I plan to submit this app the Slack App Directory はこのアプリを公開する場合なので今回はチェックは不要です。

Slack_API__Applications___Slack.png

作成後に表示されるBasic Infomationにあるclient_idclient_secretを控えておきます。
また、ここで変更できるiconはコマンドを実行した時に表示されるものなので、目的に応じたものに変えるといいでしょう。

KMS

秘密にしなければならないclient_secretなどを載せるようになるので、githubに上げることを考えて、KMSで暗号化します。

  1. IAMから[暗号化キー]の[キーの作成]
  2. エイリアス名に適当な名前(今回は slack とします)を与えて「次のステップ」
  3. キー管理アクセス許可に、コマンドラインで使っているcredentialのものを選択して「次のステップ
  4. キーの使用アクセス許可は一旦飛ばして「次のステップ」
  5. 「完了」

コンソールがaws credentialsが作成したKMSにアクセスできるものなら、下記のコマンドで暗号化されたテキストが取得できるはずです。

aws kms encrypt --key-id alias/slack --plaintext "暗号化したいテキスト" --query CiphertextBlob --output text

これでclient_idclient_secretの暗号化テキストを作ります。

lambda functionがkmsのキーを読み取れないといけないので、作成されたIAMのポリシーにkms:Decryptを追加します。apexで登録したlambdaが使うIAMはproject.jsonに記載されています。

functionごとにIAMを変えられるので権限を絞れますが、どうせ全体で使うので手抜きです。
さらに、keyを限定したい場合はResouceをちゃんと指定しましょう。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": "*"
        }
    ]
}

lambdaの配置

暗号化したテキストは下記のようにproject.jsonの環境にいれてしまいます。これをlambdaのコードのなかでkmsを呼んで復号すればもとのclient_idclient_secretが呼出せます。

project.json
{
  "name": "commands",
  "description": "slack commands",
  "memory": 128,
  "timeout": 5,
  "role": "arn:aws:iam::account-id:role/slack-commands_lambda_function",
  "environment": {
    "ENCRYPTED_CLIENT_ID": "暗号化したclient_id",
    "ENCRYPTED_CLIENT_SECRET": "暗号化したclient_secret"
  }
}

lambdaの処理は、javascript苦手でnodeがさっぱりなので全部pythonで行きます。

functions/callback/ を作成して、main.pyを配置します。一応全体公開されるので余計なexceptionを返さないことを目的としてます。

GETされた文字列はマッピングテンプレートに従って、eventparams->querystring内に入っているのでそこから値を取り出します。

slackが送ってくるqueryについては下記を参照してください。

https://api.slack.com/docs/oauth

main.py
import logging
import json
import os
import boto3
import requests
from base64 import b64decode

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handle(event, context):
    try:
        kms = boto3.client('kms')
        client_id = kms.decrypt(CiphertextBlob = b64decode(os.environ["ENCRYPTED_CLIENT_ID"]))['Plaintext']
        client_secret = kms.decrypt(CiphertextBlob = b64decode(os.environ["ENCRYPTED_CLIENT_SECRET"]))['Plaintext']
    except:
        logger.error("required environments not found")
        return {"text": "error"}

    try:
        code = event['params']['querystring']['code']
    except:
        logger.error("code not found")
        return {"text": "error"}

    try:
        response = requests.get(
          'https://slack.com/api/oauth.access',
          params={'client_id': client_id, 'client_secret': client_secret, 'code': code})
    except:
        logger.error("oauth request error. response: %s", response.json())
        return {"text": "error"}
    logger.info(response.json())
    return { "text": "ok" }

また、pythonをapexでデプロイする場合には、requirements.txtで必要なmoduleを記載し、function.jsonのhookでインストールしてやります。ついでに.apexignoreで余分なファイルを配置しないようにできます。

functions/callback/requirements.txt
boto3
requests
functions/callback/function.json
{
  "description": "oauth callback",
  "hooks":{
    "build": "pip install -r requirements.txt -t ."
  }
}
functions/callback/.apexignore
*.dist-info/

最後にcallbackをデプロイします。

apex deploy callback

するとlambdaにcommands_callbackが配置されるはずです。

IAM で KMSの権限を追加

lambdaがKMSのキーを読み取る必要があるので権限を追加します。
再度IAMの[暗号化キー]画面からキーユーザの[追加]

IAM_Management_Console.png

lambdaが使っているIAMのロール(今回はcommands_lambda_function)を選択して[追加]

IAM_Management_Console.png

これでlambdaがこのkmsのキーを読み取れます。

今回はプライベートでしか使わないのでやっていませんが、lambdaのfunctionごとに異なるIAMを設定することは可能です。各functionのfunction.jsonroleを指定できます。

api gateway

  • 新規なら[今すぐ始める]、既に使っているなら一覧画面から[APIの作成]

API_Gateway.png

  • API名に適当な名前をいれて(ここではcommands)、[APIの作成]

API_Gateway_new.png

  • [アクション] -> [リソースの作成]

API_Gateway_new_method.png

  • リソース名に callback をいれて [リソースの作成]

API_Gateway_resource.png

  • [アクション] -> [メソッドの作成] -> GETを選んでチェックをクリック

API_Gateway_method_get.png

  • セットアップ画面になるので、先ほど作成したlambdaを選んで[保存]

API_Gateway_newmethod.png

  • 権限許可確認画面がでるので[OK]

API_Gateway_resource3.png

  • メソッドの実行画面で[統合リクエスト]を選択

API_Gateway_method_ex.png

  • [本文マッピングテンプレート]をクリックして詳細を出してから、[マッピングテンプレーとの追加] -> application/json を入力してチェックをクリック

API_Gateway_mapping_template.png

  • パススルー動作の変更画面がでるので [はい、この統合を保護します]

API_Gateway_paththrough.png

  • GETの中身が欲しいだけなのでテンプレートの生成:で[メソッドリクエストのパススルー]を選んで[保存]

API_Gateway_mappingtemplate2.png

  • API gatewayはデプロイしないと反映されないので[アクション] -> [APIのデプロイ] -> APIのデプロイウィンドウがでるので、デプロイされるステージから[新しいステージ]、ステージ名を適当に(これはAPI gatewayのURLに含まれます。ここではprod)いれて[デプロイ]

API_Gateway_deploy.png

ステージでURLの呼び出し:で示されるURLを取っておきます。

API_Gateway_callback.png

oauth

まずは、slack appのページの[OAuth & Permissions] 画面で、RedirectURLにAPI Gatewayで作った呼び出しのURLを入れ[Save Changes]します。

Slack_API__Applications___wacul_Slack.png

その後、ブラウザでいいので下記を開きます。

https://slack.com/oauth/authorize?scope=commands&client_id=クライアントID

scopeは必要な権限を入れてください。ここでは、コマンドを実行できるcommandsを許可してます。
oauthのscope一覧は下記です。

https://api.slack.com/docs/oauth-scopes

クライアントIDはslack app画面のclient_idとして、このURLをブラウザでいいので直接遷移すると下記のような画面に行くはずです。

Authorize_access_to_your_account___wacul_Slack.png

[Authorize]すると、先ほどのcallback URLに遷移します。うまくいっていれば

{"text":"ok"} とだけ表示されるはずです。

apex logs callback

を叩いてログ確認します。失敗している場合でもこれでなぜ失敗したかを確認して修正します。
そもそもログでていない場合、API Gatewayからlambdaが呼ばれていないのでAPI Gatewayの設定を見直しましょう。

うまくいっていれば、ログからaccess_tokenがもらえているはずです。botincoming-webhookを有効にしていればそれらも含まれています。

これらも暗号化してproject.jsonに放り込みます。

他のslackチームで使うわけでもない場合は、callbackをAPI Gatewayから削除してしまってもいいでしょう。

これでslack appの登録の完了です。

次でslash commandとinteractive messageを登録します。

TIPS

incoming-webhookのkms

incoming-webhookを使う場合、使う文字列は https:// で始まりますが、awsコマンドでfile://http:// などで始まる文字列の場合は問答無用で、そのファイルやURLを参照しにいくので、一旦ファイルにテキストを保存して暗号化します。
カレントディレクトリのtmpにテキストを入れたとして

aws kms encrypt --key-id alias/slack --plaintex file://./tmp

.gitignore

rootに言語の.gitignore、pythonの場合は取得したモジュールがディレクトリに置かれるのでfunctionごとに.gitignoreしてます。

.gitignore
.Python
build/
develop-eggs/
dist/
eggs/
.eggs/
parts/
sdist/
*.egg-info/
*.dist-info/
.installed.cfg
*.egg
functions/callback/.gitignore
boto3/
botocore/
s3transfer/
requests/
jmespath/
docutils/
dateutil/
six.py