echo("備忘録");

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

【AWS】DynamoDBにリソースポリシーが追加されたので試してみた(※3/25修正)

※3/25 17:00 内容を一部修正しました

今回のお題

Amazon DynamoDB(以下「DynamoDB」)が、リソースポリシーでのアクセス制御に対応したので、試してみた

本題

3/21(日本時間)に、DynamoDBにリソースポリシー機能が追加されたことがAWS公式より発表されました。

aws.amazon.com

これにより、DynamoDBもS3やAPI Gatewayと同様に、リソースポリシーによりアクセス制御(Allow, Deny)を行うことが可能になりました。

そこで、このリソースポリシーの挙動を試してみました。

参考リンク

※「ポリシーの評価論理」については、IAMポリシーによるアクセス制御を理解する上でバイブルともいうべき資料なので、ぜひ一読することをお勧めします。(特に「アカウント内でのリクエストの許可または拒否の決定」項目内のフローチャート図)

※このフローチャートの日本語訳が見たいという方は、下記の方が日本語に訳して下さっているので、こちらを参照してください。

qiita.com

前提

今回は「LambdaからDynamoDBテーブルのScan(=全データ取得)を実施する」という方法でアクセスを検証します。

また、以下2つのLambdaを作成します。(ソースは下記参照)

  • allowFunction:アクセス可能なことを確認するLambda
  • denyFunction:アクセス不可能なことを確認するLambda

なお、DynamoDBのリソースポリシーはまだCloudFormationに対応していないので、今回はマネジメントコンソールから直接入力します。(もちろんAWS CDKも未対応)

【3/25 17:00修正】CloudFormationに対応済です。(表示言語を「English」にしないと表示されないので、勘違いしてました)
AWS::DynamoDB::Table ResourcePolicy - AWS CloudFormation

なおAWS CDKは、3/25 17:00時点で未対応です。

import { Context, APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient, DynamoDBClientConfig, GetItemCommand, GetItemCommandInput, GetItemCommandOutput } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, ScanCommand, ScanCommandInput, ScanCommandOutput } from '@aws-sdk/lib-dynamodb'
  
const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {  
  const config: DynamoDBClientConfig = {};
  const client = new DynamoDBClient(config);
  const dcClient = DynamoDBDocumentClient.from(client);
  
  const param: ScanCommandInput = {
    TableName : <テーブル名>,
  };
 
  const command = new ScanCommand(param);
  const data = await dcClient.send(command);
  
  console.info(JSON.stringify(data.Items));
  
  return;
};

検証1:Lambdaにアクセス許可がない場合

まずはLambdaにDynamoDBへのアクセス許可がない場合*1の検証です。

allowFunctionおよびdenyFunction共に、DynamoDBのアクセス許可はない状態です。(CloudWatch Logsのみ) *2

リソースポリシー未設定の場合

まず、DynamoDBのリソースポリシーが未設定の状態(=初期状態)でLambdaを実行します。

この場合、ポリシーが一切存在しない状態なので、結果「暗黙のDeny」が適用され、allowFunctionおよびdenyFunction共に「AccessDeniedException」が発生します。(これは従来通り)

リソースポリシーを設定する

次にリソースポリシーを設定します。

マネジメントコンソールの「テーブルについてのリソースベースのポリシー」-「テーブルポリシーを作成」から、以下の「allowFunctionのアクセスを許可する」リソースポリシーを作成します。(Principalは「Lambda関数のARN」ではないので注意です)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowStatement",
            "Effect": "Allow",
            "Principal": {
              "AWS": "<allowFunctionのIAMロールのARN>"
            },
            "Action": "dynamodb:Scan",
            "Resource": "<DynamoDBテーブルのARN>"
        }
    ]
}

上記リソースベースポリシーを設定後、再度Lambda関数を実行すると、allowFunctionは正常終了し、ちゃんとテーブルの全データが取得できます。

もちろん、denyFunctionは引き続きAccessDeniedExceptionが発生します。

このことから、リソースポリシーがちゃんと機能してそうです。

検証2:Lambdaにアクセス許可がある場合

次にLambdaにDynamoDBへのアクセス許可がある場合の検証です。

allowFunctionおよびdenyFunctionに、AWS管理ポリシーの「AmazonDynamoDBFullAccess」を一時的に付与します。*3

リソースポリシー未設定の場合

まず、DynamoDBのリソースポリシーが未設定の状態(=初期状態)でLambdaを実行します。(先程のポリシーが残っている場合、一度ポリシーを削除してください)

この場合、Lambda関数の「AmazonDynamoDBFullAccess」ポリシーが適用され、allowFunctionおよびdenyFunction共に正常終了し、テーブルの全データが取得できます。(これも従来通り)

リソースポリシーを設定する

次にリソースポリシーを設定します。

先程の同様の手順で、以下の「denyFunctionのアクセスを拒否する」リソースポリシーを作成します。(今回Principalに設定するロールのARNは「denyFunction」のものです。allowFunctionではありません)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowStatement",
            "Effect": "Deny",
            "Principal": {
              "AWS": "<denyFunctionのIAMロールのARN>"
            },
            "Action": "dynamodb:Scan",
            "Resource": "<DynamoDBテーブルのARN>"
        }
    ]
}

上記リソースベースポリシーを設定後、再度Lambda関数を実行すると、denyFunctionのみAccessDeniedExceptionが発生します。

そしてallowFunctionは引き続き正常終了し、テーブルの全データが取得できます。

このことから、Lambdaにアクセス許可がある場合もリソースポリシーがちゃんと機能してそうです。

まとめ

以上、DynamoDBのリソースポリシーの検証結果でした。

S3やAPI Gateway同様、DynamoDBにもリソースポリシーを付与できるようになり、よりアクセス制御がやりやすくなりました。

また、ポリシーを二重に付けることで、ポリシー設定ミスによる思わぬ事故の予防策になるので、有効に活用したいですね。*4

ちなみに、今回は簡単な検証のみでしたが、クラスメソッドののんピ氏がかなり突っ込んだ部分まで調査したブログを書いてますので、もし詳しく知りたい方はそちらもご参照ください。

dev.classmethod.jp

それでは、今回はこの辺で。

*1:正確には「Lambdaに割り当てられたロールにDynamoDBテーブルへのアクセス許可ポリシーがアタッチされていない場合」になります。

*2:AWS管理ポリシー「AWSLambdaBasicExecutionRole」は、CloudWatch Logsへの書き込みのみ許可するポリシーです

*3:名前の通りDynamoDBの全権限が付与されるので、扱いは要注意です

*4:もちろん、うっかりポリシー設定をミスって「(ルートユーザー以外で)アクセスできなくなっちゃった...」という事故が起こる可能性もありますが...

【AWS】CloudFormationで「A previous rotation isn’t complete. That rotation will be reattempted」エラーでデプロイできない場合の対処法

本日のお題

AWS環境で、AWS Secret Manager(以下「Secret Manager」)を使用して、Amazon RDS(以下「RDS」)のシークレット情報管理&シークレットローテーションを使用している場合に、AWS CloudFormation(以下「CFn」)でデプロイを実施すると、下記エラーが発生して、デプロイが失敗する場合があります。

「A previous rotation isn’t complete. That rotation will be reattempted(訳:前回のシークレットローテーションが完了していません。ローテーションは再試行されます)」

このエラーの原因と対処方法、及びAWS CDKでそれを実装する方法を記載します。

参考リンク

いきなり結論から

結論としては、ほとんどの場合「以下のいずれかの理由で、ローテーション用Lambda関数(以下「Lambda関数」)がSecret ManagerやRDSにアクセスできず、結果シークレットのローテーションが完了できなかったから」が原因になります。

  • Lambda関数が存在するサブネットに、Secret Managerへ到達するためのルーティングがない
  • Lambda関数がRDSへのアクセスを許可されていない

なおLambda関数のログを見ると*1、大抵何かしらのログが出力されており、正常終了していないことが分かります。(一番多いのが下記の「Lambda関数のタイムアウト」)

Task timed out after 30.03 seconds

Lambda関数が存在するサブネットに、Secret Managerへ到達するためのルーティングがない

これですが、Lambda関数Secret Managerに到達するためには、Lambda関数が存在するサブネットが以下いずれかの条件を満たす必要があります。

  • NAT Gatewayが存在するサブネットへルーティング可能であること
  • ポート443からのインバウンド通信を許可するSecret Manager用のインターフェースエンドポイントが存在する事

今回は、後者の方法について説明します。(この問題が起こる=該当サブネットが分離サブネット *2 の可能性が高いので)

AWS CDKで実装する

AWS CDKで後者の方法を実装するには、下記コードでOKです。

import { aws_ec2 as ec2 } from 'aws-cdk-lib';
  
// 省略してますが、vpcはnew ec2.Vpc(...)で作成したVPCになります
// 
// VPCエンドポイントに割り当てるセキュリティグループを作成する
// アウトバウンドは全許可  
const secretManagerEpSg = new ec2.SecurityGroup(this, 'SecretManagerEpSg', {
  vpc,
  allowAllOutbound: true,
  description: 'Security Group for VPC Endpoint for SecretManager',
});
  
// 上記セキュリティグループにポート443からのアクセスを許可する
secretManagerEpSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443));
  
// Lambda関数が存在するサブネットにSecret Manager用の
// インターフェースエンドポイントを作成する。
// それに先程作成したセキュリティグループを割り当てる  
const secretManagerEp = vpc.addInterfaceEndpoint('SecretManagerEndpoint', {
  service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
  subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
  securityGroups: [secretManagerEpSg],
});

Lambda関数がRDSへのアクセスを許可されていない

これですが、Lambda関数がローテーションを実施するには、当然RDSへアクセスする必要があります。

なので、Lambda関数はRDSへのアクセスを許可されていないといけません。(ロール・ポリシーはもちろん、ネットワークレベルでも許可されている必要がある)

VPCの場合「Lambda関数のセキュリティグループが、RDSのセキュリティグループのインバウンドとして許可されている」必要があります。

AWS CDKで実装する

AWS CDKで上記の方法を実装するには、下記コードでOKです。

import {
  aws_rds as rds,
  aws_secretsmanager as sm,
} from 'aws-cdk-lib';  
  
// RDSクラスターのcredentialsに紐づけたシークレットの場合  
const dbAdminSecret = new sm.Secret(this, 'DbAdminSecret', {
  ...(定義は省略)      
});  
  
const cluster = new rds.DatabaseCluster(this, 'AuroraCluster', {
  credentials: rds.Credentials.fromSecret(dbAdminSecret),
  ...(その他定義は省略)
});
  
// ローテーション作成  
new sm.SecretRotation(this, 'DbAdminSecretRotation', {
  secret: dbAdminSecret,
  target: cluster,
  ...(その他定義は省略)     
});
  
// その他のシークレットの場合
const dbUserSecret = new sm.Secret(this, 'DbUserSecret', {
  ...(定義は省略)    
});
  
// ローテーション作成
new sm.SecretRotation(this, 'DbUserSecretRotation', {
  secret: dbUserSecret,
  target: cluster,
  ...(その他定義は省略)     
});
  
// 最後、シークレットをクラスターにアタッチするのを忘れないように!
// これを忘れると正常にデプロイできません 
dbUserSecret.attach(cluster);

「あれ、セキュリティグループは?」と思った人がいるかもしれませんが、上記コードを実装すれば、下記を全部自動で行ってくれます。便利!

  • ローテーション用Lambda関数、及びそのセキュリティグループの作成
  • ローテーション用Lambda関数のRDSへのアクセス許可
    • RDSのセキュリティグループのインバウンドへローテーション用Lambda関数のセキュリティグループの割り当て

上記の対処をしてもやっぱりエラーになるんだけど!という場合の対処法

上記の対応を実施しても、デプロイ時にやっぱり同じエラーが発生するケースがあります。

「エラーに対処するためのコードを実装したのに、そのコードのデプロイ時にエラーが出て反映できない」なんていう本末転倒な事態が起こるわけです。

この現象ですが、SecretRotationrotateImmediatelyOnUpdate プロパティ*3がデフォルトでtrueになっていることが原因なので、明示的に false を指定することで回避できます。

なおtrue/falseによる挙動の違いは下記となります。

  • true: CFnデプロイ時にローテーションを実施する
  • false: CFnデプロイ時にローテーションは実施せず、automaticallyAfter で設定した日数が経過したらローテーションを実施する(デフォルトは30日)

それ以外の場合

それ以外の場合、おそらくポリシー関連(ローテーション関数のロールににSecret Managerのシークレット更新、及びそれのRDS適用などのポリシーがアタッチされていない...など)が挙げられます。

ただし今回紹介したAWS CDKのソースを適用すれば、自動で適切なポリシーをアタッチしてくれるので、その辺のエラーは発生しないはずです。

もしカスタムコードを適用して「Access Denied」系のエラーが出た場合は、そのあたりを確認してみてください。

それでは、今回はこの辺で

*1:ローテーション用のLambda関数はマネジメントコンソールの [Secret Manager] - [ローテーション] - [ローテーション関数] から確認できます

*2:インターネットに接続しないサブネット

*3:シークレット更新時(=CFnデプロイ時の更新も含む)にローテーションを実施するかどうか、の設定

【VS Code】音声入力でコーディングできるかどうかを検証してみた

今回のお題

Visual Studio Code(以下「VS Code」)が、今年2月のアップデートで音声入力に対応しました。(「Voice dictation in editor」)

code.visualstudio.com

そこで今回は、この音声入力機能の紹介と「実際に音声入力でコーディングはできるのか?」を試してみました。

アジェンダ

  • 音声入力の導入手順
  • 音声入力でコーディング
  • 生成AIの力を借りる
  • AIチャットで音声入力を試す
  • まとめ&おまけ(新年のあいさつ)

音声入力の導入手順

まず、VS Code拡張機能の「VS Code Speech」をインストールします。
VS Code Speech - Visual Studio Marketplace

その後VS Codeの「設定」で、「Accessibility > Voice:」以下の下記3項目について、必要に応じて下記項目を設定します。(「Speech Language以外」は、デフォルト値のままで問題ないと思います)

項目 説明 デフォルト値 備考
Speech Language 音声入力する言語 英語(米国) プログラム言語ではない
Speech Timeout 音声入力を停止した後、入力した音声をファイルに出力するまでの待ち時間(ミリ秒) 1200 音声入力を停止した後、この時間が経過したら入力した音声をファイルに出力する
Keyword Activation 「Hey, Code」と音声入力することで音声入力モードを有効にするかどうか(Google Homeの「OK, Google」みたいな機能) off(しない) ・有効にする場合、いくつかモードがある。(本記事では使用しないので省略)
・有効にする場合「Hey, Code」の音声はファイル出力されない

最後に、何かのファイルを開いて「Ctrl + Alt + V」を押すとマイクのアイコンが表示され、音声入力モードが有効になります。(終了する際はEscキーを押す)

また、先述の「Keyword Activation」を有効にしている場合、「Hey, Code」と音声入力しても音声入力モードが有効になると思います。(未検証)

音声入力でコーディング

では、実際に音声入力でコーディングを試してみます。

サンプルとして、少し前にX(Twitter)で話題になった「うるう年を判定する関数」をコーディングしてみます。(下記コードを参照)

// yearは西暦
function isLeapYear(year: number): boolean {
  if (year % 400 === 0) return true;
  if (year % 100 === 0) return false;
  return year % 4 === 0; 
}

そこで、上記のコードを音声入力でコーディングしようとしたのですが...

結果としては、なかなか音声をうまく認識せず、「自分で書いた方が速い」という結果になりました...(自分の英語力が残念なせい?) *1

とにかく、音声認識の精度が良くないのがネックでした。
あと「()」「{}」「,」など、記号に関してはどうしようもないですね...

ただ、上記のことを割り切ったうえで「ある程度のレベルまでは音声入力で対応する」のであれば、問題ないと思います。

ちなみにこれ、めっちゃ流暢に英語話せる人やアメリカ人の方が音声入力したら、思った通りのコードになるんだろうか...?

生成AIの力を借りる

という訳で、音声入力でコーディングはうまくいきませんでした。(英語力が残念なせいもあるかもしれないけど)
が、今は「生成AI」という便利なものが存在します。

そこで、今度は以下の方法を試してみます。

  • コメントに関数の内容を記載する。(ここを音声入力する)
  • 実際の関数はGitHub Copilotに生成させる。(提案は全て最初のものを採用する)

これなら日本語でも大丈夫なので、英語力が残念でも関係ないです。

こちらに関しては、結果的にはちゃんと関数を作成してくれました。(ちょっとコメントが冗長な気がしますが)

という訳で、音声入力でコーディングするのは、音声認識の問題でハードルが高そうなので、現段階では「処理のコメントを音声入力し、実際のコード作成はAIに任せる」のが良さそうな気がします。

てか、それって結局AIにコード作成してもらうのとあまり変わらないような...

AIチャットで音声入力を試す

また先述の「VS Code Speech」のページの「Getting Started」に「GitHub Copilot Chatでも音声入力が可能」という記載があったので、さっそく試してみました。

確かに、概ねGitHub Copilot Chatでは音声入力がいい感じに動作しているので、これは使えそうですね。

なお残念ながら、Amazon Q(「AWS Toolkit」拡張機能で使用可能)では、音声入力は動作しませんでした。

まとめ

以上、音声入力でコーディングできるかどうかの実証結果でした

結果的には、直接コーディングは難しそうですが、生成AIを併用する分には便利になりそうです。

また、音声認識の精度が改善されれば、いずれ音声入力だけでコーディングが可能になる時が来るかもしれませんね。

おまけ(新年のあいさつ)

これが今年最初の記事ですが、気が付けが前回の記事を投稿してから、もう80日近く経過してるんですね。

本当は今年も1月からアウトプット活動を行う予定だったのですが、昨年末に体調を崩してしまい、ブログ・登壇含め、アプトプット活動が全然できませんでした。

が、少しづつではありますがようやく復調の兆しが見えてきたので、そろそろ可能な範囲でブログ・登壇などのアプトプット活動を再開していこうと思います。

もう3月ですが、今年もよろしくお願いします。

それでは、今回はこの辺で。

*1:本当は動画で見せたかったのですが、はてなブログは動画のアップロードができないため断念。他も同じ

JAWS-UGで人生が変わった(?)話

はじめに

この記事は、JAWS-UG(AWS Users Group – Japan) Advent Calendar 2023 25日目(最終日)の記事です。

qiita.com

JAWS-UGって

いまさら書くまでもないですが、AWS (Amazon Web Services) が提供するクラウドコンピューティングを利用する人々の集まり(コミュニティ)です。

jaws-ug.jp

いわばAWS好きな人たちのコミュニティです。(メインは技術者ですが、もちろん技術者以外でもウェルカムです。)

JAWS-UGで人生が変わった?

いわばJAWS-UGは「(技術者)コミュニティ」なのですが、実際JAWS-UGに参加して「人生が変わった」と言っている人もいます。

例えばAWS HEROの松井さん、JAWS Festa 2023 実行委員長の阿部さんなどで、実際お二人はLTで「JAWS-UGで人生が変わった話」みたいな発表をしています。

技術者コミュニティって、そんなにすごいんでしょうか?

かくいう自分のその一人

これに対しては、自信をもって「そんなにすごい」と言い切れます。

なぜなら自分も、JAWS-UG(正確にいうと技術者コミュニティ)で人生が変わった一人だからです。

コミュニティ活動を行うようになった経緯

私は2019年に、業務都合で本格的にAWSを触り始めました。

が、当時は「AWSって何?」「クラウドって何?」状態でした。

ググっても当然ネットの知識は断片的なものしかなく、どうしよう...さっぱりわからんと思っていました。

そんな時、たまたまServerless Tokyoを見つけまして、それに何回か参加して色々情報を得たおかげで、非常に助けになりました。

また当時は技術者コミュニティなんて知らなかったんですが、「あ、クラウドってこういう技術者同士のつながりが持てるんだ、いいなあ」と感じまして、そこからコミュニティに積極的に参加するようになりました。

そしてその過程でJAWS-UGも知りましたし、そこからもう完全にクラウド沼にはまっていきました。

コミュニティは、モチベーションを与えてくれる存在

また、単にインプットするだけではなく、アプトプットもするようになりました。

私の登壇デビューは上記Serverless Tokyoなのですが、当時は割とレアだった?Serverless Frameworkでの発表ということもあり、結構レスポンスを頂き、励みになりました。(その時の資料が下記)

www.slideshare.net

先日のServerless Tokyoで「自分のアウトプットは、誰かのインプット」という名言が生まれたのですが、アウトプットすること自体が自分のインプットにもなりますし、またそれが誰かのインプットになりレスポンスがたくさんもらえると、モチベーションアップにもなりますし、励みになります。

実際、これをきっかけにJAWS-UG・VSCodeJP・JSConf JPなど数々のコミュニティに参加したり、登壇させて頂く機会も増え、顔を覚えてもらったり、色々お声をかけて頂くことも増えました。
そういう意味でも、私はコミュニティで間違いなく人生が変わりましたし、良かったなあと思っています。

というか、多分そういうコミュニティ活動を行っていなかったら、自分も今頃どうなっていたんだろうな...と思います。

共通のつながりがもてるという素晴らしさ

また、JAWS-UG然りServerless Tokyo然りですが、「共通の認識(例えばAWS)を持つメンバーとつながりが持てる」というのも良い点だと思います。

実際、私はJAWS-UGでかなりつながりが増えて、大変ありがたいことだなあと思っています。

大きいイベント(AWS Summit・re:Invent・JAWS Fastaなど)でそういうメンバーとリアルに会えるのも楽しみで、そのために参加する意欲も増えますし、もちろんそういうメンバー同士で情報交換したり、意見交換したり、切磋琢磨しあうことでモチベーションもかなり上がります。

なにより、普段の業務では絶対会えない全国津々浦々の人たちと意見交換したりできるって、本当にめちゃくちゃ貴重な機会だと思います。

これまた先日のServerless Tokyoで「イベント後の飲み会こそ、最大のインプットの場である」という名言が生まれたのですが、本当にその通りだと思います。

そういう意味で「お山の大将」にならず、常に最新の情報を得たり情報をアップデートする意味でも、JAWS-UGのようなコミュニティに参加する事が大切だなあと実感しています。

まずは参加してみよう

現在私はJAWS-UG CDK支部、及びJAWS-UG 名古屋に深く関わらせてもらっています。

もちろん自分がCDKが好きだったり、名古屋が地元だからということもあるのですが、それを抜きにしてこういうコミュニティに参加する事がモチベーションアップにつながるし、何より楽しいから参加しています。

それこそ、東京とか遠方のイベントに参加すると結構な金額のお金が必要ですが、それでもそれを払ってでも得るものが大きい&やっぱり楽しいから参加しています。(今年は九州&北海道にも遠征しましたし)

もちろん、いきなりそれはさすがに気が引ける...と思いますが、もし少しでも「コミュニティ」に興味を持たれたら、まずは出来る範囲でいいので、少しずつ参加してみて、自分が楽しかったら続けてみるといいと思います。(コミュニティはあくまで有志であり、強制するものはありませんので)

でも、もしJAWS-UGなりコミュニティに少しでも興味を持ったら、まずは少しでもいいのでアクションを起こしてみてはいかがでしょうか?

もしかしたら、あなたも人生が変わるかもしれませんよ?

という訳で、今回はこの辺で。

なぜAWS CDKでエラーハンドリングができないのか

はじめに

この記事は AWS CDK Advent Calendar 2023 23日目 の記事です。

qiita.com

概要

AWS CDKは、AWS公式のIaC(Infrastructure as Code)ツールということもあり、AWSでIaCを使ったインフラ構成管理を行いたい場合にお薦めで、プログラミング言語(TypeScript, Pythonなど)でコードを記載するので、アプリエンジニアにもおすすめのツールです。

その一方で「コードと違う挙動をする」と感じることがある方も多く、特に「エラーハンドリングが効かない!」と感じた人がかなり多いようです。(私はJAWS-UG CDK支部に結構関わらせてもらっていますが、実際その中でそういう話を聞くことが多いです)

そこで今回なぜ「AWS CDKでエラーハンドリングができないのか」の仕組みを記載したいと思います。

具体例

例えば、下記のコードです。
下記のコードでは、以下の挙動を想定しています。

  • パラメータストアからDockerイメージのイメージタグを取得する
  • 上記が未設定の場合エラーになるので、その際は固定イメージを設定する
  • 上記イメージタグのイメージからLambda関数を参照する
// ※注意!このコードは正しく動きません
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { Repository } from 'aws-cdk-lib/aws-ecr';
import { aws_lambda as lambda } from "aws-cdk-lib"
  
let imageTag = '';
  
try {
 // パラメータストアから最新Dockerイメージのイメージタグを取得
 imageTag= StringParameter.valueForStringParameter(this, 'IMAGE_TAG');
} catch (e) {
 // もし未設定だったらエラーになるので、その際は固定値を設定
 imageTag = 'initial';
}
  
const repo = new Repository(this, 'EcrRepo');
  
new lambda.DockerImageFunction(this, 'AssetFunction', {
  code: lambda.DockerImageCode.fromEcr(repo, {
    // 上記イメージタグのイメージからLambda関数を参照する
    tagOrDigest: imageTag,
  }),
});

しかし実際にはパラメータストアにイメージタグがない場合、deploy時に「パラメータストアに該当のキーが存在しない」エラーで強制終了してしまいます。

「ちゃんとtry~catchで判定しているのになんでだ!」となってしまうわけです。

AWS CDKの挙動を理解する

まずはAWS CDKの挙動について。

AWS CDKの挙動は大きく分けて、以下の2つです。(もちろん、正確にはもっとたくさんある)

合成(Synthesize、以下「synth」)
  • AWS CDKのソースコード(Constructsなど)から、CloudFormationテンプレートファイル(json/yaml)を作成する
  • CLIコマンドのcdk synth に該当
    • 正確には cdk synthesize コマンドだが、大抵省略形で書く
  • プログラミング言語の制御が効く(if文とか)
展開(Deploy、以下「deploy」)
  • synthで作成したCloudFormationテンプレートファイルを使用して、CloudFormationにデプロイを行う
  • CLIコマンドのcdk deploy に近い(同じではない)
    • cdk deploy はsynthの処理も一緒に行うため(synth→deploy)
  • プログラミング言語の制御が効かない

これを踏まえて、synth時とdeploy時で「なぜエラーハンドリングが効かないのか」を説明します。

synthでエラーハンドリングが効かない理由

まず前提として、synth時にはAWSへのアクセスは行われません。(ファイル変換のみ)
したがって、各種リソースのプロパティ(ARNなど)の取得も行われません。

じゃあARNなどが設定されている箇所ではどうしているかというと、「トークン(Token)」という一時的な値に置き換えられてます。

なのでこの「トークン」について説明します。

トークンを理解する

トークンについては、AWS CDKの公式リファレンスにも載っています。 docs.aws.amazon.com

Represents a special or lazily-evaluated value.

Can be used to delay evaluation of a certain value in case, for example, that it requires some context or late-bound data. Can also be used to mark values that need special processing at document rendering time.

Tokens can be embedded into strings while retaining their original semantics.

訳:
特別な、あるいは遅延評価された値を表す。

例えば、ある値がコンテキストや遅延バウンドデータを必要とする場合に、その値の評価を遅延させるために使用することができます。また、ドキュメントのレンダリング時に特別な処理が必要な値をマークするために使用することもできます。

トークンは、元のセマンティクスを保持したまま文字列に埋め込むことができます。

要はARNのような「deployしないとわからない」ような値について、一時的に仮の値を設定するために使用されているのがトークンです。

synthでエラーハンドリングが効かない理由(結論)

そしてこのトークンは「Token[...]」のような文字列値になっています。(console.logなどで出力すると分かります)

ここがポイントで、例えば「具体例」のケースだと、StringParameter.valueForStringParameter() の戻り値として 「Token[...]」 という文字列が返却されるため、特にエラーにはなりません。

したがって、当然エラーハンドリングも行われません。

説明が長くなりましたが、これが「synthでエラーハンドリングが効かない理由」です。

deployでエラーハンドリングが効かない理由

次にdeploy時にエラーハンドリングが効かない理由ですが、これは単に「jsonyamlファイルにエラーハンドリングなんて機構がないから」になります。

AWS CDKの挙動」で説明した通り、deployはCloudFormationテンプレートファイル(json/yaml)を使用します。

当然jsonyamlにエラーハンドリングなんて機構がない(そもそもプログラミングコードではない)ため、deploy時にエラーが発生しても、エラーハンドリングなんてできず、結果としてエラーで終了します。

と、ここまで長々とCDKでエラーハンドリングが効かない理由を記載しましたが「CDKでは、AWSリソースの有無で処理を分岐することはできない」と考えておくとよいかもしれません。(100%断定はできませんが...)

AWSリソースの有無で処理を分岐する方法

といっても、実際は「AWSリソースの有無で処理を分岐したい」というケースがあると思います。

その場合に一番手っ取り早いのが「AWS CLIを使う方法」です。(具体例は下記ソース参照)

// 「具体例」と同じimport分は省略
import { execSync } from 'child_process';
    
const getParameterStoreValue = (keyName: string, region: string): string => {
   const command = `aws ssm get-parameter --name ${keyName} --region ${region} --query "Parameter.Value" --output text`;
   
   try {
     const value =  execSync(command).toString().trim();
     return value;
   } catch (error) {
     return 'initial';
   }
 };  
  
const imageTag = getParameterStoreValue('IMAGE_TAG', 'ap-northeast-1');
  
// あとは同じなので省略

これならAWS CDKの外部での処理なので、ちゃんとリソースの有無で判定できますし、エラーハンドリングも効きます。

他にもやり方はあると思いますが、一例として。

まとめ

以上、AWS CDKでエラーハンドリングができない理由でした。

といっても、理由さえわかってしまえばそこまで難しい話ではないので、色々対処は出来ると思います。

AWS CDK、慣れると使いやすくて色々便利ですし、何より現在でも頻繁にアップデートがされている便利なツールですので、ぜひ導入してインフラ管理を便利なものにしてみてください。

それでは、今回はこの辺で。

【AWS CDK】【AWS Lambda】Lambdaオーソライザを実装する

はじめに

これは「AWS Lambda と Serverless Advent Calendar 2023」 16日目の記事です。

qiita.com

今回のお題

  • Lambdaオーソライザを使用し、API GatewayからLambdaを実行する前段で認可処理を実施する
  • LambdaオーソライザはRequestベースのものを使用する
  • Lambdaオーソライザ含め、API GatewayからLambdaのリソースを全てAWS CDKで作成する

Lambdaオーソライザって何?

API Gatewayにリクエストを送信された時に「特定のリクエストにのみ対象のLambdaを実行させたい」という認可処理を行いたい時があります。

この「特定のリクエスト」のチェックを行う機構が「オーソライザ」になります。
そして、このオーソライザをLambdaのソースコードで実装したものが「Lambdaオーソライザ」です。

API Gatewayでは、オーソライザとしてこの「Lambdaオーソライザ」を使用することが可能です。

そこで、今回はこのLambdaオーソライザを実装する処理について記載します。

具体的なユースケースは?

具体的なユースケースの例としては、以下の通りです。

  • ログインしているユーザーにのみ該当のLambdaを実行させる
  • アプリ上で何らかの権限を有しているユーザーにのみ該当のLambdaを実行させる

Lambdaオーソライザの種類

Lambdaオーソライザには、以下の2種類があります

種類 説明 送信する値 備考
TOKENオーソライザ JSON ウェブトークン (JWT) やOAuth, SAMLなどのベアラートークンでの認可を行う トークンの値
Requestオーソライザ リクエスト情報として渡された各種情報を元に認可を行う 各種リクエスト情報 例えば、Authorizationヘッダの値を元にログインチェックを行う、など

なお、今回はRequestオーソライザのみを扱います。(Tokenオーソライザは扱いません)

Tokenオーソライザについては、下記のブログが参考になりますので、こちらをご参照ください。

参考サイト

Lambdaオーソライザの実装

Lambdaオーソライザの実装ですが、いきなりサンプルコードを記載します。
※基本的には「参考サイト」に記載したAWS公式サイトのコードとほぼ同じです。

import { Context, APIGatewayRequestAuthorizerEvent, APIGatewayAuthorizerResult } from "aws-lambda";
  
const generateAuthorizerResult = (effect: string, resource: string): APIGatewayAuthorizerResult => {
  const result = {
    principalId: 'Authorizer',
    policyDocument: {
      Version: "2012-10-17",
      Statement: [{
        Action: 'execute-api:Invoke',
        Effect: effect,
        Resource: resource,
      }]
    }
  } as APIGatewayAuthorizerResult;
  
  return result;
}
  
export const handler = async (event: APIGatewayRequestAuthorizerEvent, context: Context ): Promise<APIGatewayAuthorizerResult> => {
  
  const param = event?.headers?.Authorization ?? '';
  console.log(`param is ${param}`);
  
  if (param === 'allow') {
    return generateAuthorizerResult('Allow', event.methodArn);
  } else if (param === 'deny') {
    return generateAuthorizerResult('Deny', event.methodArn);
  } else {
    throw new Error('Unauthorized');
  }
};

重要なのは以下の点。(前者は問題ないと思うので、以後は後者のみ記載)

  • 未認証(ステータス:401) を返したいときは、Unauthorized エラーをスローする
  • それ以外は APIGatewayAuthorizerResult 型のオブジェクトをreturnする

後者の APIGatewayAuthorizerResult 型オブジェクトで最も重要なのは policyDocument.Statementの「Effect」 で、これを「Allow」にすれば許可、「Deny」にすれば不許可です。

またActionやResourceも設定できるので、「最小権限の原則」に従い、Allowする場合はここも設定しておくとよいと思います。(実際には上ソースのように「Resource」にはevent.methodArnの値をそのまま指定すればOKです)

なおDenyの場合、ActionやResourceはどちらも"*"でもいいと思います。(もちろん指定してもいい)

ちなみに APIGatewayAuthorizerResult 型オブジェクトについては、下記AWS公式サイトに詳しく載っているので、そちらも参照してください。

Amazon API Gateway Lambda オーソライザーからの出力(AWS公式サイト)

AWS CDKでLambdaオーソライザを定義する

次はLambdaオーソライザをAWS CDKで作成する方法です。

// Lambdaオーソライザ用Lambda関数  
const authorizerLambda = new NodejsFunction(this, 'AuthorizerFunction', {
  entry: path.resolve(__dirname, '../../lambda', 'authorizer.ts'),
  handler: 'handler',
  functionName: 'AuthorizerFunction',
});
  
// Lambdaオーソライザ
const authorizer = new RequestAuthorizer(this, 'RequestAuthorizer', {
   handler: authorizerLambda,
   // オーソライザで認証に使う値
   identitySources: [IdentitySource.header('Authorization')],
   authorizerName: 'RequestAuthorizer',
   // キャッシュは思わぬ副作用があるので注意(後述)
   resultsCacheTtl: cdk.Duration.seconds(0),
});
  
// API Gateway(restApi)やLambdaオーソライザ認可後に実施する
// Lambda(hogeLambda)の定義は省略
const res = restApi.root.addResource('hoge');
res.addMethod('GET', new LambdaIntegration(hogeLambda), {
  authorizer,
  // Lambdaオーソライザの場合、ここはCUSTOMを指定
  authorizationType: AuthorizationType.CUSTOM,
});

上記ソースの通り、

  1. オーソライザ用Lambda関数を作成する
  2. Lambdaオーソライザを定義し、そこで使用するオーソライザ用Lambda関数を指定する
  3. 最後にAPI Gatewayのメソッド定義で、オーソライザと認証タイプ(AuthorizationType.CUSTOM)を指定する

という感じで定義すればOKで、AWS CDKの定義自体は意外とシンプルです。

キャッシュを扱う場合の注意

Lambdaオーソライザではキャッシュを扱うことができます。
うまく使えば処理時間になりますが、ちょっと注意が必要なケースもあります。

前提として、キャッシュは下記の挙動をします。

  • キャッシュの結果は「認証ソース」の値ごとに保持される
    • AWS CDKにおける「identitySources」で設定した項目の値
  • キャッシュはTTLで指定した秒数の間保持される(デフォルトは5分)
    • AWS CDKの「resultsCacheTtl」の値
    • キャッシュを使用しない場合は0を指定する

そして「キャッシュがあると予期しない挙動をする」ケースとして、以下に一例を示します。

キャッシュが予期しない挙動をする具体例

仮に「Lambdaオーソライザの実装」で示したソースについて、間違えて`Authorizationヘッダが「allow」の時もDenyする処理を書いてしまったとします。

if (param === 'allow') {
  // Allowと間違えてDenyにしてしまってる
  return generateAuthorizerResult('Deny', event.methodArn);
} else if (param === 'deny') {
  return generateAuthorizerResult('Deny', event.methodArn);
} else {
  throw new Error('Unauthorized');
}

この時Authorizationヘッダに'allow'を指定しても、当然そのリクエストはDenyされます。

そしてその後ミスに気付いて、コードを修正後に再度Authorizationヘッダが'allow'のリクエストを送信すると、そのリクエストは当然Allowされる

...と思いきや、しばらくはDenyされ続けてしまいます。

これが「キャッシュによる予期しない挙動」で、「Authorizationヘッダ'allow'はDenyされた」という結果をTTLの時間が経過するまでキャッシュとして保持します。

その結果、「(Authorizationヘッダが'allow'の場合)本来Allowされるべきリクエストもキャッシュが有効な期間はDenyされる」という現象が発生してしまいます。

他にも、クラスメソッドさんの下記ブログで紹介されている「Authorizationヘッダ以外の値でAllowかDenyを判定している」ケースでもこの現象が発生しうるので、キャッシュを扱う際には考慮が必要です。

dev.classmethod.jp

もちろん、うまく使えば処理時間も短縮できて便利なので、そこは扱い方次第だと思います。

まとめ

以上、AWS CDK&Lambdaオーソライザを実装する方法でした。

AWSでのサーバーレスにおいて、API Gateway - Lambda というのは「黄金パターン」と言うほどの定番なので、そこに認可処理を挟めるLambdaオーソライザはなかなか便利な機能です。

これを用意しておくと、各Lambdaで個別に認可処理を記載する必要がなくなる(かも)ので、機会があれば一度導入を検討してみてもよいと思います。

それでは、今回はこの辺で。

AWS Community Builderになって半年経った感想

はじめに

この記事は AWS Community Builders Advent Calendar 2023 の9日目の記事です。

AWS Community Builder とは?

もう前の方がさんざん書いているので概要だけ。

  • AWSの知識・知見の共有(≒アウトプット)
  • AWS技術コミュニティへの活動・貢献

をした人に、AWSが技術リソース、教育、ネットワーキングの機会を提供してくれるというプログラムです。

aws.amazon.com

普段はどういう活動をしているの?

主にこんな活動をしています。

個人

  • ブログ・SNSでのAWSに関する情報の発信
  • 社内でのAWSに関する知識・知見の展開、社内勉強会での登壇
  • AWS関連のイベントへの参加、協力
    • AWS公式の各種イベント(AWS Summit, AWS Dev Dayなど)
    • 各社さんが開催するAWSイベント(知見共有などの勉強会など)
  • AWS以外のイベントで、AWSを絡めた技術知見での登壇

コミュニティ

  • JAWS-UG, Serverless Tokyo等, 各種コミュニティイベントへの参加
    • 出来る限りオフライン参加
  • 上記イベントでの登壇
  • コミュニティ運営への協力
    • JAWS Festa 2023での運営お手伝い
    • 最近は特にJAWS-UG CDK, およびJAWS-UG 名古屋 で活動しています

どんな申請をしたの?

申請時に「自分のAWSに関する活動実績、及びAWSに関する自分の思い」的なものを記載する必要があります。(ここに苦労する人もいるかもしれません)

ちなみに、私は下記のような事を記載しました。(うる覚えですが)

  • 仕事でAWSを用いたアプリケーションの設定、AWS CDKでのインフラ構築をしており、AWSに非常に関わりが深い
  • 仕事だけでなく個人的にもAWSが好きで、休日にもたまにAWSを触って知識の習得やスキルアップ活動を行っている
  • 個人的にもサーバーレスやAWS CDKが大好きで、今後もこのあたりの知識を中心にAWSを追求していきたい
  • その他、上記「個人」や「コミュニティ」に記載した活動を通してAWSの知識共有やコミュニティ活動に貢献しており、今後もそれを続けていくつもりである

その結果、今年の8月にサーバーレス分野でAWS Community Builderに選出していただきました。

Community Builder になってよかったこと

色々なモチベーションが上がる

いろんな分野でのCommunity Builderの方と交流ができますし、そういう活動や交流を通して得られる知見・知識はめちゃくちゃ大きいです。

先日の JAWS-UG横浜 #63 AWS re:Invent 2023 宇宙一早い Recap でkawanoさんが「イベント後の飲み会こそ、最大のインプットの場所である」ということを話していましたが、上のような交流を通して、本当にそれを実感しています。(もちろん参加する・しないは個人の自由です)

そういう機会を多く得られるというのは、エンジニアとして本当にモチベーションが上がるし、自分にとってもプラスになります。

最新情報をキャッチできる

Community Builderになる際、NDAを締結する必要があります。

その関係で、未公開の最新情報をいち早くキャッチしたり、未公開のサービスに触れる機会もあります。(もちろんフィードバックすることを前提で)

コミュニティ活動にさらに積極的になった

上記のようなメリットをコミュニティ活動を通した結果得られたので、その分さらにコミュニティ活動に貢献しよう!という気持ちがさらに高まりました。

もちろん、それがなくてもコミュニティ活動を通して色々な方と交流できたり、ものすごい知見を得られるというメリットがあるので「Community Builderの為だけに」コミュニティ活動を行っているわけでは全然ないのですが、Community Builderになってからはより一層下記の点で「自分ができることは何か」を考えるようになりました。

  • コミュニティ活動をさらに盛り上げるためにはどうすればよいか
  • 自分が得た知見を、他の人に共有するためにはどうすればよいか

そして、過去記事でkagaさんや丹後さんも述べていますが、そういうコミュニティ活動を通して「各種情報に関するアンテナの感度が高くなった」「アウトプット意識が向上した」などの副次的なメリットも享受できたのが大きなメリットだなと感じています

Community Builder になって戸惑ったこと

英語の壁

これは過去の記事でも書かれていますが、Community Builderのやりとりは、専用のSlackチャンネルで英語でやり取りされます。
なので、英語が苦手だとなかなかコミュニケーションが難しいと感じます。

というか、JSConf JPやre:Invent 2023でも感じましたが、英語ができないだけで機会損失してしまう場面が少なからずあるので、やっぱり英語は必須だなと感じました

会社での評価

Community Builderになったからといって、会社で何か評価が変わるとか、そういう事があるかは分かりません。(会社や上司次第)
ここら辺はいろいろ言われている「コミュニティ活動って本当に必要なの?」問題に通じるものがあるのかなと思います。

これに関しては地道に活動して理解してもらうか、あるいはそういう活動を理解してくれる環境に異動するとかしかないかなあと思います。

まとめ

会社での評価はともかく、個人的にはコミュニティビルダーになったことで非常に活動の幅が広がりましたし、モチベーションもかなり上がった一年でした。

また、今年は本格的にコミュニティイベントへの参加・登壇・協力などにコミット出来た一年だったと思いますし、そういう活動を通じて得られたものも大きかったです。

今後もそういった活動を継続し、来年もCommunity Builderに認定されるようにしたいと思います。

あと「来年こそは」と思っている方、どんな些細なことでもいいのでなにか実績があれば、ダメ元でもいいのでぜひ一度申請してみることをお勧めします。(自分もまさか認定されるとは思わなかった)

それでは、今回はこの辺で。