Ponz Dev Log

ゆるくてマイペースな開発日記

IBM Cloud AppIDで独自の認証画面を使用する

IBM Cloudマネージドの認証サービス IBM Cloud AppID (以下AppID) には標準で認証画面が作られています。 OpenID Connectのプロトコルで認証を通すだけであれば標準画面で十分なのですが、時として独自の認証画面を使う場面が出てきます。

この記事では、WebアプリケーションとAppIDで認証する時に独自の認証画面を使う方法を記載します。

実装方法

この記事では以下の構成のWebアプリケーションを想定します。

実装方法は驚くほど簡単で、 SDKで提供された WebAppStrategy を実装したエンドポイントに対して、画面からPOSTメソッドでユーザー名とパスワードを投げつけるだけです。

以下のサンプルの通り、実は標準画面との違いはほとんどありません。 独自画面からPOSTでパラメータを投げるか、標準画面に遷移するか。たったこれだけです。

// 認証エンドポイントの定義 サンプル
// Passport.jsやAppIDのSDKの宣言は省略

// 独自画面のログイン処理
app.post(
    '/login/rop',
    bodyParser.urlencoded({ extended: false }),
    passport.authenticate(WebAppStrategy.STRATEGY_NAME, {
        successRedirect: SUCCESS_PAGE_URL,
        failureRedirect: '/login.html',
        failureFlash : true, // 認証失敗時にフラッシュ・メッセージを送り返す
    }),
    (req, res) => {
        console.log('successfully logged in!');
        res.redirect('/success.html');
    });

// 標準画面のログイン処理
app.get('/login/standard', passport.authenticate(WebAppStrategy.STRATEGY_NAME, {
    successRedirect: SUCCESS_PAGE_URL,
    forceLogin: true
}));

// 標準画面のログイン後のコールバック処理
app.get('/login/callback', passport.authenticate(WebAppStrategy.STRATEGY_NAME));
<!-- 独自実装のログイン画面 サンプル -->

<form action="/login/rop" method="post">
  <div>
    <label for="username-form">ユーザー名</label>
    <div>
      <input id="username-form" type="text" name="username" required />
    </div>
  </div>
  <div>
    <label for="password-form">パスワード</label>
    <div>
      <input id="password-form" type="password" name="password" required />
    </div>
  </div>
  <div>
    <button type="submit">ログイン</button>
  </div>
</form>

SDKが裏でやっていること

上記のソースコードでは、独自画面の両方をSDKで処理していましたが、SDKの中ではいったい何をしているのでしょうか。

SDKは独自画面からの認証処理では、OpenID ConnectのResource Owner Password Credential Grant Flowを実装・実行しています。 実態としては、AppIDのトークンエンドポイントを呼び出しているだけです。

トークンエンドポイントは仕様が公開されているので、SDKではなくてもサーバサイドで実装可能です。

参考

技術文章の文章校正をtextlintとGitHub Actionsで快適にする

8月前半に社内で『テックブログの文章校正をCIで快適にする』というタイトルでLTをしました。 もともとは技術同人誌を執筆する時に校正作業を自動化していたこともあり、ブログに応用した例の発表です。 発表から2週間ほど経ってしまったので、忘れないようにセルフ文字起こしです。

文章校正で解決したいこと

まず自分なりに「なぜ文章校正をCIで実行させたかったか」をまとめます。 ブログや技術同人誌のような外向けに文章の校正作業は、軽微な修正が繰り返して面倒です。 たとえば以下の作業が面倒な校正作業です。

  • 読みやすさのためにバラついた文体を整える。

    • 「〜です/ます。」「〜である/なのだ。」「〜やねん。」
  • 固有名詞のタイポや表記ゆれを直す。

    • 「サーバ」なのか、「サーバ」なのか、、、(好みや文脈による)
  • 修正リクエストを反映して上記を繰り返す。

    • 他人の文章と自分の文章の書き方を統一するのは骨が折れる。

上記のように軽微で繰り返し発生する作業は人間がやることじゃないですね。 なるべく執筆に専念したいところです。これが文章校正CIを作りたかったモチベーションです。

textlintとは

上記の文章校正をCIに組み込める最適なツールがtextlintです。

textlintはテキストファイルとMarkdown文章に対して、事前定義したルールに違反する文章を検出する校正ツールです。 Node.jsのパッケージで提供されているので、プログラムから起動するのに適しています。 また、公開されている校正ルールを複数取り込むことで細かい校正ルールを自分で作れます。

今回はtextlintと、私が技術同人誌2冊とブログ執筆で使っている以下の校正ルールを組み合わせます。

  • textlint-ja/textlint-rule-preset-ja-technical-writing

    • 日本語の技術文書を書くためのtextlintのrule-preset
    • e.g. 一文は100文字以下、二重否定は使用しない、半角カナを使わせない、など。
  • textlint-rule/textlint-rule-prh

    • 表記ゆれに強い文章校正ツールprh (proofreading helper)を組み合わせたルールです。
    • textlintと組み合わせて細かい校正ルールを適用できます。

textlintを実行してみる

textlintをCIへ組み込む前に、ローカルマシン上でtextlintを実行して文章校正してみます。 事前にNode.jsをインストールします。

textlint実行の下準備は以下3ステップになります。

  • npmでtextlintとルールをインストールします。

  • お好きな校正ルールをダウンロードして配置します。

  • .textlintrc に使用する校正ルールを定義します。

    • 先にインストールしたルールをオプションとともにファイルに記述します。

これで準備は整いました。npmスクリプトからtextlintを実行してみます。 実際に過去のブログをtextlintにかけてみた例は以下の通りです。ルール違反が一眼で分かりますね。

f:id:accelerk:20200823134629p:plain
textlintをローカルで実行した結果

GitHub Actionsにtextlintを組み込む

textlintも準備できたので、校正CIを構築します。 CIツールは実プロジェクトですとクラウドのCIサービスを使うケースが多いですが、今回はブログ記事をGitHubに集約していたのでGitHub Actionsを採用します。

GitHub Actionsを使用して文章校正CIは以下の流れで実行させます。

  • Markdownで文章を書きます。
  • 編集が終わったらGitリポジトリにcommit&pushします。
  • リポジトリへpushされると、GitHub Actionsのワークフローが起動してtextlintを実行します。
  • 文章にルール違反があればエラーで落ちます。

f:id:accelerk:20200823134656p:plain
GitHub Actionsの校正CIワークフロー

GitHub ActionsのワークフローはYAMLで定義します。 書き方はドキュメントや書籍が出ていますのでそちらを参考にしてください。一例として私が作成したCIワークフローの定義GitHubに上がっています。

ワークフローの定義をコミットした状態でリポジトリソースコードをプッシュすると、ワークフローが実行されます。

試しに校正ルール違反の文章をプッシュした時のワークフロー実行結果は以下の通りです。

f:id:accelerk:20200823134910p:plain
GitHub Actionsの校正CIワークフロー実行結果

無事ルール違反が検出されました。あとはバツがついた箇所を直せばOKです。

まとめ

文章校正はCIで快適にできる。

参考


9月から技術書典9が始まりますね。私も既刊のみですが参加します。 この記事で取り上げた校正CIが、技術同人誌の校正作業につまずいている方の助けになればうれしいです。

techbookfest.org

以上。

RxJSのWebSocket通信ではメッセージにJSONを期待している

TL; DR

  • RxJSの rxjs/webSocket はメッセージ送受信時にデフォルトで JSON.parse / JSON.stringify を実行します。
  • 単純なテキストを受信する場合は、オプションの serializerまたはdeserializerを指定すれば良いです。

遭遇した事象

AngularからWebSocketサーバと通信する場合は、RxJSのrxjs/webSocketを使用すれば簡単に実現できます。たとえばWebSocketでメッセージを受信するServiceクラスは以下のように数行で書けます。便利ですね。

サンプルコード

import { Injectable } from '@angular/core';
import { Subject } from "rxjs";
import { webSocket } from "rxjs/webSocket";

@Injectable({
  providedIn: 'root'
})
export class MessageService {
  connect(): Subject<string> {
    return webSocket({
      url: "ws://127.0.0.1:8000/",
    });
  }
}

しかし、サーバからシンプルなテキスト(たとえば "first message"とか)を受け取るとパースできない...これが今回解決したいことです。

SyntaxError: Unexpected token i in JSON at position 1

解決策

rxjs/webSocketのオプションを定義する WebSocketSubjectConfigを参照すると、ちゃんと書いてありました。たとえば deserializer には以下のような説明があります。(サンプルもあった)

deserializer: A deserializer used for messages arriving on the socket from the server. Defaults to JSON.parse. サーバーから受信するメッセージをデシリアライズする時に使われる関数。デフォルトは JSON.parse です。

渡ってきたテキストをそのまま呼び出し元に返すようにオプション serializer / deserializer にカスタム関数を定義すればOKです。

サンプルコード

import { Injectable } from '@angular/core';
import { Subject } from "rxjs";
import { webSocket } from "rxjs/webSocket";

@Injectable({
  providedIn: 'root'
})
export class MessageService {
  connect(): Subject<string> {
    return webSocket({
      url: "ws://127.0.0.1:8000/",
      deserializer: ({ data }) => data  // ここでカスタムのdeserializer関数を渡す
    });
  }
}

参考

サンプル実装をGitHubに置きました。

github.com


困ったら公式ドキュメントを読もうな。

以上。

AWS Lambdaの処理失敗時に「送信先」と「DLQ」で連携するデータの違い

先日Serverless Meetup Japan Virtual #0に参加してきました。その時にAWS Lambdaの処理失敗時はDLQとは違う送信先を指定できると聞いたので、連携データの違いは何か調べました。

TL; DR

  • Lambdaを非同期で呼び出す場合、処理失敗した後にデータを渡す方法は「通知先(Destination)」と「DLQ (Dead Letter Queue)」の2つ。
  • 通知先はリクエスト・レスポンス、関数などの詳細なデータが連携される。
  • DLQはリクエストの内容のみ連携される。

送信先(Destination)とDLQ

AWS Lambdaは非同期呼び出しで処理成功・失敗時にデータを連携するサービスを指定できます。 この連携先のサービスを「送信先(Destination)」と呼んでいます。送信先がサポートされたのは2019/11と割と最近です。

https://aws.amazon.com/jp/about-aws/whats-new/2019/11/aws-lambda-supports-destinations-for-asynchronous-invocations

送信先としてAmazon SQS、Amazon SNSAmazon EventBridge、別のAWS Lambda関数の4種類のサービスを選択可能です。 また、処理成功と処理失敗で送信先に別のサービスを選択可能というところが柔軟ですね。 送信先に連携されるデータは、リクエスト・レスポンス・関数そのものといった情報が含まれます。

一方でDLQ(Dead Letter Queue)は、送信先と同様に処理失敗時にデータ連携します。連携先のサービスはAmazon SQSとAmazon SNSのみです。 DLQに連携されるデータは、送信先と違ってLambdaのリクエストのみです。 ちなみにDead Letter Queueは失敗時の送信先と同時に設定可能です。

試してみる

この記事の本題です。ユースケースを交えて確認します。

Amazon API Gatewayから必ず失敗するAWS Lambdaを非同期でキックするREST APIを用意して呼び出してみます。 ここでは処理失敗時の送信時にAmazon SQS、DLQに別のSQSを割り当てます。

f:id:accelerk:20200707012315p:plain
Lambda処理失敗時のデータ連携フロー

cURLREST APIを呼びます。

curl -v https://aaaaaaaaaa.example.com/dev/async -d '{ "name": "hoge" }'

この時にDLQのSQSに連携されるメッセージはリクエストボディと同じく {"name": "hoge"} のみです。 エラータイプやリクエストIDといったトレースや最低限のエラーハンドリングに必要な情報は追加情報に付加されています。

一方で送信先のSQSに保存されるメッセージは以下のようになります。 requestContextにはLambdaの関数の情報、requestPayloadAPI Gatewayに渡されたリクエストボディです。responsePayloadにはレスポンスボディ(今回はエラーの情報)が乗っていますね。DLQに連携されるメッセージよりもかなり詳細です。 送信先を利用するメリットとして、送信先のSQSからさらにLambda関数やエラー種別で細かくエラーハンドリングできそうです。

f:id:accelerk:20200707012600p:plain
処理失敗時の送信先にSQS連携されたメッセージ

参考

docs.aws.amazon.com


以上。

IBM Cloudの既存リソースをTerraform管理下に置く

IBM Cloudの既存リソースをTerraform管理下に置く

AWSの既存リソースをTerraformの管理下に入れる記事を見たので、IBM Cloudでも同じことできるだろうと手を動かした時のメモです。

偉大なる元記事

dev.classmethod.jp

TL;DR

  • terraform importコマンドを使えば、IBM Cloud上のリソース作成状況を terraform.tfstate ファイルに落とせる。
  • tfstateをもとにリソース定義のtfファイルを書けばOK。

検証環境

  • macOS Catalina : 10.15.5
  • Terraform : v0.12.26
  • IBM Cloud CLI : 1.1.0+cc908fe-2020-04-29T09:33:25+00:00
  • IBM Cloud Provider plug-in : 1.17.1

※ Terraform as a ServiceのIBM Cloud Schematicsでは未検証です。
IBM Cloud Schematicsを使う例はQiitaの記事が詳しい。

やること

シンプルな例として、作成済のNoSQL DBであるCloudantをTerraformの管理下に置きたい場合を考えます。

(1) ダミーのtfファイルを作成する

最初に中身が空のリソース定義を作成します。

# Provider定義
variable "ibmcloud_api_key" {}
variable "region" {}

provider ibm {
  ibmcloud_api_key = var.ibmcloud_api_key
  region = var.region
}

# Cloudantのインスタンスを作成するリソース定義 (中身空のダミー)
resource "ibm_resource_instance" "db_cloudant" {
}

(2) terraform importでCloud上のリソースの状態をterraform.tfstateに取り込む

ドキュメントのimport コマンドの説明に沿ってIBM Cloud上のリソースの作成状態をローカルファイル (terraform.tfstateファイル) に取り込みます。 インポートする対象リソースのID(CRNも可)をする必要があるので、事前にIBM Cloud CLIでIDを取得しておきましょう。

# カレントディレクトリのTerraformを初期化
$ terraform init

# IBM Cloud CLIでCloudantのインスタンスのIDを取得する。
$ ibmcloud resource service-instance ponz-cloudant --output json | jq -r '.[] | .id'
crn:v1:bluemix:public:cloudantnosqldb:jp-tok:a/123abc:7890abc::

# CloudantのインスタンスのIDを指定してImport
$ terraform import ibm_resource_instance.db_cloudant crn:v1:bluemix:public:cloudantnosqldb:jp-tok:a/123abc:7890abc::

(3) terraform planで必要パラメータと差分を出力

(2)でIBM Cloud上のCloudantの状態を取り込むと、terraform.tfstateファイルの中に以下のようなJSONが出力されます。

{
  "mode": "managed",
  "type": "ibm_resource_instance",
  "name": "db_cloudant",
  "provider": "provider.ibm",
  "instances": [
    {
      "schema_version": 0,
      "attributes": {
        "crn": "crn:v1:bluemix:public:cloudantnosqldb:jp-tok:a/123abc:7890abc::",
        "id": "crn:v1:bluemix:public:cloudantnosqldb:jp-tok:a/123abc:7890abc::",
        "location": "jp-tok",
        "name": "demo-cloudant",
        "parameters": null,
        "plan": "lite",
        "resource_controller_url": "https://cloud.ibm.com/services/",
        "resource_crn": "crn:v1:bluemix:public:cloudantnosqldb:jp-tok:a/123abc:7890abc::",
        "resource_group_id": "abc...",
        "resource_group_name": "",
        "resource_name": "demo-cloudant",
        "resource_status": "active",
        "service": "cloudantnosqldb",
        "service_endpoints": null,
        "status": "active",
        "tags": [
          "env:demo"
        ],
        "timeouts": null
      }
    }
  ]
}

この状態でまずは terraform plan を打ちます。 Error: Missing required argument と必須項目チェックエラーが出力されます。 チェックエラーとなったすべての値を tfstate を参考にしながら埋めていきます。 terraform plan で必須チェックエラーが出なくなるまで定義を埋めると以下のようなtfファイルになるはず。

resource "ibm_resource_instance" "db_cloudant" {
  name = "ponz-cloudant"
  service = "cloudantnosqldb"
  location = var.region
  plan = "lite"
}

これで完成です。まだ差分が出る場合はresourceブロック内をtfstateを参考にさらに埋めればOKです。

IBM Cloud Provider plug-inで作成できるリソース

IBM Cloudのリソースを管理するときに作成できるリソースは2種類に大別されます。 個別設定可能なリソースと、Resource Management resourceという区分で作成できるリソースの2種類です。

前者はAPI GatewayやCloudFoundryアプリケーション、Classic Infrastructureが該当します。これらは専用のリソース定義が用意されているのでこれらを使用すればリソースを簡単に作成できます。
後者は個別定義されていない(ほぼ)すべてのリソースを作成するためのリソース定義です。 NoSQLのCloudantや認証・認可のSaasのAppIDなどは、こちらに分類されます。 ドキュメント上の ibm_resource_instance で作成できないか確認してみましょう。細かい設定は parameters パラメータで指定します。

1点注意すべき点として、Terraform未対応のリソースがあります。 たとえば、IBM Cloud FunctionsはIAMベースの名前空間に存在する関数やトリガを管理できません。(ただし、CloudFoundryベースはサポートされている)
Terraform採用前にどのリソースが管理可能なのかドキュメントの確認をお勧めします。
--> Index of Terraform resources and data sources

terraform importでカバーできないこと

また、Terraformでリソース管理できても terraform import だけでは完全に管理下に置けない場合があります。 最初からTerraformでIBM Cloudのリソースを作成した場合と、既存リソースを terraform import した場合ではtfファイルが異なります。2つの場合を比較すると以下のような注意点がありました。

  • リソースが所属するリソースグループの指定が必要

    • terraform importした状態だと、 resource_group_id が空になります。
    • この状態でも terraform plan は差分なしと判定される。
    • デフォルトのリソースグループの場合は未指定でも良さそうだが、リソースの権限管理上は明示的に指定した方が良い。
  • 認証情報 (Service Key) が紐づくリソースのID指定が必要

    • terraform importした状態だと、 resource_instance_id が空になる。
    • この状態でも terraform plan は差分なしと判定される。
    • 認証情報の紐付けがないため、このままではサービス再作成時に復元できない。
    • 回避策として、tfstateファイルに resource_instance_idを追加し、以下のようにtfファイルにも resource_instance_id を指定する。
resource "ibm_resource_key" "key_db_cloudant_tf" {
  name = "cloudant-credential-tf"
  role = "Writer"
  resource_instance_id = ibm_resource_instance.db_cloudant.id
}

既存リソースをすべて自動でとはいかないものの、Terraform管理下におくことができました。 ちなみにAnsibleにもIBM Cloudのリソースを管理するCollectionがあるようです。 次はAnsibleの学習がてらそちらを触ってみようかな。

以上。

Amazon WorkSpacesのWindows仮想マシンでKH Coderを動かしてテキスト分析する

テキスト分析で使用するKH CoderのインストーラWindows版しかありません。 AWSの仮想デスクトップサービスであるAmazon WorkSpacesでインストール、チュートリアルまでやってみた時の備忘録です。

aws.amazon.com

khcoder.net

WorkSpaceの準備

Amazon WorkSpacesはドキュメントに記載されている通り、WorkSpaceの構築に以下のサービスやリソースの準備が必要です。

  • IAMロールの作成
  • VPCの作成
  • ADディレクトリのセットアップ
  • ADディレクトリにユーザーアカウント追加
  • ユーザーに招待メール送信

今回は準備に時間をかけたくないので初回プロビジョニングには高速セットアップを利用します。 高速セットアップでは、上記のセットアップとWorkSpaceの作成を一手に実行してくれます。 自分がやったときはセットアップ開始から約20分でWorkSpaceが起動して招待メールが届きました。

また、Windows仮想マシンでKH Coderを動かすことが目的ですので、程よいスペックが欲しいところです。 OS / CPU / メモリといったスペックを選択するバンドルには Standard with Windows 10 を選択します。

Bundleには Standard with WIndows 10を選択
Bundleには Standard with WIndows 10を選択

その他のWorkSpaceのセットアップ方法は以下の記事が詳しいです。

qiita.com

仮想デスクトップにログイン

起動済みのWorkSpace Windows仮想マシンにログインするときはWorkSpacesクライアント、またはブラウザからアクセスします。ちなみにブラウザはWindows仮想マシンのみ、Linux仮想マシンは不可です。 私は普段からmacOSを利用していたのでWorkSpacesクライアントを以下のサイトからダウンロードしました。

clients.amazonworkspaces.com

ドキュメントにも記述されていますが、macOS Catalinaを利用されている場合はバージョン3.0.2以上をインストールしましょう。 ググるとたまに古いバージョンをダウンロードさせるページがあるのですが、 その場合はクライアント起動後にアップデートを促すプロンプトが表示されるので指示にしたがってアップデートしましょう。

If you use macOS 10.15 (Catalina), you must use version 2.5.11 or later of the macOS client application. Earlier versions of the client application have issues with keyboard input on Catalina. If you are using Catalina and are working with Linux WorkSpaces, we recommend using version 3.0.2 or later of the macOS client to avoid potential keyboard issues with some applications.

最後に認証コード / ユーザー名 / パスワードの3点をWorkSpacesクライアントに入力すれば無事Windows仮想デスクトップにログインできます。

WorkSpacesクライアントで仮想デスクトップにログインできた
WorkSpacesクライアントで仮想デスクトップにログイン

KH Coderをインストール

KH Coderのダウンロードページからインストーラをダウンロードして起動します。 インストーラの指示に従えばOKです。インストール後はそのまま起動できるかと思いきや...できません。 インストール直後にKH Coderを起動すると必ずDLLが足りないというエラーメッセージが出ます。

MSVCP100.dllが見つからなかったため、アプリケーションを開始できませんでした

これは既知の問題ですので、FAQに沿って必要なコンポーネントをダウンロードします。 https://khcoder.net/FAQ.html#dll_not_found

コンポーネントダウンロード後はWorkSpaceを再起動する必要があるので、AWS ConsoleからWorkSpaceを再起動します。 再起動中は5分程度クライアントからアクセスできないので注意です。

AWS ConsoleからWorkSpaceを再起動する
WorkSpaceの再起動

KH Coderの起動と操作

上記のインストールが完了したらKH Coderを起動できます。 KH Coderのチュートリアルに沿って夏目漱石の『こゝろ』のテキストから共起ネットワークを作成できました。パフォーマンスや描画速度、キーボードインプットのラグはまったく気にならなかったので使いやすいです。

KH Coderの共起ネットワークを描画する
KH Coderの共起ネットワークを描画する


Windowsでしか使えないソフトウェアは、Amazon WorkSpacesの仮想デスクトップを使えば自分のPCを汚さず簡単に実行できそうですね。 Amazon WorkSpacesと同じ仮想デスクトップサービスにMicrosoft AzureのWindows Virtual Desktopというサービスがあります。 まだ調べきれていないのですがこちらも興味深いので、何かの折にそっちと比較してみます。

azure.microsoft.com


以上。

Azureの基本的なリファレンス・アーキテクチャ実装のためのリンク集

GWにMicrosoft Learnを使ってMicrosoft Azureを触っていました。 Azure面白いですね。 今回はリファレンス・アーキテクチャAzure で基本的な Web アプリケーションを実行する を自分で推奨・考慮事項を実装したときに参考にしたリンク集です。 後で何回もググりそうだったので覚え書きです。

docs.microsoft.com

リファレンス・アーキテクチャの推奨・考慮事項

上記のリファレンス・アーキテクチャに記載されている推奨事項と考慮事項を抜き出すと要素は以下の通りです。

  • スケーリング
    • 自動スケールを有効にする
  • 可用性
    • DBのバックアップ
  • 管理容易性
    • リソースのデプロイのプロビジョニングの自動化する
    • アプリケーションのビルド・デプロイの自動化 (CI CD)
    • デプロイスロットを使って安全なデプロイ
    • 構成情報はアプリ構成に記述してアプリソースに記述しない
    • 診断・監視設定を有効にする
  • セキュリティ
    • ネットワークレベルのアクセス制御 (IPアドレス)
    • HTTPSを矯正する (HTTPからHTTPSへのリダイレクト)
    • 認証と承認の構築
    • DBの監査

実装のためのリンク集

※DB周りの部分が整理できていないので、今後追記します。

スケーリング

管理容易性

セキュリティ


リファレンス・アーキテクチャを1つみるだけでも面白いですね。 今後も他のクラウドと比較しながら深掘りしてみたいです。

以上。