はじめに
興味のあるトピックについてキーワードを登録しておくと勉強会支援プラットフォームであるconnpassで関連するイベントが開催された際にプッシュ通知を受け取ることのできるChrome拡張をリリースしました!
🎉 Chrome拡張リリースしました
— こびゃーし (@k0bya4) 2019年10月14日
気になるトピックを登録しておくとconnpassで関連するイベントが登録された際に通知を受け取れます!
イベントの先着順に間に合わなかったり、おもしろいイベントを終わってから知ったなんて経験のあるかたはぜひ一度お試しくださいhttps://t.co/E3YHk2oFZX pic.twitter.com/UrGGQl57U5
- エンジニアをつなぐ IT勉強会支援プラットフォーム connpassさんのAPIを使用させていただきました
以下、今回NotiConnを作成した際にやったことについてまとめていきます
構成
バックエンドでやったこと
バックエンドを用意した理由
今回の要件だとクライアント側でconnpassのapiを叩きつつ、前回との差分をlocalStorageで管理すればバックエンドは必要ないのですが、あえて用意した理由として2つあります
connpassのAPI利用による制限
connpassのAPIリファレンスにこのような注意書きが
※過度な検索やクローリングに対しては、アクセス制限を施す可能性があります。robots.txt を遵守してください。
各クライアントが一時間ごとにconnpassへリクエストさせると引っかかってしまう恐れがありました
- 地域やキーワード等で通知させるイベントを絞りたかった この条件に関してはバックエンドが絶対に必要ということはありませんが、定石としてクライアントでやるには重いので用意しました
アーキテクチャについて
今回はマネタイズができないということで、最安価格で実装する というコンセプトで構築していきました。そのためDBを使わずにS3を使用したり、EC2を利用するのではなくLambdaを使用しました
それぞれの役割は、connpassへ最新イベント100件を取得するcronを用意しつつ、S3に保存している最新のイベントIDよりも新しいイベント群をS3に上書き保存しています
クライアントからのリクエスト時には、含まれる指定キーワード、指定地域に該当するイベントを検索して返しています
Lambdaでの開発サイクル
- CI・CD環境
CircleCIからLambdaにデプロイする環境をざっくりと説明します。 develop pushされるとdev環境へデプロイできるapprovalジョブが、master pushでプロダクション環境へデプロイ出来るapprovalジョブが実行できる流れとなっています
workflows:
version: 2
approval-deploy-with-serverless:
jobs:
- setup
- test:
requires:
- setup
- request-deploy:
type: approval # approvalを設定したjob
filters:
branches:
only:
- master
- develop
requires:
- test
- deploy-prd:
filters:
branches:
only: master
requires:
- request-deploy
- deploy-dev:
filters:
branches:
only: develop
requires:
- request-deploy
ここで走っているdeploy-dev deploy-prd jobはcredentialを設定した上で事前にMakefileにまとめられている以下のコードを実行しています
.PHONY: deploy-dev
deploy-dev:
yarn sls deploy
.PHONY: deploy-prd
deploy-prd:
yarn sls deploy --stage=prd
- 環境変数の取り扱い
Lambdaの場合、環境変数はserverless.yamlをもとに埋め込まれるので、serverless.yamlに使用する環境変数を含めなければいけませんが、直接書いてしまうのはやりたくないので、秘密情報は./serverless/secret
ディレクトリを作成し、そこにyamlで書いた上でserverless.yamlにそのyamlファイルを読み込んでもらうようにしました
custom:
defaultStage: dev
profiles:
dev: noticonn-dev
prd: noticonn
env:
dev: ${file(./serverless/env/dev.yml)}
prd: ${file(./serverless/env/prd.yml)}
functions:
save:
dev: ${file(./serverless/functions/save/dev.yml)}
prd: ${file(./serverless/functions/save/prd.yml)}
secret:
slack: ${file(./serverless/secret/slack.yml)}
map: ${file(./serverless/secret/map.yml)}
ここで環境変数やkeyのファイルの場所を定義して
provider:
environment:
BUCKET: ${self:custom.env.${self:provider.stage}.BUCKET}
EVENT_FILE: ${self:custom.env.${self:provider.stage}.EVENT_FILE}
SINCE_ID_FILE: ${self:custom.env.${self:provider.stage}.SINCE_ID_FILE}
ENV: ${self:custom.env.${self:provider.stage}.ENV}
HOOKS_URL: ${self:custom.secret.slack.HOOKS_URL}
MAP_API_KEY: ${self:custom.secret.map.MAP_API_KEY}
CircleCIの設定
- ジョブでワークススペースを共有する
ブランチによって, 実行されるジョブを変えるために, workflowを用いていますが, workflowは各ジョブごとにworkspaceを持つように実行されるため, 必要な場合, 毎回bundle installや, npm install を実行する必要があります
当然, 毎回実行されるのは時間もかかりますし, 今回の構成の場合, dockerのbuildなどは必要ないため, 一度npm installを走らせれば, testの実行からdeployまでジョブを走らせることができます
そこで試してみたことが persist_to_workspace です これは, 後続のジョブに指定したディレクトリのファイルを共有する機能です. この機能を使用して, npm installが完了したディレクトリを共有することで, 一度だけ走るように調整しています
# ディレクトリを共有する場合
- persist_to_workspace:
root: . # working_directoryに対する絶対パス
- . # 共有するパス
# 共有されたディレクトリを使用する場合
- attach_workspace: # workspaceをアタッチする
at: .
- awsのクレデンシャルを通す
serverlessでprofileで環境を指定してデプロイする場合, .aws/configのデフォルト設定は読み込まれません(そんな設定はないとエラーになる). そのため, 以下のように, profileが明示されたconfigファイルが必要となります
[noticonn]
aws_access_key_id = # production環境のアクセスキー
aws_secret_access_key = # production環境のシークレット
[noticonn-dev]
aws_access_key_id = # dev環境のアクセスキー
aws_secret_access_key = # dev環境のシークレット
もちろん, CircleCiが自動で設定されることはないので, このファイルを作成するスクリプトを作成して, 走らせます
スクリプト
mkdir -p ~/.aws
touch ~/.aws/credentials
echo "[$1]\naws_access_key_id = $AWS_ACCESS_KEY_ID\naws_secret_access_key = $AWS_SECRET_ACCESS_KEY" > ~/.aws/credentials
ジョブ
- run:
name: set-credentials
command: sh ./serverless/script/credentials.sh noticonn-dev
- serverlessで環境変数を扱うために, sercretディレクトリ以下にyamlを作成する
先の項目で, 秘密情報は./serverless/secret
ディレクトリ以下に配置すると書きましたが, この秘密情報はCicleCIのEnvironment Variablesを用いて管理しています… が, awsのクレデンシャルと同じく, これもyamlをスクリプトを用いて作成する必要があります(yamlに環境変数を読み込む機能はないため)
スクリプト
touch ./serverless/secret/map.yml
echo "MAP_API_KEY: $MAP_API_KEY" > ./serverless/secret/map.yml
ジョブ
- run:
name: create-config-map
command: sh ./serverless/script/map.sh
serverless framwork + CircleCiでデプロイ環境を作るためのジョブは最終的こんな形になりました
jobs:
setup:
working_directory: ~/workspace
docker:
- image: circleci/node:10.12.0
steps:
- checkout # ソースコードを作業ディレクトリにチェックアウトする特別なステップ
- run:
name: update-npm
command: 'sudo npm install -g npm@latest'
- restore_cache: # 依存関係キャッシュを復元する特別なステップ
# 依存関係キャッシュについては https://circleci.com/docs/ja/2.0/caching/ をお読みください
key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: install-npm
command: npm install
- run:
name: install-yarn
command: npm install yarn
- save_cache: # 依存関係キャッシュを保存する特別なステップ
key: dependency-cache-{{ checksum "package-lock.json" }}
paths:
- ./node_modules
- run:
name: create-config-slack
command: sh ./serverless/script/slack.sh
- run:
name: create-config-map
command: sh ./serverless/script/map.sh
- persist_to_workspace:
root: . # workspaceのrootパス
paths:
- . # 共有するパス
test:
working_directory: ~/workspace
docker:
- image: circleci/node:10.12.0
steps:
- attach_workspace: # workspaceをアタッチする
at: .
- run:
name: test
command: npm run test
deploy-prd:
working_directory: ~/workspace
docker:
- image: circleci/node:10.12.0
steps:
- attach_workspace: # workspaceをアタッチする
at: .
- run:
name: set-credentials
command: sh ./serverless/script/credentials.sh noticonn
- run:
name: deploy-prd
command: make deploy-prd
deploy-dev:
working_directory: ~/workspace
docker:
- image: circleci/node:10.12.0
steps:
- attach_workspace: # workspaceをアタッチする
at: .
- run:
name: set-credentials
command: sh ./serverless/script/credentials.sh noticonn-dev
- run:
name: deploy-dev
command: make deploy-dev
フロントエンドでやったこと
拡張機能のフロントはTypeScriptとReactで実装しました
create-react-appを使用してChrome拡張の開発を始める
create-react-appを使用してChrome拡張の開発を始めるまでの手順は以前書いた記事がありますので参照してみてください
実装について数カ所ピックアップして解説します
ユーザの入力したトピックをlocalStorageに保存
今回はブラウザ上のアイコンクリックでポップする画面から興味のあるトピックを登録します
トピックは
- テキストボックスから登録
- 各トピックの x ボタン押下でトピック削除
可能なものを作成しました
トピックはlocalStorageに保存しました
アプリケーションではオブジェクトとして扱い、保存時にJSON文字列に変換しています
読み込み時
const [topics, setTopic] = useState<Topics>(
JSON.parse(localStorage.getItem(TOPICS_STORAGE_KEY))
);
保存時
localStorage.setItem(TOPICS_STORAGE_KEY, JSON.stringify(topics));
保存/削除のクリックイベントでオブジェクトの更新とlocalStorageへの保存を行いました
定期的にイベント取得APIを実行する
イベント一覧は定期的に最新の状態になるのでイベントの取得を行うAPIをそのタイミングに合わせて叩きます
Chrome拡張で定期的に処理を実行させるには alarms APIを使用します
毎時15分にイベントの取得を行います
const when = new Date().setHours(0, 15, 0, 0);
const periodInMinutes = 60;
const alarmName = "fetchEvents";
chrome.alarms.create(alarmName, { when, periodInMinutes });
chrome.alarms.onAlarm.addListener(alarm => {
if (alarm.name === alarmName) {
const topics = Object.keys(JSON.parse(localStorage.getItem(TOPICS_STORAGE_KEY)));
if (topics.length === 0) {
return;
}
fetchEvents(topics);
}
});
トピックにマッチしたイベントをChromeのプッシュ通知として表示させる
Chrome拡張からの通知には notifications APIを使用します
今回はイベントのURLを通知のIDとして利用することでクリック時にconnpassのイベントへリンクさせました
プッシュ通知の作成
const pushNotification = (event: Event) => {
const options = {
iconUrl: "icon128.png",
type: "basic",
title: `NotiConn`,
message: `${event.topic}に関連するイベントが公開されました\n${event.title} by ${event.owner}\n${event.url}`
};
chrome.notifications.create(event.url, options);
};
通知をクリックした際の挙動はイベントリスナーで定義できます
通知のクリック時の処理
chrome.notifications.onClicked.addListener(notificationId => {
chrome.tabs.create({ url: notificationId });
chrome.notifications.clear(notificationId);
});
まとめ
以上、初めてのChrome拡張開発でしたがReactとServerless Frameworkをメインにリーズナブルにサービスを実現できました
ぜひ、インストールして使用してみてください!
とりとめない所感など
アーキテクチャの反省点
- lambdaの同時実行制限
- 1000まで
- 現状のユーザ数では問題ないが同タイミングで全クライアントがリクエストを行う仕様を見直す必要もあるかも
- https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/limits.html
- Dynamoで良かったかも
- 保存するデータが大きくなる想定で節約のためにS3をデータストアとして利用したが、この規模感であればDynamoDBでも金額は変わらない
chrome拡張の仕様
- Chrome拡張の次期バージョンをストアに公開しても即時反映はされず同期的にクライアントをバージョンアップさせることができない
- APIとクライアントの実装が同期している必要があれば考慮が必要
- https://developer.chrome.com/apps/autoupdate
-
tabsが要らなかった話
- 新規にタブを開くのにはtabs権限はいらない
- tabs権限を要求してしまうとインストール時
閲覧履歴の読み取り
が表示されるので心象よくないかも
現在のユーザ数
運用してみてのAWSの料金
メンバー
以下のメンバーで開発・執筆しました