こんにちは。経営企画室 プロトタイプビルダーの山下です。
ラーニングサービス部 AWS認定インストラクターの傍ら、主に自分の作業まわりを改善するプロトタイプビルダーを兼任しています。
今日は今年作って、運用しているツールを一つ紹介します。
読んでいただいた方の何かのヒントになれば幸いです。
課題
トレノケートではオンライントレーニングを行う上での配信サービスとして、Zoomミーティングを利用しています。
Zoomミーティングには、投票機能があり、受講者の皆様にアンケートを取ることができます。
ミーティングにはテンプレート機能があるので、ミーティングの設定とあわせて作成しておいた投票を使い回せます。
ですが、トレノケートではミーティングを作成する部門があり、開催するインストラクターが作成するわけではありません。
そして、コース数も約1,400コースあり、それぞれで個別管理をすることが現実的ではないため、テンプレート機能は使いません。
なので、投票などの個別設定はミーティングが作成された後にインストラクターがそれぞれ行います。
そこで例えば10問ほどある投票を作成しようと思うと、毎回同じ質問と選択肢をコピー、ペーストするだけでも、10分以上の時間がかかります。
私はこの作業が面倒で面倒で苦手でした。
この作業が待っていると思うと少しばかり憂鬱になってしまい、美味しいお酒も100%楽しむことができませんでした。
そこで、まずはZoomのAPIのpollsへPostmanを使って、投票内容を書いておいたJsonをPOSTする運用を始めました。
これで毎回、秒で投票作成が完了するようになりました。
すごく便利ですごく嬉しかったので、他のインストラクターにも共有しようと思いましたが、そのためにはZoomのAPI認証情報を共有しなければなりません。
トレノケートではユーザーごとにはAPI認証情報の発行は制御されていて、管理部門から発行してもらったAPI認証情報を使っています。
そこで、以下の要件を満たすツールを作りました。
- インストラクターはログイン認証して利用する。
- 投票内容はゼロから書かなくても過去のミーティングから取得可能。
- 一度使った投票は何度でも呼び出し可能。編集削除も可能。
- 他のインストラクターの投票は見ない。
- ZoomのAPI認証情報は安全な場所で管理して使用する。
作ったもの
ログイン
[UserName]と[Password] を入力して[Login]ボタンからログインします。
投票作成までの流れ
(1) 投票内容登録
データベースに投票内容を登録します。
(2) 投票作成
データベースに登録した投票を、指定したミーティングIDに作成します。
(1) 投票内容登録
[投票内容]をJson形式で入力して、[Add]ボタンで登録します。
[名前]はこのツールで投票内容を見分けやすくするためのものです。いわば目印です。わかりやすい名前をつけてください。(オプション機能参照)
過去のミーティングから投票内容を呼び出すこともできます。(オプション機能参照)
* Jsonの例
2つの投票で、1つめの投票に2つの質問、2つめの投票は1つの質問の例です。
「好きなメニューは何ですか?」の質問は、複数選択("type": "multiple")です。
{
"polls": [
{
"title": "おはようございます!",
"questions": [
{
"name": "どこから接続されていますか?",
"type": "single",
"answers": [
"自宅から",
"会議室から",
"オフィスから",
"その他"
]
},
{
"name": "好きなメニューは何ですか?",
"type": "multiple",
"answers": [
"和食",
"中華",
"フレンチ",
"イタリアン",
"ファーストフード",
"その他"
]
}
]
},
{
"title": "午後もがんばっていきましょう!",
"questions": [
{
"name": "運動は何かされてますか?",
"type": "single",
"answers": [
"ランニング",
"筋トレ",
"ヨガ",
"その他"
]
}
]
}
]
}
オプション機能 - 名前
名前をつけて一覧で見やすくすることができます。
既存の投票内容にも名前を入力して[Update]できます。
オプション機能 - 呼び出し
過去に使用したミーティングから投票内容を呼び出せます。
過去のZoomのミーティングIDを入力して、[呼び出し]ボタンを押下します。
投票内容に反映されます。
このまま[add]ボタンから新規追加して、[投票作成]することもできます。
もちろん修正して再利用ができます。
(2) 投票作成
登録した投票が上部一覧に表示されますので選択します。
[投票内容]フィールドに選択した投票内容が表示されます。
ZoomのミーティングIDを[ミーティングID]フィールドに入力して、[投票作成]ボタンを押下します。
[ミーティングID]フィールドが空(ブランク)になったら処理終了です。
Zoomの管理画面を確認してください。
次回以降は、(2) 投票作成の操作のみで、投票作成ができます。
投票内容の編集
登録した投票が上部一覧に表示されますので選択します。
[投票内容]フィールドに選択した投票内容が表示されますので修正します。
[Update]ボタンを押下します。
投票内容の削除
登録した投票の右のゴミ箱アイコンから削除します。
構成サービス
Amazon S3
静的なHTML, CSS, JavaScript、画像を配置して配信しています。
Amazon Cognito
ユーザープールを使って、ユーザーの登録、ログイン認証を行っています。
API GatewayのCognitoオーサライザーを使用してログイン認証していないとAPIが実行できないようにしています。
Amazon DynamoDB
投票内容をユーザーごとに保存しています。
Amazon API Gateway
JavaScriptから呼び出すREST APIをセットアップしています。
それぞれLambdaを呼び出しています。
設定はこちらのSwaggerを参考にしていただけますと幸いです。
(長くなるのでOPTIONSは割愛しています)
swagger: "2.0"
info:
version: "2020-07-12T12:12:55Z"
title: "ZoomPollsAPI"
host: "abcdefghi.execute-api.us-east-2.amazonaws.com"
basePath: "/v1"
schemes:
- "https"
paths:
/polls:
get:
consumes:
- "application/json"
produces:
- "application/json"
responses:
200:
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
security:
- ZoomTool: []
x-amazon-apigateway-integration:
uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:123456789012:function:list_poll/invocations"
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "{\n \"userId\": \"$context.authorizer.claims.sub\"\
\ \n}"
passthroughBehavior: "when_no_templates"
httpMethod: "POST"
contentHandling: "CONVERT_TO_TEXT"
type: "aws"
post:
consumes:
- "application/json"
produces:
- "application/json"
parameters:
- in: "body"
name: "NoteCreateModel"
required: true
schema:
$ref: "#/definitions/NoteCreateModel"
responses:
200:
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
security:
- ZoomTool: []
x-amazon-apigateway-request-validator: "Validate body"
x-amazon-apigateway-integration:
uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:123456789012:function:create_update_poll/invocations"
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "{\r\n \"userId\": \"$context.authorizer.claims.sub\"\
,\r\n \"noteId\": $input.json('$.noteId'),\r\n \"note\": $input.json('$.note'),\r\
\n \"pollname\": $input.json('$.pollname')\r\n}"
passthroughBehavior: "when_no_templates"
httpMethod: "POST"
contentHandling: "CONVERT_TO_TEXT"
type: "aws"
/polls/addpoll:
post:
consumes:
- "application/json"
produces:
- "application/json"
responses:
200:
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
security:
- ZoomTool: []
x-amazon-apigateway-integration:
uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:123456789012:function:add_poll/invocations"
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "{\r\n \"note\": $input.json('$.note'),\r\n \"mtgid\"\
: $input.json('$.mtgid'),\r\n \"company\": \"trainocate\"\r\n}"
passthroughBehavior: "when_no_templates"
httpMethod: "POST"
contentHandling: "CONVERT_TO_TEXT"
type: "aws"
/polls/zoompolls:
get:
consumes:
- "application/json"
produces:
- "application/json"
parameters:
- name: "mtgid"
in: "query"
required: false
type: "string"
responses:
200:
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
security:
- ZoomTool: []
x-amazon-apigateway-integration:
uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:123456789012:function:get_poll/invocations"
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "{\r\n \"mtgid\": \"$input.params(\"mtgid\")\",\r\n\
\ \"company\": \"trainocate\"\r\n}"
passthroughBehavior: "when_no_templates"
httpMethod: "POST"
contentHandling: "CONVERT_TO_TEXT"
type: "aws"
/polls/{id}:
delete:
consumes:
- "application/json"
produces:
- "application/json"
parameters:
- name: "id"
in: "path"
required: true
type: "string"
responses:
200:
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
security:
- ZoomTool: []
x-amazon-apigateway-integration:
uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:123456789012:function:delete_poll/invocations"
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "{\r\n \"userId\": \"$context.authorizer.claims.sub\"\
,\r\n \"noteId\": \"$input.params('id')\"\r\n}"
passthroughBehavior: "when_no_templates"
httpMethod: "POST"
contentHandling: "CONVERT_TO_TEXT"
type: "aws"
securityDefinitions:
ZoomTool:
type: "apiKey"
name: "Authorization"
in: "header"
x-amazon-apigateway-authtype: "cognito_user_pools"
x-amazon-apigateway-authorizer:
providerARNs:
- "arn:aws:cognito-idp:us-east-2:123456789012:userpool/us-east-2_niM1Ff0ZV"
type: "cognito_user_pools"
definitions:
Empty:
type: "object"
title: "Empty Schema"
NoteCreateModel:
type: "object"
required:
- "note"
properties:
note:
type: "string"
noteId:
type: "string"
title: "Note Create Model"
x-amazon-apigateway-request-validators:
Validate body:
validateRequestParameters: false
validateRequestBody: true
AWS Lambda
ランタイムはPython3.8です。
コードを記載します。
ロギング、デバッグコードは割愛します。
DynamoDB PUT
import boto3
from botocore.exceptions import ClientError
def lambda_handler(event, context):
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('zoompoll')
try:
response = table.put_item(
Item={
'userId': event["userId"],
'pollId': event["pollId"],
'poll': event["poll"],
'pollname': event.get('pollname', '')
}
)
except ClientError as e:
print(e.response['Error']['Message'])
else:
if response['ResponseMetadata']['HTTPStatusCode'] == 200:
return event["pollId"]
else:
return ""
DynamoDB Query
import boto3
from boto3.dynamodb.conditions import Key
from botocore.exceptions import ClientError
def lambda_handler(event, context):
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('zoompoll')
try:
response = table.query(KeyConditionExpression=Key("userId").eq(event["userId"]))
except ClientError as e:
print(e.response['Error']['Message'])
else:
return response["Items"]
DynamoDB Delete
import boto3
from botocore.exceptions import ClientError
def lambda_handler(event, context):
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('zoompoll')
try:
response = table.delete_item(
Key={
'userId': event["userId"],
'pollId': event["pollId"]
}
)
except ClientError as e:
print(e.response['Error']['Message'])
else:
if response['ResponseMetadata']['HTTPStatusCode'] == 200:
return event["pollId"]
else:
return ""
Zoom GET
Zoom APIの認証情報は、Secrets Managerで安全に管理しています。
import os
import json
import requests
import boto3
import base64
from botocore.exceptions import ClientError
secret_name = os.environ.get('SECRET_NAME')
def get_secret():
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager'
)
try:
secret_id = secret_name
get_secret_value_response = client.get_secret_value(
SecretId=secret_id
)
except ClientError as e:
raise e
else:
if 'SecretString' in get_secret_value_response:
secret = get_secret_value_response['SecretString']
else:
secret = base64.b64decode(get_secret_value_response['SecretBinary'])
return secret
def lambda_handler(event, context):
try:
secret = json.loads(get_secret())
mtg_id = event['mtgid'].replace('-', '').replace(' ', '')
url = 'https://api.zoom.us/v2/meetings/{meetingId}/polls'.format(
meetingId=mtg_id
)
headers = {
'Authorization': 'Bearer {token}'.format(
token=secret['token']
),
'Accept': 'application/json, application/xml',
'Content-Type': 'application/json'
}
response = requests.get(
url,
headers=headers
)
if response.status_code == 200:
polls = json.loads(response.text)
if ('total_records' in polls):
del polls['total_records']
for poll in polls['polls']:
if ('id' in poll):
del poll['id']
if ('status' in poll):
del poll['status']
return polls
else:
return ''
except Exception as e:
raise e
Zoom POST
import os
import json
import requests
import boto3
import base64
from botocore.exceptions import ClientError
secret_name = os.environ.get('SECRET_NAME')
def get_secret():
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager'
)
try:
get_secret_value_response = client.get_secret_value(
SecretId=secret_name
)
except ClientError as e:
raise e
else:
if 'SecretString' in get_secret_value_response:
secret = get_secret_value_response['SecretString']
else:
secret = base64.b64decode(get_secret_value_response['SecretBinary'])
return secret
def lambda_handler(event, context):
try:
secret = json.loads(get_secret())
mtg_id = event['mtgid'].replace('-', '').replace(' ', '')
polls = json.loads(event['poll'].replace('\n', ''))
url = 'https://api.zoom.us/v2/meetings/{meetingId}/polls'.format(
meetingId=mtg_id
)
headers = {
'Authorization': 'Bearer {token}'.format(
token=secret['token']
),
'Accept': 'application/json, application/xml',
'Content-Type': 'application/json'
}
for poll in polls['polls']:
response = requests.post(
url,
data=json.dumps(poll),
headers=headers
)
except Exception as e:
raise e
トレノケートのAWS研修(AWS認定トレーニング)
トレノケートのAWS認定トレーニングでは、AWS社の厳格なテクニカルスキル及びティーチングスキルチェックに合格した認定トレーナーがコースを担当します。AWS初心者向けの研修や、AWS認定資格を目指す人向けの研修をご提供し、皆様のAWS知識修得のサポートをいたします。
・トレノケートのAWS研修(AWS認定トレーニング)はこちら
▼AWS初心者の方は、 AWS Cloud Practitioner Essentialsから!
座学中心の研修で、AWSを初めて学ぶ方や、営業などで提案に関わる方におすすめです。
「AWS Certified Cloud Practitioner」資格取得を目指す方の基礎知識修得にも最適です。
→ AWS Cloud Practitioner Essentials 詳細・日程はこちらから