少しだけ実戦的な内容。いろいろ調べながら助けてもらいながら初めて作ったので、詳しい説明はできないかもしれませんが、できるだけ再現性を意識して易しく書こうと思います。
ちょっと宣伝
ノベルワークスという会社でアルバイトをしながら学ばせてもらったものをまとめております。自社サービスリクシェア for kintoneも運営しておりますのでよろしければぜひ~。
実現したい機能
AWS Lambdaから複数のGoogleカレンダーたちの予定を取得。さらに、もう一工夫して、前回アクセスからのカレンダーの変更分だけを取得できるようにする。
やること
実装の手順を簡単にまとめます:
- GoogleCloudPlatformでサービスアカウントを作成
- そのアカウントを取得したいGoogleカレンダーたちに共有設定する
- トークンを保存しておくためのDynamoDBテーブルを作成
- Lambda関数の作成
用意するもの
- Googleアカウント
- Googleカレンダー
- AWSアカウント
- Node.jsが使えるPC
手順
1.サービスアカウントを作る
まずはGoogleカレンダー側の準備を進めていきましょう!
その第一歩として、GoogleCloudPlatformでログインしましょう。もしGoogleアカウントを持っていない方はこの機会に作ってみてください♪
ページを開いてもらうと、「コンソール」に行けると思います。
こんな感じになっているかなと思います。
(この画面はすでにプロジェクトを作成済みなので、もしかしたら違うかもしれませんね。よく覚えてないです)
それでは、左のメニューから「APIとサービス」を選択してください。APIを利用するにはプロジェクトを作成する必要があるので、作成しましょう。
プロジェクト作成が完了したら、左側に新しく
こんな感じのメニューが出ていると思います。「ライブラリ」を選択しましょう。こちらで、GoogleCalendarAPIを有効にします。
検索欄でgoogle calendarなどと入力すると、
こんなのが出てくるかと思います。クリックすると詳細画面に入るので、「有効にする」ボタンを押してみましょう。
さあ、ここまで出来たら、今回の手順である「サービスアカウント」を作りましょう。
「ライブラリ」メニューの下にあった「認証情報」メニューをクリックしてください。
そこで「認証情報を作成」を押すと、いくつか選択肢が出てくるので、「サービスアカウントキー」を選択しましょう。
サービスアカウント名は任意で結構です。
「役割」についてですが、僕は正直よくわかっていません(笑)よくわからないときはとりあえず強そうな権限を与えておきましょう。「Project -> オーナー」を選択します。
キーのタイプはここではJSONにしておきます。
以上の設定が完了したら「作成」ボタンをクリックしましょう。このとき、キーの情報が書かれたjsonファイルがダウンロードされるかと思いますので、大切に保管しておきましょう。後ほど、AWS Lambda 関数を作成する際に用います。
完了すると、先ほどと同じ画面に戻りますが、サービスアカウントが作成されていることが確認できるかと思います。ここで「サービスアカウントの管理」というのが右側のほうにあると思うので、クリックすると、サービスアカウントのより詳細な情報を見ることができます。
そこの、「サービスアカウントID」はコピーしておいてください。次のセクションですぐ使います。
ただし!!
おそらくサービスアカウントIDは2行にわたって表示されていると思いますが、1行ずつコピーすることをお勧めします。改行が入ってうまくいかないことがあります。僕はここで思うように動きませんでした。
2.Googleカレンダーに共有設定する
次に、予定を取得したいGoogleカレンダーにサービスアカウントを共有設定することで、カレンダーの情報を取得できるようにします。
カレンダーの設定画面に移ると、下の画面のような項目があるかと思います:
ここでユーザーを追加します。この時に使うメールアドレスが、先ほどコピーしていただいた「サービスアカウントID」になります。
これでGoogle側の準備は完了です!!これからAWS側の準備に入っていきます!
(補足)
サービスアカウントを複数のカレンダーに共有設定しておくと、それらすべてのカレンダーの情報を取得することができます。便利ですね。
3.トークンを保存しておくためのDynamoDBテーブルを作成
続きまして、トークンを保持しておくためのデータベースをDynamoDBで作成しようかと思います。
ここでトークンという曖昧な言葉を出してしまいましたので、ここでのトークンの意味について補足をしておきます。
GoogleCalendarAPIで予定を取得する際、APIがnextSyncTokenという名前のある文字列を返してくれます。これを次に予定を取得する際にパラメータとしてAPIに送信すると、なんと、APIが前回のアクセスからの変更分のみを返してくれるようになります!!
ですので、「いいからとにかく予定が取得できればいいんだ!」という方はここのセクションは見なくても全然大丈夫です(笑)ただ、次のセクションで紹介するLambda関数のコードの中にDynamoDBを操作するコードが含まれますので、その点だけご了承ください。
それでは、AWSのコンソール画面に移りましょう。AWSのアカウントを持っていない方は、アカウントを作ってください。
たくさんサービスがありますが、その中からDynamoDBを選択してください。ダッシュボードに移ると思いますので、そこで「テーブル作成」を押してテーブルを作ります。
ここでは、こんな感じのテーブル設計にしようかと思います:
{
"mainKey": "testItem",
"token": {
"カレンダーID1": "対応するトークン",
"カレンダーID2": "対応するトークン",
//以下、同じ構造が続く
}
}
正直、この辺は好きに決めてもらってかまいませんが、説明がしやすいので考えうる最もシンプルな構造にしました。お粗末ですね。
ですので、上のような構造にしたければ、パーティションキーとして"mainKey"を設定してテーブルを作成しましょう。1分もたたずにテーブルが出来上がります。AWSのすごいところですね。簡単で速い。
最後に、あらかじめ項目(アイテム)を追加しておきましょう。
テーブルを作成したときに出てくるメニューから「項目」を選択し、追加する項目を手動で作成することができます。ここではパーティションキーに何かてきとうな値を書いて保存しておきましょう。この値はまた後で使います。
以上で準備は完了です。いよいよ次のセクションでLambda関数を実装していきます!!
4.Lambda関数を作成
次にLambda関数をいよいよ実装していきます!
とりあえず関数を作成しておいて、そのあとにコーディングをしようかなと思います。
それでは、AWSのコンソールから、Lambdaを選び、関数を作成しましょう。ここでも最低限シンプルなものを作ろうかなと思います。
「一から作成」を選択し、ランタイムはここではNode.js.6.10を選択することにします。
最後にロールなのですが、ここでは注意が必要です。
そもそもAWSにおけるロールとは、AWSの各リソースに付与することのできる権限を記したもので、ここでは、今から作成するLambda関数がAWSのほかのどんなリソースにアクセスしたりできるかを規定するロールを選択することになります。
今回の記事では、DyamoDBにアクセスする権限が必要なので、もしそのようなロールがない場合は自分で作成しなければなりません。
ロールの作成については、最後に「補足~ロールの作成について~」というセクションで説明をしてみましたので、よくわからない方は参考にしてみてください。
そして、以上の選択が完了したら関数を作成しましょう!こちらもクリックしたらすぐできます。AWSすごいですね。
関数は作成できたので、いよいよコードを書いていこうと思います。
なお、簡単なコードならインラインで(AWS Lambdaの画面上で)書くことができますが、今回はNode.jsのモジュールの一つgoogleapisを使いたいので、ローカルのNode.jsが使える環境でコーディングをしたいと思います。
5.Lambda関数のコーディング~ファイル等の準備~
それでは、いよいよコードを書きたいと思います。なお、今回のコード作成で主に参考にさせてもらったwebページは
- https://qiita.com/Fujimon_fn/items/9a0ec4eca75ce0784722
- http://isd-soft.com/tech_blog/accessing-google-apis-using-service-account-node-js/
- https://developers.google.com/calendar/
- https://qiita.com/Yuki_BB3/items/83198b4d9daca7ccd746
です。2つ目と3つ目は英語ですが、ぜひぜひご覧になってください!
作業フォルダをGoogleCalendarとします。フォルダの中のファイル構成は以下のようにします:
GoogleCalendar
|--- index.js
|--- privatekey.json
ここで、privatekey.jsonは、はるか昔にサービスアカウントを作成した際にダウンロードされたJSONファイルです。名前を変えておきましょう。
以下ではコマンドラインでの操作を想定しております。上で作成したディレクトリに移動してください。
そして、GoogleAPIを使うためのNode.jsのモジュールをインストールすることをしておきましょう。一応、npmのバージョンも確認しておいてください。
$ npm --version
> 5.6.0
僕の場合はこうなりました。
そして、インストールの手順としては
$ npm init
$ npm install googleapis --save
となります。
(npm initとしたとき、いろいろ聞かれますが、全部エンターキーを押してガン無視しましょう。)
上の2つのコマンドを実行すると、モジュールがたくさん入ったフォルダ「node_modules」が生成されます。したがって、現在の作業フォルダの中身はこんな感じになっているかと思います:
GoogleCalendar
|--- node_modules
|--- index.js
|--- privatekey.json
ほかにもjsonファイルが生成されたりしますが、以降で使うのは上の3つだけです。
6.Lambda関数のコーディング~index.jsを書く~
さて、いよいよindex.jsファイルにコードを書いていくわけですが、まずは今回僕が作成したコードをとにかく見てもらい、それについて解説をしていくという形にしたいなと思います。
以下がindex.jsファイルの中身です:
'use strict';
//PART1
/////////////////////////////////////////////////////////////////
//GoogleCalendarAPIの処理で必要なもの
const {google} = require('googleapis');
const calendar = google.calendar('v3');
//privatekey.jsonを読み込む
const privatekey = require('./privatekey.json');
//認証に必要な設定
const jwtClient = new google.auth.JWT(
privatekey['client_email'],
null,
privatekey['private_key'],
['https://www.googleapis.com/auth/calendar']
);
//DynamoDBにアクセスするために必要なもの
const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient({
region: 'ap-southeast-1' //ここはリージョンを適切に変えてください。僕はシンガポールリージョンで作っているのでこうです。
});
//テーブル名とパーティションキー
const tableName = 'testTableOta';
const partitionKey = 'testItem';
///////////////////////////////////////////////////////////////////////
exports.handler = (event, context, callback) => {
Promise.resolve()
//PART2
//////////////////////////////////////////////////////////////////
//認証処理
.then(function(){
return new Promise(function(resolve, reject){
//authenticate request
jwtClient.authorize(function (err, tokens) {
if (err) {
reject(err);
}else {
console.log("認証成功");
resolve();
}
});
});
})
/////////////////////////////////////////////////////////////////////
//PART3
////////////////////////////////////////////////////////////////////
//Dynamoからまずは必要なデータを取得してくる
.then(function(){
return new Promise(function(resolve, reject){
const params = {
'TableName': tableName,
'Key': {
'mainKey': partitionKey
}
};
dynamo.get(params, function(error, tableData){
if(error){
console.log("DynamoDBからの取得失敗 error:", error);
reject(error);
}else{
console.log("DynamoDBからの取得成功!");
console.log("DynamoDB datas:", tableData['Item']['token']);
resolve(tableData['Item']['token']);
}
});
});
})
///////////////////////////////////////////////////////////////////////////////////
//PART4
///////////////////////////////////////////////////////////////////////////////////
//カレンダーリスト取得
.then(function(token){
return new Promise(function(resolve, reject){
calendar.calendarList.list({
'auth': jwtClient
}, function(error, resp){
if(error){
reject(error);
}else{
const tokenAndCalendarList = {
'token': token,
'calendarList': resp.data.items
};
resolve(tokenAndCalendarList);
}
});
});
})
//////////////////////////////////////////////////////////////////////////////////
//PART5
//////////////////////////////////////////////////////////////////////////////////
//カレンダーたちから予定を取得する
.then(function(tokenAndCalendarList){
//カレンダーリスト、トークン
const calendarList = tokenAndCalendarList['calendarList'];
const token = tokenAndCalendarList['token'];
//取得するカレンダーの個数(この数だけあとでPromiseを生成する)
const len = calendarList.length;
//tokenを記録しておくための連想配列
const syncTokens = {};
//PART5.5
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//動的に生成するPromiseオブジェクトたちを記録しておく連想配列
const promises = {};
//動的にPromiseオブジェクトを生成する
for(let i = 1; i <= len; i++){
//i番目のカレンダーID
const calendarId = calendarList[i-1]['id'];
//Promise生成
promises['promise' + i] = new Promise(function(resolve, reject){
const params = {
'auth': jwtClient,
'calendarId': calendarId
};
//tokenが存在すれば、パラメータに追加して、カレンダーの予定の変更分のみ取得する
if(token){
params['syncToken'] = token[calendarId];
}
//予定の取得
calendar.events.list(params, function(error, {data}){
//カレンダーの予定達
const events = data.items;
//nextSyncTokenを記録
syncTokens[calendarId] = data.nextSyncToken;
//それぞれのPromiseでは、カレンダーの予定を返す
resolve(events);
});
});
}
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
//以上でPromiseたちを使ってそれぞれのカレンダーたちの予定を取得し、トークン情報を記録できた
const arrayPromises = [];
for(let j = 1; j <= len; j++){
arrayPromises.push(promises['promise' + j]);
}
return Promise.all(arrayPromises).then(function(eventsList){
//DynamoDBへトークンの情報を保存する処理をする
const item = {
'mainKey': partitionKey,
'token': {}
};
for(let k = 0; k < len; k++){
const calendarId = calendarList[k]['id'];
item['token'][calendarId] = syncTokens[calendarId];
}
const params = {
'TableName': tableName,
'Item': item
};
return new Promise(function(resolve, reject){
//DyamoDBテーブルへの更新処理
dynamo.put(params, function(error, data){
if(error){
console.log(error);
reject(error);
}else{
console.log("DynamoへのPUT成功 data:", data);
resolve(eventsList);
}
});
});
});
})
////////////////////////////////////////////////////////////////////////////////////////
//PART6
/////////////////////////////////////////////////////////////////////////////////////
.then(function(eventsList){
//成功!
callback(null, eventsList);
})
.catch(function(error){
//失敗。。。
callback(error);
});
};
とても長くなりましたが、これですべてです。あとはテーブルネームやパーティションキーの名前を適切に変更すれば動くようになっています。
以下でコードの解説をしていきます。
7.Lambda関数のコーディング~コード解説~
解説といっても、まだ僕自身理解できていないところがいくつかあるので、そこはご容赦ください。わかったら随時更新していきます。
PART1
ここでは、今回のコード全体に必要な情報で最初に準備できるものを先に定義しています。
まずはGoogleCalendarAPIにアクセスするためのオブジェクトを用意:
const {google} = require('googleapis');
const calendar = google.calendar('v3');
その次のコードでは、サービスアカウントを作成したときに取得したJSONファイルを読み込んでいます。JSONの形がそのままオブジェクトになって使うことができるので、たとえばprivatekey['client_email']
などど要素を参照することができます。
const privatekey = require('./privatekey.json');
その次は、認証に必要な設定です:
const jwtClient = new google.auth.JWT(
privatekey['client_email'],
null,
privatekey['private_key'],
['https://www.googleapis.com/auth/calendar']
);
認証とは、要はいつもやっている「メールアドレスとパスワードを入力するやつ」です。それの別バージョンだと思ってください。
また、4つ目の引数は、GoogleCalendarAPIのスコープです。詳しくはこちらをご覧ください。僕もよくわかってないです。
PART1の最後では、DynamoDBの操作の準備をしています。
//DynamoDBにアクセスするために必要なもの
const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient({
region: 'ap-southeast-1' //ここはリージョンを適切に変えてください。僕はシンガポールリージョンで作っているのでこうです。
});
//テーブル名とパーティションキー
const tableName = 'testTableOta';
const partitionKey = 'testItem';
dynamo
がDynamoDBのオブジェクトを表し、これにputやgetというメソッドがあるので、それによってテーブルのデータを取得したり更新したりできます。
tableName
は、ご自身が作成したテーブルの名前にしてください。
partitionKey
は、テーブルを作成したときに手動で作成した項目のパーティションキーの値を書いてください。
PART2
ここでは実際にjwtClient
を用いて認証処理をしています。
PART3
ここでは、DynamoDBからトークンのデータを取得してきます。
注意しておきたいのは、リクエストパラメータです:
const params = {
'TableName': tableName,
'Key': {
'mainKey': partitionKey
}
};
TableNameの方はテーブルの名前で大丈夫です。
問題はKeyの値です。ここには、テーブルの要素を確定させるための情報をオブジェクトの形で渡してやる必要があります。
今回の場合は、要素を一意に決めるのはパーティションキーだけですので、それだけ記述すればOKです。
ただ、テーブルを作成するときにソートキーを追加して、「パーティションキー+ソートキーの組み合わせで一意に決める」というテーブル設計にしている場合は、ソートキーの情報も記述してください。
また、取得したデータ(tableData)はオブジェクトの形になっており、今回のテーブル設計では以下のような構造になっています
tableData: {
Item: {
mainKey: 'testItem',
token: {
'カレンダーID1': '対応するトークン'
}
}
}
ですので、次のthenでの処理にはtableData['Item]['token']
の部分だけを渡すようにしています。
なお、tokenの部分は、サービスアカウントを共有したカレンダーの数だけ増えていきます。
PART4
ここでは、カレンダーリストを取得しています。カレンダーリストとは、サービスアカウントを共有設定したカレンダーたちそれぞれの情報です。たとえば、カレンダーIDなどの情報が記されています。
具体的には、resp.data.item
がそれに相当します。
なお、次のthenの処理ではトークンとカレンダーリストの情報を使いますので、それを連想配列にまとめてから後続の処理に引き渡しています。
PART5(前半)
ここでは、PART4でから引き継いだトークンとカレンダーリストの情報を用いて、実際に各カレンダーの予定を取得していきます。ここが本当にやりたかったところです。
ここで、僕がコーディングするうえで意識した点を述べておきます:
- 各カレンダーから予定を取得するという非同期処理は、それぞれ個別にPromiseを用意して管理したい。
- ただ、カレンダーの個数は事前に知ることはできないので、任意個のカレンダーに対応できるように動的にPromiseを生成して処理しないといけない。
- 予定を取得するときに、一緒にnextSyncTokenも取得できる。
- 予定の取得がすべて完了したら(=トークンをすべて取得したら)、DynamoDBの項目の内容を更新しないといけない。
このくらいです。PART5の前半では、その準備をしています。
特に、変数len
がポイントです。これの個数に合わせて動的にPromiseを生成していきます。
PART5.5
ここでは、実際にPromiseを使って非同期処理を実行しています。
//動的に生成するPromiseオブジェクトたちを記録しておく連想配列
const promises = {};
//動的にPromiseオブジェクトを生成する
for(let i = 1; i <= len; i++){
//i番目のカレンダーID
const calendarId = calendarList[i-1]['id'];
//Promise生成
promises['promise' + i] = new Promise(function(resolve, reject){
const params = {
'auth': jwtClient,
'calendarId': calendarId
};
//tokenが存在すれば、パラメータに追加して、カレンダーの予定の変更分のみ取得する
if(token){
params['syncToken'] = token[calendarId];
}
//予定の取得
calendar.events.list(params, function(error, {data}){
//カレンダーの予定達
const events = data.items;
//nextSyncTokenを記録
syncTokens[calendarId] = data.nextSyncToken;
//それぞれのPromiseでは、カレンダーの予定を返す
resolve(events);
});
});
}
まずポイントは、いったん空の連想配列を準備しておき、それに要素を追加していくことで、「キー=変数名」、「値=変数の値」とみなすことで仮想的に変数を生成していることです。
これについては、こちらの記事で簡単にまとめましたので是非ご覧ください。
パラメータの作成では、認証情報、カレンダーIDが必要なのですが、それに加えてトークンの情報がある場合には、それも追加でパラメータに含ませています。
そして、calendar.events.list
の部分で実際に予定を取得しています。変数data
の構造は以下のようになっています:
data = {
(なんかその他いろいろ)
nextSyncToken: 'xxxxxxxxxxxxxxxxxxxxxx',
items: [
{予定1},
{予定2},
{予定3},
・・・
]
}
見てわかるように、この中にnextSyncTokenの情報がありますので、これを連想配列で記録しておきます。
PART5(後半)
生成したPromiseたちを並列で管理するためにPromise.all
を用います。
先ほど述べたように、Promiseたちの処理がすべて完了したら、DynamoDBにトークンの情報を更新します。
そのときのパラメータは以下のような構造にしなければいけません:
{
'TableName': tableName,
'Item': {
(この中はテーブルに保存する項目の情報)
}
}
Item
の中は、テーブルに保存する項目をオブジェクトの形でまとめたものを与えてやる必要があります。ただし、後でput
メソッドで更新をするのですが、該当する要素が存在しない場合はテーブルに追加保存してくれ、該当する要素が存在している場合は、与えられた情報に基づいて更新してくれます。
PART6
最後は開発者側の処理です。
最後のthenは、ここまで無事に処理が到達すれば、それは処理が成功(少なくともエラーは起きていない)したことを意味するので、Lambdaコンソールに表示されるようにします:
callback(error, success);
成功なので、第1引数にはnull
を渡して、第2引数にはなにか適当なものを渡しておきます。僕のコードでは取得したすべての予定を渡していますが、ここは何でもいいです。
また、catch
は、どこかのPromise処理でエラーが起こったりreject
関数が呼ばれたときに実行される処理を記述しているところです。要は、エラーをここですべて捕捉しているんですね。ですので、エラーオブジェクトをcallback関数の第1引数に渡しています。
8.zipファイルをLambdaにアップロードしよう
もう一度フォルダの構成を確認しましょう:
GoogleCalendar
|--- node_modules
|--- index.js
|--- privatekey.json
Lambdaにこれらをアップロードする際には、これらをzipファイルにまとめてからアップロードしなくてはいけません。
この時注意したいのは、作業ディレクトリの中にある「node_modules、index.js、privatekey.json」をきちんと指定して、これらをzipファイルにまとめることです。
それができましたら、Lambdaコンソーの少しスクロールしたところに以下のような「関数コード」という部分があるかと思います:
ここで、画像にあるように「1.zipファイルをアップロード -> 2.アップロードをクリックしてファイルを選択 -> 3.保存」としてください。
これで作成したコードをLambdaに適用することができました!
9.テストをしてみよう
ここまでできたら取得の準備は完了です!お疲れさまでした!!
それでは、実際に作成したLambda関数を実行してみましょう。Lambdaコンソールの「テスト」というところをクリックしてみましょう。
テストイベントを作成するよう指示が出ますが、今回はそのイベントオブジェクトは使わないので、てきとうで結構です。
(本来は、Lambda関数の呼び出し元から送られてきたデータを想定して記述しないといけないですが、今回の記事では呼び出し元のところまでは触れません)
そして、もう一度「テスト」の部分をクリックしましょう!
うまく成功しましたでしょうか??何かしらエラーが出ましたら、Lambdaコンソール上でも確認できますし、CloudWatchというAWSのサービスでも確認ができます。
まとめ
以上が僕が実際にやってみた一連の作業です。僕自身まだ完全に理解できていませんが、随所にポイントがたくさん散りばめられていて、勉強になる制作物になったと思います。
長文でしたが、最後まで読んでくださいましてありがとうございました。
補足~ロールの作成について~
AWSコンソールの「IAM」を選択し(←これ、アイアムって読むそうですよ)、「ロール」を選択しましょう。そこで新しいロールの選択ができます。
今回はLambdaに権限を与えるロールを作成したいので、上野画像のように選択して、「次のステップ:アクセス権限」をクリックしましょう。
すると、「ポリシー」と呼ばれる権限を選択することができます。ここで、先ほど説明したように、DynamoDBにアクセスするための権限を付与することになります。
下画像のように、検索欄にDynamoDBとでも入力すると、「AmazonDynamoDBFullAccess」というロールが選択できることがわかります。左のチェック欄にチェックをつけましょう。
ついでに、ログを確認したりするためにCloudWatchへの権限も付与しておくといいでしょう。「CloudWatchFullAccess」というロールにもチェックを付けてください。
以上でロールを作ることができました。AWSのサービスでうまく動作しないときに、適切に権限を付与できていないという場合もよくあるみたいなので、気を付けてください。