この記事はぷりぷりあぷりけーしょんず Advent Calendar 2020の10日目を穴埋めする記事です。
朝活をしたいけど寒くて辛い
最近、仕事前の切り替えスイッチにと朝活を始めようと決心したのですが見事に3日坊主で終わりました。
本当にきっかり3日で終わった。ことわざ?凄い!
どんな物事が継続できるか?
こんな私でも今月頭からしっかり継続できていることがあります。
それがFitBoxing2です
毎日30分ほどのキツいコースを可愛い女の子(orイケメン)インストラクターが褒めたり応援してくれたりしながらこなしていくゲームです。
つまり
- 寒い
- 誰も褒めてくれない
- 誰も応援してくれない
- 寒い
大体こんな理由でやめてしまった朝活も可愛い女の子が褒めてくれたり応援してくれたら継続できるような予感がします。
朝活botを作ったよ
Slackに朝活チャンネルを作り、毎朝褒めたり応援したりしてくれるbotを作りました。
- SlackのEventAPIでLambdaを発火
- channelにjoinしたらユーザー登録
- botにメンションしたら朝活記録を登録
- channelから去ったらユーザー削除
- CloudWatch Eventsで毎朝応援通知
データの格納先はDynamoDB、AWSの各サービスはCDKで管理していきます。
bot
日本のKAWAIIの代表といえば、そうこの方ですね
Slack Appからアプリを作成しBotイメージに上記の写真を設定し、この辺りの権限を付与しておいてあげます。
CDK
AWS CDKで各サービスを定義していきます。
Lambda
今回は弊コミュニティの素晴らしいエンジニアが記事にしていたNodejsFunctionを使ってLambdaを定義します。
const handler = new NodejsFunction(this, 'asakatsu-handler', {
functionName: `${appName}-handler`,
timeout: Duration.seconds(10),
runtime: lambda.Runtime.NODEJS_12_X,
initialPolicy: [
new iam.PolicyStatement({
actions: ['dynamodb:*'],
effect: iam.Effect.ALLOW,
resources: [
Stack.of(this).formatArn({
service: 'dynamodb',
resource: 'table',
resourceName: 'Asakatsu*'
})
]
}),
new iam.PolicyStatement({
actions: ["ssm:GetParameter"],
effect: iam.Effect.ALLOW,
resources: [
Stack.of(this).formatArn({
service: "ssm",
resource: "parameter",
resourceName: "asakatsu-bot/*",
}),
],
}),
]
});
NodejsFunctionで定義するとランタイムがなぜかデフォルトでNODEJS_10_X
になってしまうので最新のNODEJS_12_X
を指定しておきます。
今までは定義した関数のファイルの場所を指定しなければなりませんでしたが
(experimental) Path to the entry file (JavaScript or TypeScript).
If the NodejsFunction is defined in stack.ts with my-handler as id (new NodejsFunction(this, 'my-handler')), the construct will look at stack.my-handler.ts and stack.my-handler.js.
とのことなのでasakatsu-bot-stack.asakatsu-handler.ts
という少々長ったらしいファイルではありますがそこにLambda関数を用意してあげることにします。
また、最新のNodejsFunctionはesbuildを使ってビルドを行なっていますがローカルにesbuildがないととんでもないサイズのdockerイメージでビルドされてしまうので予めnpm install --save-dev esbuild
しておきます。
$ docker image list
REPOSITORY TAG IMAGE ID CREATED SIZE
cdk-baf5f3dcfbe97271b31c2605b638d0610712bf0d176333d75863abaeb2ec6ddb latest e51cd2a7cb5e 4 days ago 1.9GB
amazon/aws-sam-cli-build-image-nodejs12.x latest 2eada5d9f2c4 2 weeks ago 1.87GB
DynamoDB
続いてDynamoDBの定義です。
今回NodejsFunctionの恩恵でLambdaのhandlerと一緒にビルドしてもらえることになったのでここに定義したテーブルの仕様をCDKとhandlerで二重管理しなくて良いようになりました。
import * as cdk from '@aws-cdk/core';
import {RemovalPolicy} from '@aws-cdk/core';
import * as dynamodb from '@aws-cdk/aws-dynamodb';
import {AttributeType} from '@aws-cdk/aws-dynamodb';
import {TableOptions} from '@aws-cdk/aws-dynamodb/lib/table';
interface TableProps extends TableOptions {
readonly tableName: string;
}
interface DynamoDBTable {
readonly id: string;
readonly tableProps: TableProps;
}
export const User: DynamoDBTable = {
id: 'asakatsu-user',
tableProps: {
tableName: 'AsakatsuUser',
partitionKey: {
name: 'id',
type: AttributeType.STRING
},
removalPolicy: RemovalPolicy.DESTROY
}
}
export const Achievement: DynamoDBTable = {
id: 'asakatsu-achievement',
tableProps: {
tableName: 'AsakatsuAchievement',
partitionKey: {
name: 'date',
type: AttributeType.STRING
},
sortKey: {
name: 'user_id',
type: AttributeType.STRING
},
removalPolicy: RemovalPolicy.DESTROY
},
}
export function createTable(stack: cdk.Stack): void {
const tables: DynamoDBTable[] = [User, Achievement];
tables.forEach(table => {
new dynamodb.Table(stack, table.id, table.tableProps);
});
}
例えばユーザー登録でDynamoDBを呼び出すときはここで定義しているtableName
をそのまま使うことができます。
(おそらくもっとちゃんとテーブル定義を作り込むことはできそうですが今回はざっくりこんな感じで)
import {DB} from './common';
import {User} from '../dynamodb';
export async function register(userId: string): Promise<void> {
try {
await DB.put({
TableName: User.tableProps.tableName,
Item: {
id: userId
}
}).promise();
} catch (e) {
throw e;
}
}
その他
その他APIGatewayやCloudWatchEventsなどの定義ですが特筆する点もないのでざっくりこんな感じなんだなーと見てもらえればと思います。
// cloud watch events
const lambdaTarget = new eventsTarget.LambdaFunction(handler);
new events.Rule(this, `${appName}-event`, {
ruleName: `${appName}`,
targets: [lambdaTarget],
schedule: events.Schedule.cron({
minute: '30',
hour: '21',
})
});
// api gateway
const api = new apigateway.RestApi(this, `${appName}-gw`, {
restApiName: `${appName}-gw`
});
const integration = new apigateway.LambdaIntegration(handler, {
proxy: true
});
api.root.addResource(`${appName}`)
.addMethod('POST', integration);
EventHookされるスクリプト
上述通り、今回はAWSLambdaがEvent発火をトリガーに実行されます。
LambdaはNodejsFunctionの定義の簡易さの恩恵を最大に活かすため、コントローラとなるhandlerだけの定義を中心に以下のようなdir構成にしました。
.
├── asakatsu-bot-stack.asakatsu-handler.ts
├── asakatsu-bot-stack.ts
├── dynamodb.ts
└── lambda
├── common.ts
├── index.ts
├── notify-handler.ts
├── register-handler.ts
├── remove-handler.ts
└── report-handler.ts
import {
Response,
SUCCESS_RESPONSE,
report,
register,
remove, notify,
} from './lambda';
function challenge(challenge: string): Response {
return {
'isBase64Encoded': false,
'statusCode': 200,
'headers': {'Content-type': 'application/json'},
'body': JSON.stringify({'challenge': challenge})
};
}
export async function handler(event: any): Promise<Response> {
if (event.source == 'aws.events') {
await notify();
return SUCCESS_RESPONSE;
}
const body = JSON.parse(event.body);
if (body.challenge) {
return challenge(body.challenge);
}
switch (body.event.type) {
case 'member_joined_channel':
await register(body.event.user);
break;
case 'member_left_channel':
await remove(body.event.user);
break;
case 'app_mention':
await report(body.event.user, body.event.text);
break;
}
return SUCCESS_RESPONSE;
}
Lambda実行時に受け取るevent
に合わせて呼び出すhandlerを指定するようにしました。
小さなAPIを作るのであれば管理が統一されるのとイベントが増えるたびに上記のhandlerに追加していくだけで良いので楽そうです。
各handlerの詳細についてはDynamoDBにストアする、Slackで応援メッセージを通知するなどのロジックになるので今回は割愛します。
完成したよ。
cdk deploy
をし、SlackAppの設定をworkspaseのchannelにしたら完成です。
できました。これで明日から毎朝ぱみゅぱみゅが応援してくれるのでどんなに寒くて辛くてもお布団から脱出し朝活ができそうです。好きな人に嘘なんてつけないですからね。