AWS Lambdaで環境変数とKMSを使ってkintoneのAPIアクセス情報を設定する

モチベーション

表題の通りですが、Lambdaで環境変数が利用できるようになり、またKMS(Key Management Service)を組合わせることで、暗号化・復号化の利用も簡単になりました(公式ブログ)。そこで、これまでLambdaのコード中に記述していたkintoneのAPIアクセス情報をこの環境変数とKMSの機能を使って書き換えたいと思います。

Developer Guideを参考に、早速設定してみたいと思います。

AWS KMSの設定

まず、KMSで今回kintoneにアクセスするLambda関数で利用するためのマスターキーを設定します。

Lambdaで利用するマスターキーの設定

「IAM」の「暗号化キー(コンソール画面リンク)」をクリックして、マスターキーの設定をスタートさせます。マスターキーはリージョン毎に管理されていますので、Lambdaを利用するリージョンが選択されていることを確認して、「キーの作成」をクリックします。
lambda_env1

キーの「エイリアス」を入力して、「次のステップ」をクリックします。
lambda_env2

キーの管理を許可するユーザーを選択します。今回はユーザー1名にチェックを入れ、「次のステップ」をクリックし、次の設定へ進みます。
lambda_env3

続いて、このキーを使用してデータを暗号化・復号化できるユーザーとロールを選択します。今回は「lambda_basic_execution」とLambdaを設定するユーザー1名にチェックを入れました。チェックしたら、「次のステップ」をクリックします。
lambda_env4

設定のプレビュー画面で内容を確認して、「完了」でマスターキーの設定終了です。「Allow access for Key Administrators(キー管理者)」や「Allow use of the key(キーユーザー・ロール)」を今一度確認しておきましょう。
lambda_env5

Developer Guideにも記載があるのですが、Lambdaの実行ロールに次のようなKMSのdecryptのポリシーをアタッチする必要性も考えたのですが、今回の設定の流れだとどうやら不要のようでした。先程のキーの利用ユーザーの選択部分が効いているのかと思います。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1483809323000",
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

AWS Lambda の設定

kintoneにアクセスするLambda関数の設定を行いますが今回次のパラメータを環境変数で渡すことにします。

項目 環境変数のkey 環境変数のvalue 環境変数の暗号化適用
kintoneのドメイン KINTONE_DOMAIN {subdomain}.cybozu.com
アプリID APP_ID {アプリID}
APIトークン API_TOKEN {アプリのAPIトークン} 適用

Lambda関数の設定

「Lambda」の設定は「Create function」からblueprintは「Blank」を選択し、Triggersも未選択で、メインの設定画面を開きます。

次のように設定します。
lambda_env6

インラインに貼り付ける今回のサンプルコードはこちらです。環境変数で指定したkintoneアプリに対して、レコード一括取得(GET/records)のkintone REST APIを実行する内容です。

'use strict';

const AWS = require('aws-sdk');

// APIをコールする汎用関数
const requestAPI = (url, method, headers, data) => {
  // リクエストボディは"object"もしくは"string"
  if (typeof(data) !== 'string') {
    try {
      data = JSON.stringify(data);
    } catch (e) {
      throw new Error('Request body is invalid. It must be "JSON object" or "string".');
    }
  }
  let requestFunc; // HTTP or HTTPSによって関数振り分け
  let urlParams = require('url').parse(url, false, false);
    // プロトコル判別
  switch (urlParams.protocol) {
    case 'https:':
      urlParams.port = 443;
      requestFunc = require('https');
      break;
    case 'http:':
    default:
      urlParams.port = 80;
      requestFunc = require('http');
      break;
  }

  // パラメータ生成・セット
  const options = {
    host: urlParams.hostname,
    port: urlParams.port,
    path: urlParams.path,
    method: method,
    headers: headers
  };

  // リクエスト
  return new Promise((resolve, reject) => {
    let req = requestFunc.request(options, (res) => {
      let body = []; // レスポンスボディセット用
      res.setEncoding('utf8');
      // データ転送時
      res.on('data', (chunk) => {
        body.push(chunk);
      });
      // データ転送終了時
      res.on('end', () => {
        // レスポンス
        let ret = [
          body.join(''), // ボディ
          res.statusCode, // ステータスコード
          res.headers // ヘッダ
        ];
        resolve.apply(this, ret);
      });
    });
    // エラー時
    req.on('error', (e) => {
      reject(e);
    });
    // リクエストボディを送信
    req.write(data);
    // リクエスト終了
    req.end();
  });
};

// KMS復号化の関数
const decrypt = (encrypted) => {
  const kms = new AWS.KMS();
  return kms.decrypt({
    CiphertextBlob: new Buffer(encrypted, 'base64')
  }).promise().then((data) => {
    return data.Plaintext.toString('ascii');
  });
};

// メインプロセス
exports.handler = (event, context, callback) => {

  const encryptedAPIToken = process.env['API_TOKEN'];
  const domain = process.env['KINTONE_DOMAIN'];
  const appId = process.env['APP_ID'];

  // APIトークンを取得(復号化)
  decrypt(encryptedAPIToken).then((APIToken) => {
    // リクエスト情報
    const url = 'https://' + domain + '/k/v1/records.json?app=' + appId;
    const method = 'GET';
    const headers = {
      'X-Cybozu-API-Token': APIToken
    };
    // APIリクエスト
    return requestAPI(url, method, headers, {});
  }).then((r) => {
    console.log(r);
    callback(null, r);
  }).catch((e) => {
    callback(e);
  });
};

メインプロセス部分を見ると、暗号化データから復号化して実値を得る「decrypt」という関数で環境変数として設定されたAPI_TOKENのデータキーからAPIトークンを取得し、APIをコールする「requestAPI」という関数ででkintone REST APIをコールしています。いずれの関数もPromiseを使って記述しています。

Lambda関数の設定を続けていきます。今回の肝になる部分ですね。環境変数で渡すパラメータ表を再掲しておきましょう。

項目 環境変数のkey 環境変数のvalue 環境変数の暗号化適用
kintoneのドメイン KINTONE_DOMAIN {subdomain}.cybozu.com
アプリID APP_ID {アプリID}
APIトークン API_TOKEN {アプリのAPIトークン} 適用

「Enable encryption key」をチェックを入れると「Encryption key」が選択できるようになりますので、先ほど設定したマスターキーのエイリアスを選択します。
更に、環境変数として「KINTONE_DOMAIN」、「APP_ID」、「API_TOKEN」を入力して、「API_TOKEN」については「Encrypt」をクリックして暗号化を行います。

lambda_env7

「API_TOKEN」の暗号化が完了すると「Encrypt」ボタンが「Decrypt」ボタンに置き替わります。
lambda_env8

設定も終盤です。ここで大切なのは、先ほど設定したKMSが利用可能な「lambda_basic_execution」のロールを選択することです。
lambda_env9

ここまで設定したら、「Next」をクリックして確認画面へ遷移させます。
lambda_env10

「Create function」をクリックして、Lambda関数の設定完了です。
lambda_env11

「Test」をクリックして、設定したLambda関数を起動してみます。
lambda_env12

CloudWatch Logsを見ると、kintoneのレコードが取得できています。成功です。
lambda_env13

まとめ・所感

LambdaからkintoneへのAPIアクセス時に、Lambdaの環境変数でアクセス情報を渡すことをやってみました。また、APIトークンについては、KMSを利用した暗号化を伴う設定も適用してみました。
LambdaにおけるKMSを用いた暗号化・復号化についてはググると紹介されている記事がしばしば見受けられましたが、暗号化をコマンドで実行して、Lambda中で復号化して利用するといった方法が取られていました。しかし、これからはLambdaの設定画面中でKMSによる暗号化を実行し、環境変数としてそのまま暗号化データ(ciphertext)を渡し、Lambdaで復号化して利用することが可能ですので、非常に容易かつ柔軟に設定できるようになります。

kintoneのアクセス情報の中でも、認証情報はやはり隠蔽することが望ましいので、これを機にベタ書きは控えるようにしていきましょう。

(おまけ)

環境変数を複数暗号化した際の書き方について質問がありましたので、Basic認証の情報を埋め込むケースとして、メインプロセス部分を書き換えてみます。環境変数を配列にして、Promise.all() と map で decrypte() を処理すると良いかと思いますので、こちらも参考になれば、幸いです。

// メインプロセス
exports.handler = (event, context, callback) => {
  // 暗号化パラメータを配列化
  const encryptedParams = [
    process.env['BASIC_USER'],
    process.env['BASIC_PASSWORD'],
    process.env['API_TOKEN']
  ];
  const domain = process.env['KINTONE_DOMAIN'];
  const appId = process.env['APP_ID'];

  // APIトークンを取得(復号化)
  Promise.all(encryptedParams.map((encrypted) => {
    return decrypt(encrypted);
  })).then((params) => {
    // 復号化パラメータを取得
    var basicUser = params[0];
    var basicPassword = params[1];
    var APIToken = params[2];
    // リクエスト情報
    const url = 'https://' + domain + '/k/v1/records.json?app=' + appId;
    const method = 'GET';
    const headers = {
      'X-Cybozu-API-Token': APIToken,
      'Authorization': 'Basic ' + (new Buffer(basicUser + ':' + basicPassword)).toString('base64')
    };
    // APIリクエスト
    return requestAPI(url, method, headers, {});
  }).then((r) => {
    console.log(r);
    callback(null, r);
  }).catch((e) => {
    callback(e);
  });
};

 

同じカテゴリーの記事