echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Serverless Framework】Serverless Frameworkで外部ファイルの値を読み込み(YAML/JSON/JavaScript)

概要

Serverless Frameworkでは、serverless.ymlだけでなく、外部ファイルに定義を設定して読み込むことも可能です。 そのやり方についてです。

なお、読み込み可能なファイルは、以下の通りです。

serverless.com

YAMLJSONファイルの読み込み

YAMLJSONなど「キー:値」形式のファイルは、以下の方法で読み込みます。

※以下はYAMLで読み込んでいますが、JSONも(構造さえ気をつければ)同じです。

# settings.ymlに以下を定義する
hoge: hogehoge
fuga: fugafuga
# serverless.ymlで上記settings.jsonの値を読み込む
functions:
  exampleFunc:
    handler: exampleFunc.handler
    name: exampleFunc
    tags:
      hoge: ${file(./settings.yml).hoge}
      fuga: ${file(./settings.yml).fuga}

上記のように、外部ファイルの値を読み込みたいキーに対して、値を

${file(外部ファイルのパス).値を読み込みたいキー}

と設定すれば、外部ファイルの値を読み込んでくれます。
※上の例の場合、Lambda関数「exampleFunc」のタグに、

  • 名前:hoge, 値:hogehoge
  • 名前:fuga, 値:fugafuga

という2つのタグが登録されます。

また、上記のように「複数のキーを読み込む」場合、「custom」セクションの独自変数をうまく使うことで、読み込み回数を1回にできます。(下記参照)

# 独自変数「settings」に、settings.ymlの定義を格納する。
custom:
  settings: ${file(./settings.yml)}

# 実際に値を参照する場合、上記のcustom.settingsの各キーを参照すれば、
# キーの値を読み込みできる。
functions:
  exampleFunc:
    handler: exampleFunc.handler
    name: exampleFunc
    tags:
      hoge: ${self:custom.settings.hoge}
      fuga: ${self:custom.settings.fuga}
使いどころ

使いどころとしては、例えば下記ケースです。

  • 条件により、設定値を変更したい
    • 環境(開発/本番) など
  • 各環境の設定値自体は不変(Auroraの接続情報 など)

上記の場合、まず下記のようにフォルダ&ファイルを構成します。

【注意】
 ・ [Root]はserverless.ymlがあるフォルダ
 ・ 各「settings.yml」には環境ごとの定義がされている

  • [Root]
    • [dev]
      • settings.yml
    • [prod]
      • settings.yml

次に、serverless.ymlのcustom.settingsを下記のように設定する。
※${opt:env}は「CLIコマンドの引数envの値を参照する」という、
Serverless Frameworkの独自変数

# 独自変数「settings」に、settings.ymlの定義を格納する。
custom:
  settings: ${file(./${opt:env}/settings.yml)}

最後に、デプロイ時のCLIコマンドで、引数「env」に環境に対応したフォルダ名を指定すればOKです。

> sls deploy --env dev

JavaScriptファイルの読み込み

JavaScriptファイルの読み込み(=関数の戻り値を設定)も、大きな違いはないです。
定義としては、下記の書式となります。

キー: ${file(読み込みjsファイルのパス):戻り値を参照する関数名}

# 例:タグ「hoge」に、settings.jsのgetHoge()関数の戻り値  
# (=デプロイ時の日時情報)を格納する。
functions:
  exampleFunc:
    handler: exampleFunc.handler
    name: exampleFunc
    tags:
      hoge: ${file(./settings.js):getHoge}
// settings.jsでは、「module.exports.関数名」で関数を定義し、
// 設定したい値をreturnすればOK
module.exports.getHoge = () => {
    return new Date().toString();
};

なお、

  • 関数に引数を渡すことはできない
  • 戻り値をJavaScriptオブジェクトにして、そのキーを参照することは可能

のようです。

使いどころ

使いどころとしては、例えば下記ケースです。

  • 毎回値が変わる情報を設定したい
    • デプロイ日時情報など(上記の例のように)
  • serverless.ymlに、生の設定値を書きたくない。
    • Auroraの接続パスワード、外部APIのアクセストークンなどの重要情報
    • 本来、SecretManagerで管理するような情報

上記の場合、まずserverless.ymlでは上記同様、JavaScriptファイルの戻り値を設定するように記載して...

functions:
  exampleFunc:
    handler: exampleFunc.handler
    name: exampleFunc
    tags:
      token : ${file(./token.js):getToken}

実際のJavaScriptでは、生の設定値を記載しなくてよいような処理を施しておけばOKです。(例えば下記のような)

// token.jsの内容
module.exports.getToken = () => {
  
    // crypto-jsは暗号化&複合化を行うライブラリ
    const cryptoJs = require("crypto-js");
    const salt = 'set password salt phrase';
  
    // 「encryptedText」には、「tokenに設定する値を暗号化した値」を  
    // 設定する。
    const encryptedText = "encryptedtoken";
  
    // returnする値(=tokenに設定する値)は、  
    //「encryptedTextを復号化した値」を指定する。
    const decryptedText = cryptoJs.AES.decrypt(encryptedText, salt).toString(cryptoJs.enc.Utf8);
  
    return decryptedText;
};

こうすれば、生の設定値を書かなくて済む分、良くなると思います。

※もちろん下記ケースがあるとばれる可能性があるので、そこは注意です。
(.gitignoreに追加するなど、何かしら対応が必要です)

  • token.jsの内容(暗号化した値、復号化処理の内容など)がばれる
  • token.jsをgitHub等で公開している

まとめ

と、久々にServerless Frameworkに関する情報でした。

実は前回から、Serverless Frameworkに限らず、AWS/IaC/クラウドなどのナレッジがかなりたまってきたので、少しずづ公開したいなあと考えています。

また、近々イベントで登壇するかもしれないので、確定したら合わせて公開します。

てか、もう少し定期的にブログを公開したいなあ。

それでは、また次回。

【JavaScript】「とりあえずawait」は卒業しよう

概要

先日、非同期処理に関して、下記のようなツイートをしました。

このツイートに対して、思った以上に反響を頂いた(と思う)ので、今回もう一度async/awaitのことを記事にしました。

ちなみに前回書いたasync/await関連の記事はこちら。
makky12.hatenablog.com

なお今回の記事を書くにあたり、下記ブログを参考にしました。
(ブログを書こうと思ったきっかけもこれを見たから)

「メソッド実行とawaitの分離」の書き方など、役立つソースが多いです。

前提:async/awaitの基本

async/awaitですが、ざっくり書くと

Promiseによる非同期処理を、同期処理っぽく書ける仕組み

です。

これを利用することで、例えば下記のメリットがあります。

  • 同期処理の様にコードが書けるので、ソースが直感的にわかりやすい。
  • Promiseの記載が不要なので、ソースがシンプルになる
    • Promise.then()ネスト(≒コールバック地獄)が不要になる
    • returnが最後に来るので、処理の流れや戻り値が把握しやすい
// 例えば、Promiseだけの場合、非同期処理はこう書くけど...  
// ※axios.get()は、WEB APIなどにgetメソッドでリクエストを送る非同期処理  
function sample() {  
  
    return new Promise(resolve () => {
        axios.get()
        .then((a) => {
            axios.get(a)
            .then((b) => {
                axios.get(b)
                .then((c) => {
                    resolve(c);
                });
            });
        });
    });
}
  
// async/awaitだと、こんな感じでシンプルに書ける。
async function sample() {
  
    const a = await axios.get();
    const b = await axios.get(a);
    const c = await axios.get(b);
  
    return c;

}

本題

で、元の僕のツイートにある

  • 非同期処理A〜Cの3つ
  • 全て成功した場合のみ処理Dを実行
  • A〜Cは依存関係なし

という処理を書く場合、ついつい「とりあえず生中」ならぬ「とりあえずawait」な感じで、条件反射的に下記ソースを書いてしまいがちです。

//  ダメな例
async function sample() {
  
    const a = await axios.get("./a");
    const b = await axios.get("./b");
    const c = await axios.get("./c");
  
    const d = funcD(a, b, c);
    return d;
  
}

が、これだと処理の流れは

  1. 処理Aのリクエストを送り、レスポンス取得まで処理B以降は実施しない
  2. 処理Bのリクエストを送り、レスポンス取得まで処理C以降は実施しない
  3. 処理Cのリクエストを送り、レスポンス取得まで処理Dは実施しない
  4. 処理Dを実施

と直列処理になり結果として処理時間は「A + B + C + D」となってしまいます。

今回の場合、処理A~Cは非同期処理、かつ依存関係はないので、それならば下記の流れで、A~Cは並列に実行すべきです。

1-1. 処理Aのリクエストを実施し、レスポンスを待たずに処理2を実行
1-2. 処理Bのリクエストを実施し、レスポンスを待たずに処理2を実行
1-3. 処理Cのリクエストを実施し、レスポンスを待たずに処理2を実行
2. 処理A~Cのレスポンスをすべて取得するまで待つ
3. 処理Dを実施

これならば、処理時間は「MAX(A, B, C) + D」ですみますので、結果としてA~Cの処理時間に関しては、他の2処理の処理時間分、短い時間で実行可能です。
(例えばCが一番処理時間が長い場合、(A + B)時間、処理時間を短縮できます)

これをコードにすると、下記のようになります。(3パターンのソースを書きました)

なお、「ラーメンで理解するasync/await」にある通り、非同期処理の実行とawaitは、絶対にセットで書かなきゃいけない...なんてことはありません。
下記のように、別々に書いても問題なく動きます。

//  OKな例(async/await使用)
async function sample() {
  
    // とりあえず、リクエストだけは一括で投げておいて...
    const axiosA = axios.get("./a");
    const axiosA = axios.get("./b");
    const axiosC = axios.get("./c");
  
    // awaitするのは、レスポンスの受け取りのみにする。
    // これなら、どれが一番時間がかかっても、待ち時間はMAX(A, B, C)になる
    const a = await axiosA;
    const b = await axiosB;
    const c = await axiosC;
  
    const d = funcD(a, b, c);
    return d;
  
}
  
//  OKな例(Promise.all使用)
function sample() {
  
    return new Promise((resolve) => {
  
      // とりあえず、リクエストだけは一括で投げるのは同じ。
      const axiosA = axios.get("./a");
      const axiosA = axios.get("./b");
      const axiosC = axios.get("./c");
  
      // Promise.all([])は、配列内の全処理のレスポンスが返るまで、
      // then()内の処理の実行を待つメソッド。
      // 逆に、どれか一個でもレスポンスが返ってきたら  
      // then()内の処理を実行する「Promise.race([])」もある
      Promise.all([axiosA, axiosB, axiosC])
        .then((result) => {
          // resultには、Promise.allで指定した処理のレスポンスが配列で
          // 格納される
          const d = funcD(result[0], result[1], result[2]);
          resolve(d);
        });
    })
}
  
//  OKな例(async/awaitとPromise.all併用)
function sample() {
  
    // とりあえず、リクエストだけは一括で投げるのは同じ。
    const axiosA = axios.get("./a");
    const axiosA = axios.get("./b");
    const axiosC = axios.get("./c");
  
    // Promise.all([])とawaitを併用する
    const result = await Promise.all([axiosA, axiosB, axiosC]);
  
    const d = funcD(result[0], result[1], result[2]);
    return d;
  
}

結局、どれで書くべき?

「ラーメンで理解するasync/await」のコメントでも、

  • こう書けばPromise.all()はいらんのか
  • Promise.all()いらんけど、並列実行がわかりにくいから微妙。
  • awaitばっか羅列する書き方()だと、並列実行が分かりにくい

などなど、いろいろ書かれてました。

個人的には、それぞれのメリットとしては、例えば下記だと思います。

  • async/await

    • ソースがシンプルになり、分かりやすい(同期処理っぽくてわかりやすい)
    • 前の非同期処理の結果に依存する非同期処理を実施する場合でも利用可能
      • 「前提:async/awaitの基本」で書いたような、前の非同期処理のレスポンスを利用する、など
  • Promise.all

    • 並列処理ということが一目でわかりやすい
    • awaitの乱用を防げる

ただ、正直どれを使っても大差はない、と思います。
(好みやプロジェクトに応じて使い分ければいいかと)

※個人的には「async/awaitとPromise.all併用」が、一番分かりやすいかな...と思いますが。

まとめ

正直、「結局、どれで書くべき」については、好みやらこだわりやら宗教やらもあるので、どれでも構わないと思います。

ただ「とりあえずawait」で非同期処理を書いていると、今回書いたような「不要な時間をかけてる処理」が出来てしまうので注意しましょう、という話でした。

実装する機能の仕様や設計から、処理フローの構成をしっかり検討することが大切だと思います。

【Serverless Framework】Serverless FrameworkからAWS Lambdaのテストを実施する

前回の更新が2019/10/12...ずいぶん間が空いてしまったなあ。
実際のところ、前回から

  • 本番リリース
  • 遅い夏休み(お遍路結願)
  • 謎の体調不良(1か月ほど)

で、全然ブログもスキルアップ活動も勉強会参加もできませんでした。

が、ようやく回復してきたので、リハビリがてら。

概要

  • Serverless Framework(以下SF)を使って、クラウド上のソースをテストする。
  • 今回はAWS Lambdaのみ
  • 前提として、事前にSFを使用してAWS Lambda(などのAWSリソース)デプロイをしておく。

SFについては、こちらを参照

AWS上のLambdaを実行する

とりあえず、最低限のコマンドがこちら。
※「funcName」には、SFの「functions」セクションで定義したLambda関数の定義名が入ります。

> sls invoke --function funcName

※必須のパラメータは「--function」だけですが、それ以外にも以下のような任意パラメータがあります。(僕が良く使うもの)

オプション名 説明 備考
--stage 実施したい環境 省略時はdev
--region 実施するAWSのリージョン 省略時は--stageの環境に紐づくリージョン(?)
--path イベントハンドラのevent引数に設定するJSONオブジェクトを定義したjsonファイルの相対パス 今回はは割愛したが、--dataだとテキスト形式で指定できる
--log CloudWatchのログをターミナルに出力させるかどうか --logを指定すると、CloudWatchのログをターミナルに出力できる。(これがすごい便利)

--pathオプションについて

--pathオプションに指定するjsonファイルには、ハンドラの「event」引数の中身を定義することができます。

Lambdaに渡す値は、この「event」引数の「queryStringParameters」「body」キーなどに格納されているので、このjsonファイルの中身を変えることで、いろいろな値を指定してテストを行うことができます。

なお、event引数自体にはたくさんのキー&値がありますが、テストする場合は最低限のキー&値を定義すればOKです。

// 例えば、このLambda関数をテストする場合...
exports.handler = async (event, context, callback) => {

  console.log({event});

  let greeting = "No Name";
  if (event && event.queryStringParameters && event.queryStringParameters.greeting) {
    greeting = `${event.queryStringParameters.greeting} Makky12`;
  } 

  let resBody = {
    greeting: greeting,
  }

  const response = {
    "statusCode": 200,
    "headers": {
      "Access-Control-Allow-Origin" : "*"
    },

    "body": JSON.stringify(resBody)
  }

  return response;
}
{
    // jsonファイルは、最低限下記定義さえあればOK。
    "queryStringParameters": {
        "greeting": "Guten"
    },
}
# 下記コマンドでテストすると、以下のようなレスポンスが表示される。  
# (event.jsonに上記json定義があるとする)
> sls invoke --function funcName --path ./event.json 
  
{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"greeting\":\"Guten Makky12\"}"
}

--logオプションについて

通常、テストを実行した場合にターミナルに表示されるのはレスポンスの内容だけですが、「--log」オプションを付けることで、CloudWatchの内容を追加で表示できます。

個人的にはこれがかなり便利な機能だと思っていて、いちいちAWSにアクセスしなくても、エラーの原因がある程度わかりますので、デバッグにもかなり役立ちます。

# --logオプションありだと、レスポンスの下にCloudWatchのログが表示される。  
# ※これがクッソ便利!
> sls invoke --function funcName --path ./event.json --log
  
{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"greeting\":\"Guten Makky12\"}"
}

START RequestId:xxx Version: $LATEST  
2019-12-28 09:07:34.901 (+09:00)   INFO    { event: { queryStringParameters: { greeting: 'Guten' } } }  
END xxx  
REPORT xxx  Duration: 110.36 ms  

ローカルのLambda関数を実行する

先程までは実際にAWS上のLambdaに接続しましたが、ローカルのLambdaをテストすることもできます。

ローカルのLambdaを実行するコマンドはこちら。(「local」が追加されただけですが)

> sls invoke local --function funcName

オプションもAWS上で実行する場合とほとんど同じですが、下記が追加されてます。

オプション名 説明 備考
--env name=value形式で、環境変数を指定できる serverless.ymlで「${opt:env}」(コマンド引数envの値を参照する)定義があるとエラーになる。
この場合省略形(-e)を使用する。
--docker Docker上での動作を可能にする node.js, Ruby, Python, Javaのみ
--docker-arg Dockerで実行する際、Dockerに渡す引数を指定できる

envオプションについて

上記に記載の通り、--envオプションで、Lambdaに定義した環境変数の値を変更できます。

たとえば、先述のLambda関数のresBody.greetingの値に下記の環境変数を定義した場合、下記のような差があります。

変数名
MY_SAMPLE_ENV hogehoge
# AWSのLambda関数を実行した場合
# この場合、--envの指定は無視され、Lambdaで定義した「hogehoge」になる。
> sls invoke --function funcName --path ./event.json --log --env MY_SAMPLE_ENV=fugafuga
  
{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"greeting\":\"hogehoge\"}"
}
START RequestId:xxx Version: $LATEST  
2019-12-28 09:07:34.901 (+09:00)   INFO    { event: { queryStringParameters: { greeting: 'Guten' } } }  
END xxx  
REPORT xxx  Duration: 110.36 ms  
  
# ローカルのLambda関数を実行した場合
# この場合、--envの指定が適用され、--envで指定した「fugafuga」になる。
# なおローカルの場合、--logオプションは無視される
> sls invoke --function funcName --path ./event.json --log --env MY_SAMPLE_ENV=fugafuga
  
{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"greeting\":\"fugafuga\"}"
}

AWS上での直接実行とローカル実行の比較

実行方法 メリット デメリット
直接実行 ・実運用に即したテストができる
・エラー原因が特定しやすい(ログを出力できるので)
・通信費がローカル実行よりかかる
・修正後の動作確認時に、いちいちデプロイや更新を行う必要がある
ローカル実行 ・通信費が直接実行より安い(※)
・修正後の動作確認が簡単
・エラーの特定が直接実行より難しい(ログが出力できないので)
・実運用に即したという点で、直接実行には劣る

※最初「ローカル実行だから、実際のAWSリソース(S3やDynamoDBとか)にはアクセスできない(=エラーになる)」と考えていましたが、実際は普通にSecretManagerやAuroraの値を取得できました。
pluginでローカル実行用の(Mock)プラグインの定義をしなかった場合、普通にリソースの値はAWSに接続して取得するのか?(この辺りは、要調査です)

まとめ

結論としては、結構使いやすいと思います。
やはり「--log」によるログ出力機能が便利だなあ、と感じました。

「ローカル実行時でも、(実運用時と同様に)ハンドラ関数呼び出しによるブラックボックステストしか行えない(privateな内部関数のテストが行えない)」点はありますが、それを差し引いても便利だなあと感じます。
その辺は、Jestなどと併用して解決していけばよいと思います。

体調不良やらいろいろありましたが、何とか年内にブログを書けて良かった。

それでは、良いお年を。

【AWS】Cognitoによる認証を実装する(その2)

概要

その1で、Cognitoユーザープールの説明の前半部分を書きましたが、今回はその後半になります。

アプリクライアントの作成

このユーザープールを使用して認証を行うアプリの設定をします。
設定項目は下記の通り。

項目名 説明 備考
アプリクライアント名 このユーザープールを使用するアプリの名称
トークンの有効期限を更新 ログイン時に発行される、各種トークンの有効期間(日) デフォルトは30日。この期限を超えたら、トークンのリフレッシュが必要になる
クライアントシークレットを作成 クライアント用のシークレットを作成する SDKではクライアントシークレットを使用しない(使用すると例外を出す)ので、SDKを使用する場合はチェックを外すこと
サーバーベースの認証でサインイン API を有効にする AWSコンソールなど、サーバーベースでのアカウント作成を許可する チェック推奨。コンソール以外にも、AWS CLIからアカウント生成&パスワード認証を行えるようになる。(下記リンク参照)
カスタム認証のみを許可する 標準の認証フローを使わず、Lambdaトリガの「認証チャレンジ」による認証フローのみ許可する Lambdaトリガについては後述
アプリベースの認証でユーザー名とパスワードの (SRP を使用しない) フローを有効にする Secure Remote Passwordプロトコルを使用しない、使用するアプリからのID&パスワードのみによる認証を有効にする 公式ページによると、旧(レガシーな)システムからのユーザー移行を行う場合、チェックを付けると移行がスムーズとのこと。(逆に言えば、それ以外ではチェックを外すのがよい)

また、「属性の読み込みおよび書き込みアクセス権限を設定する」をクリックすると、各種ユーザー属性(attributes)の読み込みや書き込みの許可/不許可が設定できます。(デフォルトはすべて読み書き可能)

f:id:Makky12:20190916180541p:plain
f:id:Makky12:20191010194927p:plain

Lambdaトリガの設定

Cognitoでは、各種認証処理(サインアップ・サインインなど)の前後に、Lambda関数による独自の処理を追加することができます。
その独自処理を行うタイミング&実行するLambda関数を定義します。

これを使用すると、例えば以下のようなことができます。

  • サインアップ時に、検証コードや検証リンクなしにメールアドレスを強制的に「検証済み」にする
  • ログイン時に、Cognitoによる認証(ID・パスワード・検証コードなど)以外に、独自の認証ロジックを追加できる(下記リンク参照)
  • サインアップ&認証コード送信時に送るメールの内容を、自由にカスタマイズできる。

てかこれ、実際触ると分かるんですが、本当に便利なんです。

※このLambdaトリガに関しては、次回or次々回くらいに、ブログに記載しようと思ってます。

ちなみに、下記公式ページに詳しいことが記載されています。
また、Lambdaトリガ処理は下表のタイミングで実施することができます。(公式ページより抜粋)

f:id:Makky12:20191010195647p:plain

f:id:Makky12:20190916180600p:plain

f:id:Makky12:20190916180619p:plain

あとは最終確認として設定内容が表示されるので、問題なければ「プールの作成」をクリックすると、ユーザープールの作成が実行されます。

フェデレーテッドアイデンティティ

Cognitoには、「認証を行ったユーザーに対して、特定のロールを割り当てる」ことができる「フェデレーテッドアイデンティティ」という機能があります。
これを利用すると、例えば下記のことができます。

  • ユーザープールごとに、使用できる機能(例えば、実行できるLambdaなど)を割り当てることができる
  • 認証(=ログイン)をしていないユーザーにも、権限を与えることができる。
    • 認証の有無で、実行できる権限を変更できる
  • 外部SNSなど、Cognito以外の認証(=OAuth)をサポートし、権限を設定できる
IDプールの作成

「新しいIDプールの作成」画面では、下記の設定を行います。
※ちなみに「IDプール」は、「ユーザープール」とは別物です。

項目名 説明 備考
IDプール名 このIDプールの名称
認証されていないID 未認証(=未ログイン)のユーザーに対しても、AWSへの各種リソースへのアクセスを有効にするか パブリックなアクセスが可能になるので、ロールの設定には要注意
認証プロバイダ 認証に使用するプロバイダ。Cognitoの他、FacebookGoogleなどのSNSも設定可 設定項目はプロバイダ毎に異なる。Cognitoの場合、「ユーザープールID」と「アプリクライアントID」

f:id:Makky12:20190916180641p:plain

ロールの設定

「認証されたユーザー」及び「認証されていないユーザー」それぞれに割り当てるロールを設定できます。
(先述の「認証されていないID」のチェックを外しても「認証されていないユーザー」が表示されるのは、なんか変ですが...)

「認証されたユーザー」及び「認証されていないユーザー」それぞれに、下記を編集します。
最後に「許可」をクリックすればOKです。

項目名 説明 備考
IAMロール 適用するIAMロール(既存項目の使用or新規作成)
ロール名 (IAMロール新規作成時の)ロールの名前
ポリシードキュメント 適用するポリシー(=インラインポリシー) 「編集」ボタンをクリックすると編集可能

f:id:Makky12:20190916180705p:plain

と、長々と書いてしまいましたが、これで一通りCognitoユーザープールの説明ができました。

次回からは、実際にソースコードから下記を実行しようと思います。

  • Cognitoユーザープールを行った認証
  • 先述の「Lambdaトリガ」の具体的な使用例

【.net Core】Blazorアプリを作成する&Azureで公開する

概要

先日9/23に、ついに.net core(ver3.0)がメジャーリリースされました。(LTSはver3.1だそうですが)

そして、9/26(木)にマイクロソフトで開催された、下記イベントに参加してきました。

この中で、マイクロソフトの井上さん(チャックさん)が話された、.net Coreの概要について、下記が魅力的だな、と思いました。

  • .net CoreがWPF、及びWinFormsに対応
  • .net Coreメジャーリリースに合わせて、Blazorも正式対応

上記のうちBlazorについて簡単に触って、ついでにAzureに公開するところまでやりましたので、紹介がてら。

Blazorとは?

Blazorの概要&詳細については、マイクロソフト公式ページに書いてありますので、そちらを参照。

個人的に、上記イベントでのBlazorの印象は、

「Vue.jsの"単一ファイルコンポーネント"を、C#で実現した感じ」

でした。(「Vue.jsの単一ファイルコンポーネント」についてはググって下さい)

実際、一言で表現するなら、こんな感じになると思います。

プロジェクトを作成する

とりあえず、Visual Studioで、下記手順でBlazorプロジェクトを作成します。

  • Visual Studioで[新規作成] - [ファイル]を選択し、テンプレートから[Blazorアプリ]を選択
  • プロジェクト名などを設定し「作成」をクリック

プロジェクトを作成後、下記手順で新しいRazorコンポーネントを追加します。
- ソリューションエクスプローラ上で「Pages」フォルダを右クリック
- [追加] - [新しい項目] - [Razorコンポーネント]を選択

f:id:Makky12:20191004192648p:plain f:id:Makky12:20191004192700p:plain

プログラムを書く

とりあえず、サンプルで書いたのソースが下記。(クッソ長くなったな...)

razor記法についてはここでは突っ込みませんが、特徴は下記あたりでしょうか。

  • 「@page」でページのリンク(ルーティングパス)を定義
  • 「@code」にバックグラウンドロジックを記載する
    • Vue.jsでいう「<script>」タグ
  • HTML内に「@onclick」「@if」など、分かりやすいディレクティブが使える
    • Vue.jsで言う「v-on:click」や「v-if」
    • 下記ソースには「@onclick」はないですが、デフォルトで作成される「Pages/Counter.razor」に記載されています。

やたら「Vue.jsで言う」と言っていますが、このあたりが先程の

「Vue.jsの"単一ファイルコンポーネント"を、C#で実現した感じ」

な気がします。

// Pages/MyRazor.razor
@page "/myrazor"  
  
@using BlazorApp1.Data
@using System.Collections.Generic
@using System.Linq;
@using System.Net.Http;
@using Newtonsoft.Json;
@using System.Diagnostics  
  
<h1>MyFirstRazor</h1>

<div class="container">
    <div class="row">
        <div class="col-10 offset-1">
            <div class="btn-group btn-group-toggle" data-toggle="buttons">
                <label class="btn btn-primary active">
                    <input type="radio" name="options" id="option1" autocomplete="off" checked />全員
                </label>
                <label class="btn btn-primary">
                    <input type="radio" name="options" id="option2" autocomplete="off"  />日本人
                </label>
                <label class="btn btn-primary">
                    <input type="radio" name="options" id="option3" autocomplete="off" />外国人
                </label>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-10 offset-1">
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th scope="col">ID</th>
                        <th scope="col">Name</th>
                        <th scope="col">Country</th>
                    </tr>
                </thead>
                <tbody>
                    @if (goalkeepers != null)
                    {
                        foreach (var gk in goalkeepers)
                        {
                            <tr>
                                <td>@gk.Id</td>
                                <td>@gk.Name</td>
                                <td>@gk.Country</td>
                            </tr>
                        }
                    }
                </tbody>
            </table>
        </div>
    </div>
</div>  
  
@code
{
    List<GoalKeeper> goalkeepers = null;
    List<GoalKeeper> allGoalKeepers = null;
  
    protected override async Task OnInitializedAsync()
    {
        var client = new HttpClient();
        var response = await client.GetStringAsync("(Azure Functionの呼び出し用URL)");
        allGoalKeepers = JsonConvert.DeserializeObject<List<GoalKeeper>>(response);
        goalkeepers = new List<GoalKeeper>(allGoalKeepers);
    }
  
    private EventCallback FilterGoalKeeper(int mode)
    {
        Debug.WriteLine(mode.ToString());
        List<GoalKeeper> gk = null;

        switch (mode)
        {
            case 0:
                gk = new List<GoalKeeper>(allGoalKeepers);
                break;
            case 1:
                gk = (List<GoalKeeper>)from item in allGoalKeepers
                                       where item.Country == "Japan"
                                       orderby item.Id
                                       select item;
                break;
            case 2:
                gk = (List<GoalKeeper>)from item in allGoalKeepers
                                       where item.Country != "Japan"
                                       orderby item.Id
                                       select item;
                break;
        }
    }
}  
  
  
// Data/GoalKeeper.cs  
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BlazorApp1.Data
{
    public class GoalKeeper
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Country { get; set; }

        public GoalKeeper(int id, string name, string country)
        {
            this.Id = id;
            this.Name = name;
            this.Country = country;
        }
    }
}
Bootstrapの適用

また、HTML部分を見て気づいた人もいるでしょうが、上記HTMLにはBootstrap4を適用してます。

Blazorでも、下記パッケージをNuGet経由でインストールすれば、Bootstrapを使用出来ます。

インストール後、Pages/_Host.cshtmlファイルの「body」タグに下記を追加するだけでOKです。

<body>
    <!-- この1行を追加-->
    <script src="_content/BlazorStrap/blazorStrap.js"></script>
</body>

Azureで動かす

で、クライアント部分は完成したので、せっかくなのでAzureにデプロイして、Azure上で動かします。

デプロイ方法ですが、下記方法でOKです。

  • ソリューションエクスプローラでプロジェクトを右クリックして「発行」を選択
  • 「発行先を選択」で「Azure App Service」を選択
  • 「App Service 新規作成」で、下記画像の各種情報を指定し、「作成」をクリックする。
    • 「リソースグループ」「ホスティングプラン」は、その場で新規作成ができます。
    • デフォルトだとUSリージョンが選ばれている場合があるので注意。

※もし「発行先」で「WebサーバーかAzurか」という選択が出た場合は「Azure」を選択してください。
(一度だけ出たのですが、再現しなかった...)

f:id:Makky12:20191004193752p:plain f:id:Makky12:20191004193802p:plain

ただ、Azureにデプロイ完了しても、このままでは動きません。
Azureがまだ.net Core3.0に対応していないようなので、.net Core3.0が動くようにする必要があります。

それには、下記手順を実行すればOKです。

  • デプロイしたBlazorの「App Service」リソースで、「拡張機能」を選択する
  • ASP.NET Core 3.0 (x64) Runtime」をインストールする
  • 「設定」→「構成」をクリックし、「全般設定」タグで以下の設定をする。
    • スタック:.net core
    • プラットフォーム:64bit

f:id:Makky12:20191004194003p:plain f:id:Makky12:20191004194014p:plain

これでデプロイしたBlazorアプリの「概要」に表示されているURLを表示すれば、Azure上でBlazorアプリが動くはずです。

f:id:Makky12:20191004195255p:plain

※なお、デプロイ&再デプロイ時に下記ダイアログが表示されますが、これは先程の「Azureがまだ.net Core3.0に対応していない」のが理由と思われるので、とりあえずは気にしなくてOKです。

f:id:Makky12:20191004195436p:plain

ちなみに、Azure Functionのソースはこちら。

using System;
using System.IO;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
  
namespace Company.Function
{
    public static class HttpTriggerCSharp
    {
        [FunctionName("HttpTriggerCSharp")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");
  
            var gk = new List<GoalKeeper>();
            gk.Add(new GoalKeeper(1, "ハーフナー・ディド", "Netherland"));
            gk.Add(new GoalKeeper(2, "伊藤 裕二", "Japan"));
            gk.Add(new GoalKeeper(3, "楢崎正剛", "Japan"));
            gk.Add(new GoalKeeper(4, "川島永嗣", "Japan"));
            gk.Add(new GoalKeeper(5, "ミチェル・ランゲラク", "Australia"));
  
            string resopnseJson = JsonConvert.SerializeObject(gk);
            return (ActionResult)new OkObjectResult(resopnseJson);
        }
    }
  
    class GoalKeeper
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Country { get; set; }

        public GoalKeeper(int id, string name, string country) {
            this.Id = id;
            this.Name = name;
            this.Country = country;
        }
    }
}

まとめ

と、かなり駆け足でしたが、Blazorについて、簡単に紹介しました。

個人的にマイクロソフトは、環境やプラットフォームはかなりシェア持ってるけど、Web系言語としての採用率がJS系フレームワークPHPなどに比べてイマイチ...と思っていただけに、巻き返しに期待したいところです。

【AWS】Cognitoによる認証を実装する(その1)

概要

今まではServerless FrameworkやCloudFront周りのネタでしたが、今回からはしばらくCognito関連のお話。
今回はユーザープール&IDプールによる、認証基盤作成について。

※ちなみに次回以降は、下記ネタの予定。

  • Amplify/Authを利用した認証処理
  • 認証処理のLambdaトリガー

今回触れること

  • ユーザープール作成
  • IDプール作成

Cognitoの認証基盤について

以下の2つがあります。

プールの種類 説明 リソースへのアクセス
ユーザープール Cognitoのベースで、ID・Eメール・パスワードなどによるユーザー認証関連の基盤を提供する。 Cognito認証の有無
IDプール ユーザープールを使用してサインインしたユーザーに、設定したロールを割り当てる。 ロールにアクセス権限があるかどうか

今回は主に「ユーザープール」について記載します。

手順0 ユーザープールの作成

  • AWSコンソールで「Amazon Cognito」を選択する
  • [ユーザープールの管理] - [ユーザープールの作成] をクリックする。

手順1 ユーザープール名の設定

  • プール名:作成するユーザープール名を入力すればOKです。
  • 作成方法:下記2つから選択できます。
    • デフォルトを確認する:全項目にCognitoのデフォルト値が設定されます。(作成確定前に変更可能です)
    • ステップに従って作成する:順番に1項目ずつ設定を行います。(自分でカスタマイズしたい場合はこちら)

※この記事は「ステップに従って作成する」を前提に記載してます。

手順2 サインイン方法&属性の指定

サインイン方法、およびサインアップに必要なユーザー属性を設定します。
※これらは一度ユーザープールを作成すると、あとで変更できないので注意!

サインイン方法
大項目 オプション項目 説明 備考
ユーザー名 ユーザー名(username)でログインする オプションは複数選択可
Eメールサインインも許可 メールアドレス(email)でもログイン可能にする
電話番号サインインも許可 電話番号(phone_number)でもログイン可能にする
任意ユーザー名 ユーザーが許可した属性(≒カスタムユーザー名)でもログイン可能にする ユーザー名を変更したい場合に選択。
(Cognito標準のユーザー名(user_name)は変更不可なので)
Eメールおよび電話番号 Eメールまたは電話番号でログインする オプションは1つのみ選択可能
Eメールを許可 メールアドレスでのみログイン可能にする
電話番号を許可 電話番号でのみログイン可能にする
両方を許可 メールアドレス/電話番号のどちらでもでもログイン可能にする ユーザー名が選択可能
ユーザー属性(attributes)

サインアップする際に、入力を必須とするユーザー属性を指定します。(どんな属性があるかは、画像を参照)
チェックを付けた属性は「入力必須」となり、サインアップ時に値を指定しないと、サインアップ不可となります。

  • チェックを付けてない項目は「使用不可」ではなく「任意項目」になるだけなので、サインアップ時に値を指定すれば、問題なく使用&登録可能です。
  • 「email」や「phone_number」は、後述する多要素認証でEメールやSMSでコードを送る場合、必須になります。
カスタム属性について

属性を自分で追加することも可能です。
「カスタム属性を追加する」をクリックすると、自分でカスタム属性を設定可能です。(下記項目を指定できます)

項目 説明 備考
タイプ 文字型(string)/数値型(number)
名前 カスタム属性の名前です。 作成後、自動で「custom:」が先頭に付与されます(※)
範囲 文字列の長さの範囲(文字型)/値の範囲(数値型) 「タイプ」により異なる
変更可能 値を変更できるかどうか

※Amplify/AuthやAWS-SDKAPIで指定する場合、下記の様にしないとエラーになります。

attributes = {
    // これはNG  
    "hoge" : "fuga"  
  
    // こうしないといけない
    "custom:hoge" : "fuga"
}

f:id:Makky12:20190915172150p:plain

セキュリティ設定

パスワード強度設定

パスワードの強度を設定できます。設定可能なのは下記です。

  • パスワードの最低文字数
  • パスワードの必須入力文字
    • 数字
    • 特殊文字(=記号)
    • 半角大文字
    • 半角小文字
自己サインアップの許可
項目 説明 備考
管理者のみ許可 ユーザープールを作成するAWSアカウントの管理者、または開発者(credential認証者)のみサインアップ可能にする アプリから作成するには「adminXXX」というメソッドを使う必要がある。(signUpでは不可)
ユーザーの自己サインアップを許可 AWSアカウントの管理者、開発者でなくてもサインアップ可能にする signUpメソッドで、サインアップ可能。ただし認証コード等による検証が必要(これについては後述)
有効期限の設定

管理者(または開発者)がサインアップした場合、そのアカウントには「仮パスワード」が付与されます。
その仮パスワードの「有効期限」を設定します。

※この場合、対象アカウントは(仮パスワードでの)初回ログイン時に「本パスワード」を設定する必要があります。

f:id:Makky12:20190915172432p:plain

多要素認証(MFA)設定

多要素認証(MFA)について、下記を設定できます。

項目 説明 備考
多要素認証の有効化 オフ/省略可能/必須 「省略可能」の場合、ユーザーが多要素認証を使用するかどうかを設定できます。
※オフにしても、自前で多要素認証を実装できます。
第二の要素 SMS/ワンタイムパスワード(TOTP) 「SMS」の場合、登録した電話番号に検証コード送信します。
「TOTP」の場合、時間ベースで一意に生成される認証コード値により認証を行います(※)
送信先 Eメール/電話番号/両方/なし 検証コードの送付先。「両方」にした場合、(なぜか)SMSにしか送られない模様。
「なし」にした場合、パスワードを忘れた際の再設定ができません。(再設定用コードを送れないので)
SMSロールの提供 (ロール名を記載) SMSによる認証コードを送信する場合、SMS送信に必要なロールを指定する。
この画面で新規作成することも可能。
※SMS送信しないなら空欄でOK

Eメールアドレスの設定

Eメールで検証コードを送信する場合の設定です。
事前にSES(Simple EMail Service)に検証済Eメールを登録済の場合、SESの登録情報から下記を選択できます。

  • リージョン
  • 送信元Eメールアドレス
  • 返信先Eメールアドレス

※SES登録していない、または使用しない場合、どれもデフォルトのままでOK。

その他、Eメール送信にSESを使用するかどうかを選択できます。
使用するする場合は上記のメール設定が、使用しない場合はデフォルトのメール設定が適用されます。

※使用しない場合、送信元Eメールアドレスは「no-reply@verificationemail.com」となります。

f:id:Makky12:20190915172514p:plain

メッセージのカスタマイズ

検証コード送信メール、及びユーザー名&仮パスワード送信メールの内容を設定できます。
それぞれ、下記のメールが送信されます。

状況 送信メール
サインアップ完了直後(検証コード未送信時) 検証コード送信メール
管理者(or 開発者アカウント)でのサインアップ完了時 ユーザー名&仮パスワード送信メール

設定できるのは下記項目です。

  • 検証タイプ(検証コード/検証用URL)
  • 件名(Eメールのみ。SMSは不要)
  • メール本文

※下記スクショの通り、

  • ユーザー名:{username}
  • 検証コード、パスワード: {####}

とすると、実際のメールに上記が埋め込まれます。

f:id:Makky12:20190915172710p:plain
f:id:Makky12:20190915172731p:plain

タグ&デバイス記憶の設定

ユーザープールにタグを埋め込むかの設定です。
タグを埋め込みたい場合、キー&値のペアを設定します。
(無理に埋め込む必要はありません。)

またデバイス記憶は、下記の設定を行えます。

項目 説明 備考
バイス記憶の有効化 常に/ユーザーが選択/記憶しない
第二の認証の省略 はい/いいえ 「はい」にした場合、記憶済デバイスからのアクセスの場合、認証コード/リンク/TOTPによる認証をスキップ可能。
バイスを記憶しない場合、設定不要です

なんかかなり長くなってきたので、続きはその2で。

【Serverless Framework】package設定について&パッケージが終わらない場合の対策

概要

以前、簡単デプロイとserverless.ymlの記載についてでServerless Framework(以下SFW)のserverless.ymlファイルについての記事を書きました。
今回はその中の「package」項目(=デプロイパッケージの設定)に関する内容です。

Serverless Framework公式サイト

共通設定

全functionsで共通のpackage設定は、ルートの「package」項目内に設定します。
主に、下記の設定項目があります。

項目名 意味 備考
exclude デプロイパッケージに含めないファイル/フォルダを指定する 基本的に、ここに含めなかったファイル/フォルダはすべてパッケージに含まれる
include excludeで指定したフォルダの中で、パッケージに含めたいファイル/フォルダを指定する
artifact デプロイファイルとして設定したい*.zipファイルを指定する 指定すると、SFWのパッケージ処理は無視される(設定も含め)?
※ここはイマイチよくわかってません。別ツールでデプロイパッケージを作成する場合に指定する?
individually functionsの関数について、個別にパッケージを行う(=*.zipファイルを作成する)かどうか(true/false) デフォルトはfalse
excludeDevDependencies パッケージに含める全フォルダ内のpackage.jsonの「devDependencies」に定義したモジュールを自動でパッケージから除外するかどうか(true/false) デフォルトはtrue。
詳細は「Excluding development dependenciesが終わらない場合」を参照

関数別設定(functions)

「共通設定」に記載した設定項目ですが、これはfunctionsの各関数で個別に設定することも可能です。(基本は両方を組み合わせて使用する...のかな?)

(例)

package:
  individually: true
  exclude:
    - lambda/**
    - src/**
  include:
    - lambda/common/mysql2  
  
functions:
 func1:
    handler: lambda/func1/index.handler
  package:
    individually: true
    include:
      - lambda/func1/*
    exclude:
      - lambda/func1/*.json  
  
 func2:
    handler: lambda/func2/index.handler
  package:
    individually: true
    include:
      - lambda/func2/*
    exclude:
      - lambda/func2/*.json

※上記の例だと、こんな感じでファイルができます。(デフォルトでは「.serverless」フォルダ内にできます)

  • func1.zip
    • lambda
      • common
        • mysql2
      • func1
        • index.js
  • func2.zip
    • lambda
      • common
        • mysql2
      • func2
        • index.js

勘違いしてたこと

共通設定だけではだめ

最初は「共通設定だけ設定すればいいんだな」と、共通部分の設定のみ行っていたのですが、これだと個別の*.zipファイルはできますが、

  • 各*.zipファイルに、全functionが格納されている

という、変な状態になります。

(例)下記serverless.ymlでパッケージを行うと...

package:
  individually: true
  exclude:
    - src/**
  include:
    - lambda/**  

本当は「 関数別設定(functions) 」記載した構成の個別*.zipファイルができてほしいのですが、実際はこうなります。(commonフォルダは省略)

  • func1.zip
    • lambda
      • func1
        • index.js
      • func2
        • index.js
  • func2.zip
    • lambda
      • func1
        • index.js
      • func2
        • index.js

...そりゃあ、パッケージ処理が遅くなりますよね。

functions格納フォルダは共通設定でexcludeする

また、始めは「(上記の例の場合)共通設定でlambdaフォルダは含めないとダメだろ」と、serverless.ymlに下記の設定をしていました。
※(全functionsの格納フォルダである、lambdaフォルダをパッケージに含めている)

package:
  individually: true
  exclude:
    - src/**
  
functions:
 func1:
    handler: lambda/func1/index.handler
  package:
    individually: true
    include:
      - lambda/func1/*
    exclude:
      - lambda/func1/*.json  
  
 func2:
    handler: lambda/func2/index.handler
  package:
    individually: true
    include:
      - lambda/func2/*
    exclude:
      - lambda/func2/*.json

が、これで作成される*.zipファイルは、先述の「共通設定だけではだめ」でできた構成と同じになります。

なので、「関数別設定(functions)」に記載したserverless.ymlファイルのように、

  • 全functions共通で必要なもの以外は、すべて共通設定でexcludeし、functionsで個別にincludeする

ということでしょうね。

Excluding development dependenciesが終わらない場合

AWS CLIの「serverless package」コマンドで実際にデプロイパッケージ作成処理を行った際、

  • 「Excluding development dependencies...」が全然終わらない

という現象が発生する場合があります。

これは、SFWがデプロイパッケージ作成の際、

  • (パッケージ対象フォルダの)全package.json内のdevDependenciesのモジュールを再帰的にチェックし、パッケージ対象から外す

処理を行っているからです。

一見よくわからないかもしれませんが、例えばどこかでpackage.jsonがあったら、

  • そのpackage.jspnのdevDependenciesをパッケージから外す
  • そのpackage.jsonの全dependenciesモジュールについて、一つずつ
    • そのモジュールのpackage.jsonを見に行く
    • そのモジュールのpackage.jsonのdevDependenciesをパッケージから外す
    • そのモジュールのpackage.jsonの全dependenciesモジュールについて、一つずつ...(以下繰り返し)

を全package.json、及びそのdependencies&devDependenciesについて行うので、設定次第ではめちゃくちゃ遅くなります。

で、対策ですが「共通設定」で説明した「excludeDevDependencies」を「false」に設定すればOKです。

※ただし、その場合devDependenciesのパッケージが除外されないので、パッケージサイズが大きくなるケースもあります。
わかるなら事前にexcludeするなどの対策が必要です。

package:
  individually: true
  exclude:
    - lambda/**
    - src/**
  include:
    - lambda/common/mysql2  
  excludeDevDependencies: false

参考:“Excluding Development Dependencies” Takes Forever

まとめ

今回は久々にSFWの内容になりました。
SFWは本当に便利&奥が深いので、時間が許す限り、もっともっと突き詰めてみたいところです。

ただ、公式サイトが最近リニューアルしましたが、その結果なんか記事が見にくく(探しにくく)なった感じがしますが...

てか、AWS Cognitoの記事はどうなった...