LoginSignup
20

More than 3 years have passed since last update.

React × Serverless Framework でキーワードにマッチするconnpass のイベントを通知するChrome拡張を開発した話

Last updated at Posted at 2019-11-03

はじめに

興味のあるトピックについてキーワードを登録しておくと勉強会支援プラットフォームであるconnpassで関連するイベントが開催された際にプッシュ通知を受け取ることのできるChrome拡張をリリースしました!

以下、今回NotiConnを作成した際にやったことについてまとめていきます

構成

  • connpassのAPIを定期的に実行
  • イベントをJSON形式でS3に保存
  • イベントのフィルタと取得を行うAPIを用意してChrome拡張から定期的に実行 s_12530FC0C6FE5E723C939D257CFB7C7E1D83D7DF49C7EA1087A7A61ADFBD0958_1572761125239_noticonn+4.png

バックエンドでやったこと

バックエンドを用意した理由

今回の要件だとクライアント側で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に保存

今回はブラウザ上のアイコンクリックでポップする画面から興味のあるトピックを登録します

スクリーンショット 2019-10-20 20.37.43.png

トピックは

  • テキストボックスから登録
  • 各トピックの 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のプッシュ通知として表示させる

スクリーンショット 2019-10-12 14.50.05.png

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の同時実行制限
  • Dynamoで良かったかも
    • 保存するデータが大きくなる想定で節約のためにS3をデータストアとして利用したが、この規模感であればDynamoDBでも金額は変わらない

chrome拡張の仕様

現在のユーザ数

50人到達しました 🎉
image (1).png

運用してみてのAWSの料金

image (2).png

メンバー

以下のメンバーで開発・執筆しました

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20