Node.js
MySQL
AWS
Aurora

AuroraDBのIAM認証に盛大にハマるの巻

動機

AuroraDBのIAM認証にかなりハマったのでその体験を共有しておこうと思います。とても長くなったので先に結論を書いておきますね。

  • Node.js SDK + IAM Roleの組み合わせの問題
    →ドキュメントにひっそり書いてある
  • IAM認証は実はアプリケーションからの接続にはあまり向いていない
    →ドキュメントにひっそり書いてある

AuroraDBのIAM認証とは何か

丸投げで恐縮ですが下記のようなものです。

http://blog.serverworks.co.jp/tech/2017/04/27/rds-iam-auth-lambda-python/

  • MySQLへの接続時にパスワードを用いない認証手段
    • アプリケーションの設定ファイルなどにDBのパスワードを書かなくていい
  • EC2インスタンス、ECSタスク、Lambdaファンクションなどにひも付けたIAMロールに権限を与えることでDBにコネクトできる

何が嬉しいのかというと前述通りDBのパスワードを管理しなくて良いことです。
DBのパスワードの扱いは何かと気を使うものです。プログラムにベタ書きしてGithubにpushしちゃうのは論外で、暗号化して設定ファイルに書くとか、実行時にどこかから取得するとか何らかの仕組みを考える必要があります。

蛇足ですが、最近ではSSMパラメータストアというものがあって、KMSを使って暗号化されたDBパスワードをAWSマネージドな領域で管理することが出来ます。今回はなんだかんだ言って結果的にこれを採用しました。

IAM認証の使い方及び仕組み

事前準備

IAM認証には2つのものが必要です。

  1. Aurora(MySQL)側の接続用ユーザー
  2. IAMロールにポリシー設定

接続用ユーザーはAWSAuthenticationPluginというものを使って認証する用に設定されたユーザーであることが求められます。

CREATE USER 'appuser'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS';
GRANT ALL ON mydatabase.* to 'appuser'@'%' REQUIRE SSL;

ポリシーにはrds-db:connectというActionをAllowすることが求められます。

{
    "Action": "rds-db:connect",
    "Resource": "arn:aws:rds-db:ap-northeast-1:756xxxxxxxx:dbuser:yourclusterName/appuser",
    "Effect": "Allow"
}

Node.js + Sequelizeから利用する

今回はNode.jsとSequelizeというORマッパーを利用しました。(この先で述べる問題点はNode.js SDK固有の問題ですが、Sequelizeに依存するものではありません)

冒頭でIAM認証ではパスワードを使わないと書きましたが、半分嘘で、事前定義されたパスワードを用意する必要が無いというのが本当のところです。内部的には一時パスワードを発行することでやり取りしています。

一時パスワードは次のようなものが生成されます。(都度変わります)

 orenodb-database-rdscluster-mo9ij14anj9n.cluster-ca61ppcgmuds.ap-northeast-1.rds.amazonaws.com:3306/?Action=connect&DBUser=appuser&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAITON6LGVCDOOD25A%2F20180116%2Fap-northeast-1%2Frds-db%2Faws4_request&X-Amz-Date=20180116T100947Z&X-Amz-Expires=900&X-Amz-Signature=36028f3fb1abfca72b2dbb61aff1f6470744d8aad7d6fde9963e729cf52954f1&X-Amz-SignedHeaders=host

AWS署名ですね、これ。

この一時パスワードをRDS.SignerのgetAuthTokenで取得し、そのパスワードを使ってあとは普段通りMySQLに接続するわけで、下記コードのSequelize クラスのコンストラクタの第三引数tokenがパスワードになるわけです。

"use strict";
const AWS = require("aws-sdk");
const Sequelize = require("sequelize");

const RdsSigner = new AWS.RDS.Signer();
const token = RdsSigner.getAuthToken({
  region: "ap-northeast-1",
  username: process.env.DB_USER,
  hostname: process.env.DB_HOST,
  port: 3306
});

console.log("TOKEN:", token);

const sequelize = new Sequelize(
  process.env.DB_NAME,
  process.env.DB_USER,
  token,
  {
    dialect: "mysql",
    dialectOptions: {
      host: process.env.DB_HOST,
      ssl: "Amazon RDS",
      authSwitchHandler: (data, cb) => {
        if (data.pluginName === "mysql_clear_password") {
          // https://dev.mysql.com/doc/internals/en/clear-text-authentication.html
          const buffer = Buffer.from(token + "\0");
          cb(null, buffer);
        }
      }
    }
  }
);

(async () => {
  return await sequelize.databaseVersion();
})()
  .then(res => {
    console.log(res);
    process.exit(0);
  })
  .catch(err => {
    console.error(err);
    process.exit(1);
  });

mysql_clear_passwordの件はIAM認証を使う上での儀式だと思ってください。

で、結局オマエ何にハマったのよ?

前置きが長くなりました。

1. IAM認証は実はアプリケーションからの接続にはあまり向いていない

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html

このドキュメントをよーく読む必要があります。

まず、

Amazon RDS for MySQL および Aurora MySQL データベースエンジンでは、1 秒あたりの認証試行回数に制限はありません。

という一文があり、私は「へー、Aurora MySQLは認証回数に制限ないんだ−」と受け取りました。

そして、下の方にあった下記一文は気にも止めていませんでした。

アプリケーションで最大数の接続が必要な場合は、IAM データベース認証を使用しないでください。

こちらは当時の日本語訳がイマイチで、現在は私のドキュメント改善要望が通ったのか、下記のように修正されています。

アプリケーションで 1 秒あたり 20 を超える新しい接続が必要な場合は、IAM データベース認証を使用しないでください。

一応原文も引用しておきます。

Don't use IAM database authentication if your application requires more than 20 new connections per second.

サポートの回答としては

  • Aurora MySQLでもこの20 new connections per second制限は適用される
  • よって多数のコネクション確立要求を一気に投げるようなアプリケーションには適さない

というものでした。

ユースケースによると思いますが、私の場合はこれは引っかかりうるつらい制約でした。

2. Node.js SDK + IAM Roleの組み合わせの問題

https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/RDS/Signer.html#getAuthToken-property

このドキュメントをよーく読む必要があります。

まず、

Examples:
Generating an auth token synchronously

という例があり、「へー、Nodeには珍しく同期で取得できるんだ」と捉えました。

ところがこれがクセモノで、IAMロールと併用する場合は非同期でないと取得できないのです。

Note: You must ensure that you have static or previously resolved credentials if you call this method synchronously (with no callback), otherwise it may not properly sign the request. If you cannot guarantee this (you are using an asynchronous credential provider, i.e., EC2 IAM roles), you should always call this method with an asynchronous callback.

ぇ・・・こんな注意書きあったっけ?(私がサポートケースを起票したから追記された気がしてならない)

結局、なまじ同期的な方法があったのでそれを使ったがために盛大にハマったのでした。