ホーム エンジニア AWS AWS LambdaのFunctionURLsでRedmineAPIを利用した話
AWS LambdaのFunctionURLsでRedmineAPIを利用した話
 

AWS LambdaのFunctionURLsでRedmineAPIを利用した話

はじめに

最近オートミール食のレパートリーが増えてきました。ゲームデザイン(GD)部クライアントエンジニアの中村です。

我々GD部ではプロジェクトのタスク管理にRedmineを利用しています。Redmineのタスクチケットは基本的にPM職が管理しており、CSVファイルから一括でインポートする機能がよく使われています。この機能はチケットの一括新規作成が可能ですが、一括更新には利用できませんでした。そこでこの度、CSVファイルを読み込み一括で更新する機能をPythonスクリプトで作成することとなりました。

制約

今回作成する「Redmine一括更新withCSV(仮称)」は以下の制約を課しました。

  1. 基本的にPM職が利用するものである
  2. WindowsやMacなどのプラットフォームに依存しない
  3. 低コスト

1については、利用者のPCにスクリプト実行環境を整えなければいけないという点で大きなハードルとなります。2については、プロジェクトごとにMac派とWindows派が別れているため必須要件となります。3については言わずもがな。

技術選定

これらの制約を考慮して、以下の3パターンを考えました。

  • AWS ChatbotとSlackを連携
  • botkitとSlackを連携
  • Lambda Function URLsをブラウザから実行

AWS ChatbotとSlackを連携

AWS Chatbotを利用するとSlackからaws-cliのコマンドを実行できます。そのため、Slackワークスペースから必要情報を入力するだけでLambda Invokeができるのではと考えました。

しかし、CSVファイルをアップロードしなければならないという点で問題がありました

  • Slackワークスペースにファイルアップロードができない
  • AWS ChatbotのcliではLambdaにファイル指定できるのはjsonだけ
  • AWS ChatbotのcliではS3にファイルアップロードできない

以上よりボツとなります。

botkitとSlackを連携

botkitはnodejsで記述するSlackのチャットボットです。Slackに送信したメッセージに反応し、そのメッセージに対する何らかの処理を実行することができます。そのため、SlackにCSVファイルをアップロードしたら自動でRedmineAPIを実行し更新させることができます。

しかしながらこの案ではbotkitをデーモン化して維持するためのサーバーリソースが必要となるため、コストの面でボツとなりました。(あと単純にAWSサービスを使いたかった)

Lambda Function URLsをブラウザから実行

AWS Lambda Function URLsは公開から1年が経過した割と新しい機能です。もともとLambdaはAPI Gatewayを経由してhttpから実行することができましたが、API Gateway自体がコスト面で高めだったためこういったユースケースでは利用していませんでした。

Lambda Function URLsの登場でhttp経由で簡単にLambdaを実行できるようになったため、折角なので利用していこうと思った所存です。Lambdaはコスト面で優秀であり、Function URLsにはIAM認証が搭載されているためセキュリティ面でも良い選択肢となりました。

Redmine一括更新withCSV(仮称)の構築

AWSサービス構成図

今回利用したAWSサービスは少ないので、以下のような非常にシンプルな図になります。

Redmine Update 構成図

使用したサービスとしては以下のとおりです。

  • Route53 – https証明書のためにドメイン取得
  • CloudFront – S3 Bucketに置いたSPAの配信
  • CloudFrontFunction – CloudFrontに対してBasic認証を設置
  • S3 Bucket – ブラウザに表示するためのReact-SPAにファイルアップロードができるFormを配備。
  • Lambda – CSVを解析してRedmineAPIを利用する。FunctionURLsを設定。

Lambda Function URLsのIAM認証

Lamdba Function URLsには認証せずパブリックにアクセス可能なNONE設定と、認証されたIAMユーザーとロールのみがアクセス可能なAWS_IAM設定の2つが用意されています。今回はセキュリティを固くする意味でもAWS_IAM設定で進めます。

まず、このLambdaFunctionに対するInvokeのみを許可するIAMポリシーを持ったユーザーを作成します。このIAMポリシーは公式ドキュメントにサンプルがあります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "InvokeLambda",
            "Effect": "Allow",
            "Action": "lambda:InvokeFunctionUrl",
            "Resource": "arn:aws:lambda:ap-northeast-1:000000000000:function:LambdaRedmineUpdaterKari",
            "Condition": {
                "StringEquals": {
                    "lambda:FunctionUrlAuthType": "AWS_IAM"
                }
            }
        }
    ]
}

このユーザーはマネジメントコンソールへのログインを許可せず、アクセスキー・シークレットアクセスキーの発行のみ行いました。このアクセスキーを使用してLambda FunctionURLsのIAM認証を行います。

FunctionURLsのIAM認証にはAWS Signature Version 4 (SigV4)が必要になるようです(公式ドキュメントより)。こちらについてはaws-sdk-js-v3に含まれているためyarnから簡単に利用開始できますが、公式に明確なサンプルコードがないためgithubのissueを参考に実装しました。

IAM認証に使用するアクセスキー、更新するCSV、実行するLambdaのホスト、更新先のRedmineエンドポイントをすべてフォームから入力することとしています。Javascriptコード上にはアクセスキー等秘匿情報が残らないので安全かと思います。

const signer = new SignatureV4({
  credentials: {
    accessKeyId: data.AwsAccessKeyId,
    secretAccessKey: data.AwsSecretId,
  },
  region: "ap-northeast-1",
  service: 'lambda',
  sha256: Sha256,
});
const parsedUrl = new URL(data.LambdaUrl);
const endpoint = parsedUrl.hostname.toString();
const path = parsedUrl.pathname.toString();
const req = new HttpRequest({
  hostname: endpoint,
  path,
  method: "POST",
  body: JSON.stringify(formBody),
  headers: {
    host: endpoint,
    "Content-Type": "application/json",
  },
});
const signed = await signer.sign(req);
const resp = await fetch(data.LambdaUrl, {
  method: req.method,
  body: req.body,
  headers: req.headers,
});

このようにすることでブラウザからLambda Function URLsを実行することができました。

無事、LambdaからPythonを使用してRedmineAPIも実行できました。

フロントページのBasic認証

今回S3バケットにはReactで作成したSPAを設置しています。独自ドメインを使用するためにCloudFrontを設置しているため、折角なのでCloudFront Functionを利用してBasic認証を実装しました。前提としてS3バケットにはCloudFrontのOAIアクセスのみ許可されています。

CloudFront Functionsの利用方法は公式ドキュメントに詳しく記述されています。Basic認証は以下のように記述しました。

 var response401 = {
  statusCode: 401,
  statusDescription: 'Unauthorized',
  headers: { 'www-authenticate': { value: 'Basic' } }
};
function handler(event) {
  var request = event.request;
  var headers = request.headers;
  var basicBase64 = '<user:pass> Base64';
  var authString = 'Basic ' + basicBase64;

  if (typeof headers.authorization === "undefined" || headers.authorization.value !== authString) {
    return response401;
  }

  return request;
}

CloudFront Functionsは Lambda@Edge より少し機能が低下しているように感じますが、簡単なIP制限や別ページへのリダイレクト、Headerの追加などがシンプルに利用できます。無料利用枠もあるので小規模にも使いやすいでしょう。

最後に

今回はLambda Function URLsを利用してRedmineAPIを実行しCSVから一括更新する仕組みを作成しました。

こういったちょっとした便利機能のPython/Nodeスクリプトが必要になったとき、エンジニア以外の環境でその実行環境を用意してあげるよりもLambdaで実行できるようにしたほうがメンテナンスもしやすくなるでしょう。また、ブラウザから実行できるので複数人からの実行も当然可能です。

Lambda Function URLsにIAM認証を適用すればセキュリティ的にもかなり安全です。また、IAMユーザーのポリシーも該当のLambdaの実行に限られているため、アクセスキーが流出しても影響を受けるのはLambdaだけです。(流出しないのが一番ですが)

以上、またちょっとしたスクリプトが必要になったときこのような形で機能を用意しようと思います。

記事を共有

最近人気な記事