この記事を読むとできるようになる事
こんな感じでその日の予定をGoogleカレンダーから取得してSlack通知を飛ばす事ができます。
目指す構成
① CloudWatch EventsでLambdaを起動。
② API経由でGoogleカレンダーから予定を取得。
③ Slackに通知を飛ばす。
使用技術
- Slack
- Google API
- AWS
- Lambda
- CloudWatch Events
- Ruby
- 2.5系
※ Slack通知用のBotは各自あらかじめ作成しておいてください。
参照: ワークスペースで利用するボットの作成
完成形(Githubリポジトリ)
一から作るのが面倒な人、作業を進める途中で詰まった人などは↑のリポジトリをcloneするなり比較して参考するなりしてください。
実装
では、作業開始です。
下準備
まず、API経由でGoogleカレンダーにアクセスするために必要な準備がいくつかあるので、そちらからやっていきましょう。
秘密鍵(JSON)を作成
- Google Cloud Platformにログイン
- Google Calendar APIを有効化
- サービスアカウント(認証用)を作成
- 秘密鍵(JSON)をダウンロード
この辺に関しては私が以前に書いた記事(↓)に詳しく記載しているので、そちらをご覧ください。
参照: PHP/Laravelを使ってGoogleドライブにファイルをアップロードしてみる
記事内ではGoogleドライブを使用していますが、基本的な流れは同じなのでそれぞれの単語を「Googleカレンダー」に適宜置き換えて操作していけば問題無いはず。
サービスアカウント作成後、秘密鍵などの情報が含まれたJSONファイルをダウンロードするところまで進めればOKです。
Googleカレンダーの設定
Googleカレンダーの右上にある歯車マークを押して「設定」へと進みます。
左サイドバー内の「マイカレンダーの設定」→「特定のユーザーとの共有」から、先ほど作成したサービスアカウントのメールアドレスを追加しましょう。
権限に関してはお好みですが、今回はあくまで情報の「取得」しかしないので「予定の表示」で大丈夫です。
あとは「カレンダーの統合」内にある「カレンダーID」をメモに控えておいてください。こちらも後の実装で必要になってきます。
コード
下準備が完了したので、ここから実際の処理を行うコードを書いていきます。
リポジトリを作成
$ mkdir slack-notifier-for-google-calendar && cd slack-notifier-for-google-calendar
Gitを設定
$ git init
$ touch .gitignore
.bundle
.ruby-version
/vendor/bundle
credentials.json
.env
Gitで管理したくないものを記述。
Rubyのバージョンを指定
$ rbenv local 2.5.1
2.5系か2.7系を推奨。(Lambdaで運用する事を考慮して)
各種gemをインストール
$ bundle init
↑のコマンドでGemfileを生成し、以下のように編集します。
# frozen_string_literal: true
source 'https://rubygems.org'
gem 'async-websocket'
gem 'google-api-client'
gem 'dotenv'
gem 'slack-ruby-bot'
その後、必要なGemをインストール。
$ bundle install --path vendor/bundle
※ 後ほどgemも含めてzipファイルにパッケージングする必要があるため、グローバルではなくローカル(「vendor/bundle」以下)にインストールします。
サービスアカウントの秘密鍵(JSON)をルートディレクトリ直下に配置
下準備のところで作成したサービスアカウントの秘密鍵(JSON)を「credentilas.json」にリネームし、ルートディレクトリ直下に配置します。
うっかりGitHubなどにアップしてしまうと大変な事になるので、ちゃんとgit管理から外れているか確認してください。
app.rbを作成
$ touch app.rb
require 'bundler/setup'
require 'google/apis/calendar_v3'
require 'googleauth'
require 'date'
require 'dotenv'
require 'slack-ruby-client'
Dotenv.load
APPLICATION_NAME = 'Google Calendar × Slack'.freeze # そこまで重要ではないので適当な名前でOK。
CREDENTIALS_PATH = './credentials.json'.freeze # サービスアカウント作成時にDLしたJSONファイルをリネームしてルートディレクトリに配置。
CALENDER_ID = ENV['CALENDER_ID'].freeze # Googleカレンダー設定ページの「カレンダーの統合」という項目内に記載されている。
class GoogleCalendar
def initialize
@service = Google::Apis::CalendarV3::CalendarService.new
@service.client_options.application_name = APPLICATION_NAME
@service.authorization = authorize
@calendar_id = CALENDER_ID
end
# 認証。
def authorize
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: File.open(CREDENTIALS_PATH),
scope: Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY
)
authorizer.fetch_access_token!
authorizer
end
# 引数に渡した日付をもとに予定一覧を取得。
def fetch_events(date)
@service.list_events(
@calendar_id,
max_results: 10, # 取得する予定の最大数。
single_events: true,
order_by: 'startTime',
time_min: "#{date}T00:00:00Z", # 取得を開始するタイミング
time_max: "#{date}T23:59:59Z" # 取得を終了するタイミング
)
end
end
def create_slack_message
google_calender = GoogleCalendar.new
events = google_calender.fetch_events(Date.today)
# その日の予定が何も無かった場合はここで処理終了。
return '本日の予定はありません。' if events.items.empty?
event_list = ''
events.items.each_with_index do |event, index|
start_time = event.start.date || event.start.date_time
end_time = event.end.date || event.end.date_time
event_details = "* #{event.summary} (#{start_time.strftime('%H:%M')} ~ #{end_time.strftime('%H:%M')})"
event_details << " #{event.hangout_link}" if event.hangout_link # もしGoogleハングアウトのURLがある場合はそれも拾う。
event_details << "\n\n" unless index == events.items.size - 1
event_list << event_details
end
# ↑で取得した予定をメッセージの形に整形する。
message = <<~EOS
本日の予定です!
```
#{event_list}
```
EOS
end
# Slack通知を行うための初期設定。
Slack.configure do |conf|
conf.token = ENV['SLACK_BOT_TOKEN']
end
def post_to_slack
message = create_slack_message
client = Slack::Web::Client.new
client.chat_postMessage(
channel: ENV['SLACK_CHANNEL_NAME'], # 通知を飛ばしたいSlackチャンネルを指定。
text: message,
as_user: true
)
end
post_to_slack
環境変数をセット
$ touch .env
CALENDER_ID=hoge@example.com
SLACK_BOT_TOKEN=xoxb-*******************
SLACK_CHANNEL_NAME=#hoge-channel
それぞれの値をセットしてください。(人によって違います)
テスト実行
$ bundle exec ruby app.rb
指定したチャンネルにメッセージが飛んでいれば成功です。
ちゃんと正確に取得できていますね。
デプロイ
正常な動作が確認できたら、いよいよデプロイしていきます。
app.rbを編集
「./app.rb」を次のように書き換えます。
......
def post_to_slack
message = create_slack_message
client = Slack::Web::Client.new
client.chat_postMessage(
channel: ENV['SLACK_CHANNEL_NAME'], # 通知を飛ばしたいSlackチャンネルを指定。
text: message,
as_user: true
)
end
post_to_slack
......
......
def post_to_slack(event:, context:)
message = create_slack_message
client = Slack::Web::Client.new
client.chat_postMessage(
channel: ENV['SLACK_CHANNEL_NAME'],
text: message,
as_user: true
)
end
......
- 「post_to_slack」の引数に「event:」「context:」を渡す。
- 最後の行で「post_to_slack」を実行している部分を削除。
引数に「(event:, context:)」を渡さないとLambda上では動かないので注意してください。
参照記事: AWS Lambda 関数ハンドラー
gemをパッケージングしてLambdaレイヤーを作成
Lambdaレイヤー: 複数のLambda関数で外部ライブラリやビジネスロジックを共有できる仕組み。
参照: AWS Lambda Layersでライブラリを共通化
今回のデプロイではLambdaレイヤーを利用します。
各種gemをLambda関数と切り離しておく事でコードサイズを小さく保つ事ができますし、今後同じようなLambda関数を作る事になった際には使い回しができるので便利です。
gemをパッケージング(zipファイル)
$ zip -r layers.zip ./vendor
gemが入っている「vendor」以下をzipファイルに閉じ込めます。
上手くいくとこんな感じで「layers.zip」として出力されるはず。
Lambdaレイヤーを作成
AWSコンソール画面から「Lambda」→「レイヤー」→「レイヤーの作成」へと進み、先ほど出力したzipファイルをアップロードします。
これで今後、Googleカレンダーから予定を取得してSlack通知を飛ばすのに必要なgemを複数のLambda関数で使い回す事ができるようになりました。
Lambda関数を作成
Lambdaレイヤーの準備ができたので、次はLambda関数の作成に入ります。
AWSコンソール画面から「Lambda」→「関数」→「関数の作成」へと進み、必要な情報を入力してLambda関数を作成します。
実行コード(app.rb)と秘密鍵をアップロード
$ zip -r codes.zip app.rb credentials.json
先ほどと同じ容量で「app.rb」と「credentials.json」をzipファイル化。
Lambda関数の管理画面からzipファイルをアップロードします。
上手くいくとこんな感じで2つのファイルが反映されているはず。
各種設定
あとは細かい設定がいくつかあるのでそれらを片付けていきます。
メモリ&タイムアウトを変更
- メモリ
- 512MB
- タイムアウト
- 30秒
この辺の数値はお好みで変えてください。
ハンドラ名を変更
「ハンドラ」を「lambda_function.lambda_handler」から「app.post_to_slack」に変更。
Lambdaレイヤーとの紐付け
「レイヤーの追加」から先ほど作成したレイヤーとバージョンを指定して追加。
環境変数をセット
それぞれ適宜入力。
- GEM_PATH
- /opt/vendor/bundle/ruby/2.5.0
- TZ
- Asia/Tokyo
↑この2つは固定。
テスト実行
これで大体の設定は済んだので、実際に動くかどうかテストします。
- テンプレート
- hello-world
- 名前
- test
- パラメータ
- 空欄でOK
「呼び出し」を実行してSlackの通知が飛んでいれば成功です。
スケジュール実行
最後に、決められたスケジュールで定期実行がされるようにします。
「トリガーを追加」をクリック。
- トリガー名: CloudWatch Events
- ルール: 新規ルールの作成
- ルール名: 任意
- ルールの説明: 任意
- ルールタイプ: スケジュール式
- スケジュール式: 任意
cron式の書き方の説明については今回省略。
参照: クーロン(cron)をさわってみるお
今回は平日の午前10時に通知してもらう事を想定して「cron(0 1 ? * MON-FRI *)」としました。(UTCだと日本時間と9時間ほど時差があるので注意。)
時間の経過を待ち、しっかりと定期実行されていれば成功です。
もし上手く行かなかった場合はCloudWatchのロググループ内にログが出力されているはずなので適宜デバッグしてください。
あとがき
お疲れ様でした。もし記事通りに進めて上手く動作しない箇所があったらコメント蘭などで指摘していただけると幸いです。