そこに仁義はあるのか(仮)

略してそこ仁!

AsanaのWebhookからタスクの追加を検知するbotを作る

タスク管理サービスのAsanaでタスクを追加した時にslackに通知するbotを作りました。
通常は以下の画像のようにAsanaとIntegrationできるアプリがたくさんあるので1からコードを書く必要はほぼ無いのですが、会社Asanaと会社slackにて追加できるアプリにそれぞれ制限があり、今回わざわざコードを書くことになりました。
Webhookをやろうとした時に、ちゃんとセキュリティを担保したいけれどその辺りの情報が全然ない…!ということで困ったので、今後何かAsanaのWebhookでアプリを作る人のお役に立てば幸いです。

f:id:syobochim:20201022005123p:plain:w500


認証用のPersonal Access Tokenを作成する

最初に認証のためのPersonal Access Tokenを作成します。

まず、AsanaのDeveloper App Consoleにアクセスします。
このURL「 https://app.asana.com/0/developer-console 」にアクセスするか、 Asanaでログインした画面の右上にあるプロフィール画像から「My Profile Settings」をクリックし、ポップアップの「Apps」タブから「Manage Developer Apps」をクリックしても同じ画面に遷移します。

「+ New Access Token」をクリックしToken名を入力すると以下のような形式のトークンを取得できます。プログラムやcurlからのアクセスにはこのTokenを使っていきます。 1/1234567890123456:bive3xuos5Ohcavei4yie7ha0Oovaph8

Webhookを登録する

Webhookを登録するにはTaskが登録されるProjectのIDと、イベントが発生した時に情報を送信するURLが必要です。
ProjectのIDはAsanaのURLから確認できます。タスクを登録するプロジェクトのリストやボードを確認した時のURLに含まれている数字がProject IDを示します。
以下の例であれば 1234567890123456 がProject IDです。
https://app.asana.com/0/1234567890123456/board

先ほど取得したAccess Tokenを{access-token}、Project IDを{project-id}、Webhookイベントの送信先URLを{target-url}に置き換えてcurlコマンドを実行します。
action : added でタスクの追加イベントを検知します。addedの他にもchanged removed deleted undeletedなどが指定できるようです。

$ curl -X POST https://app.asana.com/api/1.0/webhooks \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {access-token}' \
  -d '{
  "data": {
    "filters": [
      {
        "action": "added",
        "resource_type": "task"
      }
    ],
    "resource": "{project-id}",
    "target": "https://{target-url}"
  }
}'

Handshakeをする

一番最初にWebhookのイベントが発生すると、Handshake用にX-Hook-SecretをHTTPヘッダーに含めたリクエストがtarget-urlに設定したURLへ送られます。
このリクエストに対して 200 OK もしくは 204 No Content のレスポンスを返すことで登録が完了します。
以下が初期登録時のNode.jsでのAWS Lambdaコードです。2行目でHTTPヘッダーの X-Hook-Secret をログ出力します。
このX-Hook-Secretの値は後ほど使うのでMEMOしておいてください。 また、このコードは初回登録時の1回のみ利用します。

exports.handler = async (event) => {
    console.log('this is secret header ----->' + event.headers['X-Hook-Secret'])
    const response = {
        statusCode: 200,
        headers: {"X-Hook-Secret": event.headers['X-Hook-Secret']},
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};

X-Hook-Secretja0ge6aChaiPhee7ioph7cucoongaht3 のような形式の文字列です。

また、このコードが正常に動作しているかどうかはこちらのサイトを利用すると簡単に確認できます。
https://asana-webhook-tool.herokuapp.com/

Webhookで受け取ったリクエストを検証する

次に、実際にWebhookが送られてきたデータを受け取ります。
先ほど得た X-Hook-Secretを利用することで、target urlへ送られてきたリクエストがAsanaから送られたものであることを確認でき、安全にリクエストを処理できます。
以下はAWS Lambdaのコードです。{X-Hook-Secret} は先ほど得た値に置き換えてください。
AsanaからはX-Hook-Signature ヘッダーを持つリクエストが送られてきます。このX-Hook-Signatureの値はリクエストのbodyをX-Hook-Secretを使ってSHA 256 HMACでHash化したものになります。
Hash化した値とX-Hook-Signatureが同じ値かどうか確認し、同じ値であれば正常なリクエストとして処理します。

var crypto = require("crypto");

exports.handler = async (event) => {
    const signature = event.headers['X-Hook-Signature'];
    const hash = crypto.createHmac('sha256', '{X-Hook-Secret}')
        .update(String(event.body))
        .digest('hex');

    // ヘッダーのシークレットをチェックする
    if (signature != hash) {
        console.error('Calculated digest does not match digest from API. This event is not trusted. : ' + signature);
        return response = {
            statusCode: 401
        };
    }

    // Webhookのデータを処理するコードを書く

    const response = {
        statusCode: 200
    };
    return response;
};

以上で基本的なWebhookの設定は完了です。
ヘッダーのチェックをした後は、HTTPリクエストのbodyを処理していけば安全にWebhookのリクエストを操作できます。

参考リンク

公式ドキュメント

Node.js
Crypto | Node.js v15.0.0 Documentation

Gitの認証がうまくいかなくなった時の対処法

Macを使っていてGitのリポジトリへpushしようとしたら認証エラーが発生しました。
その対処方法のメモです。

remote: HTTP Basic: Access denied
remote: You have 2FA enabled, please use a personal access token for Git over HTTP.
fatal: Authentication failed for 'https://example.com/repo/sample.git/'

パスワードも合っているのに何故…?と思っていたけど、Mac OSX キーチェーンに保存されている認証情報が変な状態になっていることが原因でした。

Gitの設定情報を確認すると、デフォルトで credential.helper が登録されていました。

$ git config --list
credential.helper=osxkeychain
...

GitHubのドキュメントを見ると、以下の条件があるとのこと。確かに当てはまる。

  • osxkeychain 認証情報ヘルパーを利用するには、Git 1.7.10 以降が必要です。
  • Homebrew を使って Git をインストールした場合、osxkeychain helper はインストール済みです。
  • Mac OS X 10.7 以降を実行しており、Apple の Xcode コマンドラインツールで Git をインストールした場合、osxkeychain helper は インストールした Git に含まれています。

Git に GitHub の認証情報をキャッシュする - GitHub Docs

私は特定のリポジトリでこのcredential.helper を上書きしており、そこの設定が osxkeychain に保存されていることで問題が発生していたようでした。

$ git config --list
...
credential.usehttppath=true
credential.helper=!aws codecommit credential-helper $@ 
...

解決方法

解決方法としては、キーチェーンに保存されている情報を削除すれば認証情報が更新されてアクセス可能になりました。
参考 : OSX キーチェーンから認証情報を更新する - GitHub Docs

コマンドラインで削除する方法もありますが、今回はGUIの操作を紹介します。(どちらの方法も上記のGitHubドキュメント見ればわかります。)

まずはキーチェーンアクセスを開きます。Spotlight検索からすぐに開きます。
f:id:syobochim:20200909192334p:plain:w500

右上の検索ボックスで git と検索すると対象のデータが表示されます。
f:id:syobochim:20200909192721p:plain

右クリックして「情報を見る」を選択します。
f:id:syobochim:20200909192853p:plain:w500

「アクセス制御」をクリックし、対象の git-credential-osxkeychain を選択したら下の「ー」をクリックすれば削除が完了します。
f:id:syobochim:20200909193015p:plain:w500

これで再びGitリポジトリへアクセスしたら事象が解決しました。👏

特定のディレクトリ配下のリポジトリに別のGitの設定を適用する

Gitの設定(コミット時に利用する user.name や user.email など)は基本的に git config コマンドを利用して設定をしていきます。
git config にはいくつかのオプションがあり、基本的には --global を使ってPCのユーザーとしての設定をし、個別に設定を変更したいリポジトリについては --local を利用して設定してきました。
しかし、この方法だと毎回設定の手間がかかったり、設定するのを忘れてしまって私用のユーザー名やメールアドレスで会社のリポジトリにコミットしてしまったりと、課題がでてきます。

git config --global コマンドを実行すると ~/.gitconfig に設定が保存されていきますが、Gitにはディレクトリ配下に対して別の config ファイルを適用できます。
今回はそれの備忘MEMOです。

例として、以下のようなディレクトリ構成とします。ここでは ~/dev/project ディレクトリに会社のユーザーを利用したいと想定します。

~/dev
├── study
├── github
└── project
           ├── projectA
           ├── projectB
           └── projectC

まず、 ~/.gitconfig に以下のような設定を追加します。

[user]
    name = syobochim
    email = syobochim@email.com
+ [includeIf "gitdir:~/dev/project/"]
+         path = ~/.gitconfig-project

includeIf に、設定を変更したいディレクトリのパスを書き、 path に適用させたい設定ファイルのパスを書きます。
~/.gitconfig-project は会社用の設定として以下のように記載をします。
ここではuser設定のみ追加していますが、 認証情報の保存をする credential.helper などを追記する場合も多いかと思います。

[user]
    name = syobochim-office
    email = syobochim-office@email.com

そうすると、 ~/dev/project ディレクトリ配下でgitリポジトリを作成したときに、~/.gitconfig-project の設定が適用されます。
git config --list コマンドにて設定を確認してみると、 ~/dev/project ディレクトリ配下のGitリポジトリでは以下のように出力されました。
user.name=syobochim-officeuser.email=syobochim-office@email.com にて設定が反映されているのがわかります。

$ git config --list
credential.helper=osxkeychain
user.name=syobochim
user.email=syobochim@email.com
includeif.gitdir:~/dev/project/.path=~/.gitconfig-project
user.name=syobochim-office
user.email=syobochim-office@email.com
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true

実際にコミットしてみても、会社用のユーザーでコミットされていますね。

$ git log
commit 634ab266a2beee348c1de123569f29c78aff7fde
Author: syobochim-office <syobochim-office@email.com>
Date:   Tue Aug 25 09:09:44 2020 +0900

    test commit

しかし、~/dev/project ディレクトリ以外のGitリポジトリを作成して設定やコミットを実行しても、以下の通り設定は反映されませんでした。

$ git config --list
credential.helper=osxkeychain
user.name=syobochim
user.email=syobochim@email.com
includeif.gitdir:~/dev/project/.path=~/.gitconfig-project
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true
$ git log
commit a7bbb75e5fabd46afb362401d1ab8ace7393f2ae
Author: syobochim <syobochim@email.com>
Date:   Tue Aug 25 09:13:06 2020 +0900

    test commit

ということで、 [includeIf "gitdir:<<path>>"]~/.gitconfig に記載すれば、特定のディレクトリ配下のGit設定を一括で変更できました。

参考
Git - git-config Documentation

Chrome拡張でユーザーからの入力値を利用する

Chrome拡張作りにハマっています。
syobochim.hatenablog.com

Chrome拡張でユーザーからのInputを利用したくなりました。そこで調べたらオプションページを作って入力させられるとのことでした。
オプションページはmanifest.jsonoptions_uiのプロパティを設定して作っていきます。
"open_in_tab": falseで、オプションページをタブではなくポップアップで作成できる。調べてみると、ポップアップが推奨っぽい。
設定した値はストレージに保存したかったので、permissionsstorageも追加しました。

{
...
  "permissions": [
    "activeTab",
    "storage"
  ],
  "options_ui": {
    "page": "option/options.html",
    "open_in_tab": false
  }
}

最初はoptionsページもVue.jsで作ろうと思っていましたが、Chrome拡張のセキュリティポリシーでInline JavaScriptが利用できないらしく、v-model@clickなどを書いていたら警告が出てしまいました。
Content Security Policy (CSP) - Google Chrome

Vue.jsを利用する場合はこういうリポジトリもあったけど、なんとなくbuildしたくなかったので、結果としてプレーンなJavaScriptでファイルを作成することにしました。
GitHub - Kocal/vue-web-extension: 🛠️ A boilerplate for quickly starting a web extension with Vue, webpack 4, ESLint and more!

optionページはこんな感じ。認証情報をユーザーの入力から取得する。
Chrome拡張のセキュリティポリシーによって、scriptはファイルとして外出ししなければならない。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <title>Resources List - Options</title>
</head>

<body>
    api key : <input type="text" id="api_key"><br />
    secret key : <input type="text" id="secret_key"><br />
    <button id="save">Save</button>
    <div id="status"></div>
    <script src="options.js"></script>
</body>
</html>

options.jsの中はこんな感じ。Inilne JavaScriptが使えないからHTMLタグの方にonclickを設定することもできず、addEventListenerを利用している。
データの保存はlocalとsyncがある。どちらが安全なのかは今後調べる予定。

function save_options() {
    chrome.storage.sync.set({
        api_key: document.getElementById('api_key').value,
        secret_key: document.getElementById('secret_key').value
    }, function () {
        var status = document.getElementById('status');
        status.textContent = 'Saved.';
        setTimeout(function () {
            status.textContent = '';
        }, 750);
    })
}

function restore_options() {
    chrome.storage.sync.get({
        api_key: 'input here',
        secret_key: 'input here'

    }, function (items) {
        document.getElementById('api_key').value = items.api_key;
        document.getElementById('secret_key').value = items.secret_key;
    });
}

document.addEventListener('DOMContentLoaded', restore_options);
document.getElementById('save').addEventListener('click', save_options);

設定した値は、メインの方のJavaScriptにてこのように利用可能。設定値を別のプロパティに詰め替えてfunctionの外で利用しようと思っていたけれど、なぜかundefinedになってしまったので制限されているのかな。
なので、メインの方の処理はこのfunctionの中に移動した。

    chrome.storage.sync.get(["api_key", "secret_key"], function (input) {
        input.api_key;
        input.secret_key;
    });

はじめてのGoogle Chrome拡張(とVue.js)

社内サイトのちょっと手の届かないところを触りたくてGoogle Chrome拡張をはじめて作ってみたので、備忘メモ。
ツール自体はかなりサクッと作れました。はじめてのChrome拡張とはじめてのVue.jsだったけど、雑務しつつも2時間くらいで完成できました。
Selectボックスの中の要素を取ってきて、インクリメンタルサーチして、選択したらその値をSelectボックスに設定するという、それだけのツール。

構成ファイル

作ったファイルとフォルダ構成はこんな感じ。
manifest.jsonで読み込むJSファイル(vue.jsmain.js)を指定しています。

.
├── README.md
└── src
    ├── js
    │   ├── main.js
    │   └── vue.js
    └── manifest.json
{
  "manifest_version": 2,
  "name": "tool name",
  "version": "0.0.1",
  "run_at": "document_end",
  "content_scripts": [
    {
      "matches": [
        "https://example.com/roles/*",
        "https://example.com/users/*"
      ],
      "js": [
        "js/vue.js",
        "js/main.js"
      ]
    }
  ],
  "permissions": [
    "activeTab"
  ]
}

Vue.jsはcurlコマンドで格納。これでVue.jsが使える。

$ curl https://cdn.jsdelivr.net/npm/vue/dist/vue.js -o src/js/vue.js

めっちゃ雑なコード。かなりサクッと動かせてよかった。あと、わざわざ別でhtmlファイルを作らなくても画面にフォームなどをお手軽に挿入できて、Chrome拡張のためのVue.jsかと錯覚した。
画面が動的に読み込まれるので、最初に8秒待ったりしている。
拡張を挿入する画面側がjQueryでchangeイベントからHTTPリクエスト投げたりしているんだけど、Elementのvalue変えただけではchangeイベントが発火してくれなかったのでdispatchEvent(new Event("change"));で無理矢理発火させたりしてる。
拡張を更新して試さないとエラー出ないとか辛い…。コンパイルしたい…。

(function () {
    'use strict';
    setTimeout(
        function () {
            var comboboxList = document.querySelectorAll(".combobox")
            var index = 0;

            comboboxList.forEach(
                function (combobox) {
                    combobox.insertAdjacentHTML(
                        "beforebegin",
                        `
                        <style>
                        .selecting-item:hover {
                            background: #e0dfff;
                        }
                        </style>

                        <div id="vue-app-${index}">
                            Policy Filter
                            <input type="text" v-model="policyName">
                            <ul style="list-style-type: none;">
                                <li v-for="item, index in policyList" v-if="policyName && item.text.indexOf(policyName) > -1" 
                                :value="item.value" @click="selectPolicy(item, ${index})" class="selecting-item">
                                    {{ item.text }}
                                </li>
                            </ul>
                        </div>
                        `
                    );

                    var box_options = combobox.options;

                    let vue = new Vue({
                        el: "#vue-app-" + index,
                        data: {
                            policyList: box_options,
                            policyName: ""
                        },
                        methods: {
                            selectPolicy: function (item, index) {
                                combobox.value = item.value;
                                combobox.dispatchEvent(new Event("change"));
                            }
                        }
                    });

                    index++;
                }
            )
        }, 8000)
})();

読み込み方法

公開はしたくなかったので、ローカルのフォルダを読み込ませて動かしました。

1. Google Chromeの「設定」から「拡張機能」をクリック
2. 画面左上の「パッケージ化されていない拡張機能を読み込む」をクリック
3. 「src」フォルダを指定

スキーマの変更をデプロイする方法

【翻訳記事】デプロイ戦略の定義のブログでアプリケーションをデプロイする方法のパターンについて書きました。その記事のはてブにコメントをいただいた(ありがとうございます!)のですが、データベースの変更をした際のデプロイ方法についても気になるというコメントがありました。
そこで、翻訳記事の元になったブログとは違いますが、今読んでいる書籍「進化的アーキテクチャ」にデータベースの変更をデプロイするパターンについて記載されていたので、それを抜粋してブログにまとめます。
と言っても、データベースの変更はアプリケーションほどデプロイ種類があるわけではありません。データはあるか、統合点(後ほど説明します)はあるかという観点で3つのパターンにまとめられています。
そして、【翻訳記事】デプロイ戦略の定義で書いたそれぞれのデプロイ戦略をとる場合、どのケースに当てはめると良さそうなのかも考えました。諸々、私の考えが反映されているので、そうではないプレーンな情報を見たい方は書籍の「5章 進化的データ」をご参照ください。

進化的アーキテクチャ ―絶え間ない変化を支える

進化的アーキテクチャ ―絶え間ない変化を支える

そもそも、今回紹介するスキーマ変更のパターンは、書籍「データベース・リファクタリング」にて説明されているexpand/contractパターンと呼ばれる一般的に利用されているリファクタリングパターンとのことです。

  • まず最初に:データベースの統合点とは
  • expand / contractパターンとは
  • expand / contract パターンの選択肢
    • 選択肢1 : 統合点も従来のデータもない
      • アプリケーションのデプロイ戦略では
    • 選択肢2 : レガシーデータはあるが、統合点はない
      • アプリケーションのデプロイ戦略では
    • 選択肢3 : 既存のデータがあり、統合点もある
      • アプリケーションのデプロイ戦略では
  • まとめ
続きを読む

マイクロサービスではどんな単位でサービスを分割すればいいか

最近、マイクロサービスアーキテクチャについて耳にすることが増えていますが、サーバレスのコンテンツと共に語られているケースもあり、「そのサービス単位では分割しすぎなのでは?」とたまに思います。
もちろん、何が正しいかはその現場のコンテキストによるとは思いますが、書籍「マイクロサービスアーキテクチャ」の第3章にサービスの分割についての考え方が記載されており、しっくりくるとても良い内容だったので、それをまとめてみました。
サービス単位を適度に適切に分割することでマイクロサービスの利点をより享受できるようになると思います。まずは、サービス分割のために重要になる概念を説明し、その後、サービス単位をどのように定めるのか記載していきます。

マイクロサービスアーキテクチャ

マイクロサービスアーキテクチャ

  • 作者:Sam Newman
  • 発売日: 2016/02/26
  • メディア: 単行本(ソフトカバー)

2つの重要な概念

マイクロサービスで優れたサービスを作っていくためには、「疎結合」と「高凝集性」という2つの概念を意識する必要があります。この二つの概念によって関連する振る舞いは一箇所にまとめ、他のサービスとの境界は通信ができる限り「疎」になるよう適切にサービスを分割していきます。「疎結合」ばかりに注目し、「高凝集性」を蔑ろにすると、必要以上にバラバラになったサービス群ができてしまって変化に対するスピードが遅くなってしまうのではと思います。

疎結合

マイクロサービスでは、あるサービスを変更しても他のサービスに影響や変更がなくデプロイできることが重要になります。
サービスはお互いが疎結合であることが求められます。疎結合のサービスは、連携する他のサービスに関して必要最低限のことだけしか把握しません。

高凝集性

機能を変更する際、その変更をできるだけ早くリリースするために、関連する振る舞いは一緒にまとめておく必要があります。
機能変更のために様々な箇所で修正が必要になると、その分多くのサービスをリリースする必要が出てくるのでリスクや変更にかかる時間が大きくなってしまいます。

サービス分割の考え方

例えば経理部門と倉庫を管理している部門があったときに、それぞれの仕事内容は全く異なります。経理部門に所属する人は倉庫の内部作業の知識は必要ありませんし、逆もまた然りです。
他の部署とのやりとりは、在庫報告書や給与明細など、決まったインターフェースを持ちます。

f:id:syobochim:20200430220602j:plain

経理部門は倉庫の詳細な内部作業の知識は必要ありませんが、例えば下の図のように在庫状況を把握して企業評価の帳簿を最新に保つという業務があったときは「在庫品目」の情報が必要になります。
この「在庫品目」が経理部門と倉庫の2つのコンテキスト間の共有モデルになります。ここで注意が必要なのが、他の不要な項目は公開する必要はないということです。内部の情報を共有しないことで、結合が密になるのを防ぎます。

f:id:syobochim:20200430222822p:plain

マイクロサービスでは、「倉庫」と「経理部門」のような、それぞれのコンテキストが分割される箇所と一致するようにサービスを分割していきます。(境界づけられたコンテキスト。)初めは粒度の荒いサービスになるでしょう。そして、そのうち、倉庫のサービスの中を、注文、在庫管理、入荷などの機能に分解できることに気づくと思います。
マイクロサービスの境界を検討する際には、まず大きく粒度の荒いコンテキストの観点で考えて、それからさらに分割する利点を考えていきます。時期尚早にサービス分割してしまうと、サービスリリースの際に多くのサービスの変更が必要になってしまい、結果として変更に関するコストが上がるので注意してください。

機能ごとにサービスを分割した方が効果的であると判断した場合は、それぞれのサービスを他の連携先のマイクロサービスから隠し、入れ子の状態にしておくと、結合も密になりにくく、テストもしやすくなるので効果的です。
ただ、サービスを入れ子にしておくのとトップレベルに切り出すか、入れ子にするかの選択に厳格な規則はありません。
判断する上で優先する事項として、組織構造に合わせてサービスを分割した方が管理がしやすいです。「倉庫」の仕組みを1チームで管理している場合は入れ子に、それぞれのサービス単位でチームがある場合はトップレベルに切り出しましょう。

f:id:syobochim:20200430234608p:plain

最後に、「入れ子の方がテストもしやすくなる」を少しだけ深堀りします。
サービスを入れ子にしておくと、テストのときに各サービスをスタブ化する必要がなくなります。他のサービスと連携するような粗い粒度のAPIだけをスタブ化しておけば十分になるので、テストが簡単になります。
例えば「在庫管理」のサービスをテストしたい場合、データ連携しているサービスが多いと、以下のようにスタブ化しなければならない範囲が増えてしまいます。

f:id:syobochim:20200501005457p:plain

まとめ

もちろん組織やサービスの状況などによって、どのようにサービスを分割するかという選択は様々だと思います。
ただ、「疎結合」ばかりに着目するのではなく、「疎結合」と「高凝集性」という2つの概念をバランスよく考え、組織構造と連携させるようにサービス分割するのが変更速度の高い状態を維持できるマイクロサービスになるのではと思います。
最初に記載しましたが、この内容は書籍「マイクロサービスアーキテクチャ」の第3章により詳細が載っています。マイクロサービスの利点や考慮すべきことが書籍を通して書かれており、とても面白い本なのでオススメです!

マイクロサービスアーキテクチャ

マイクロサービスアーキテクチャ

  • 作者:Sam Newman
  • 発売日: 2016/02/26
  • メディア: 単行本(ソフトカバー)