echo("備忘録");

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

【AWS CDK】S3サーバーアクセスログを有効にすると、「The bucket does not allow ACLs」エラーが発生する

本日のお題

AWS CDK(以下「CDK」と記載)において、既存のS3バケットを別バケットの「サーバーアクセスログ」の送信先に設定すると、CloudFormation(以下「CFn」と記載)にデプロイした際にエラーになる

詳細

(Webホスティングを行っているS3バケットなど)Public Readに設定しているS3バケットにおいて、サーバーアクセスログを有効にしてアクセス監視・解析をするということは、運用上よくあると思います。(定義については「参考情報:AWS CDKでアクセスログを有効にする方法」を参照)

ところが、サーバーアクセスログ送信先に既存のバケットを指定した場合、これをデプロイすると、CloudFormationでなぜか以下のエラーが発生する場合があります。

Resource handler returned message: "The bucket does not allow ACLs (Service: S3, Status Code: 400, Request ID:..(中略)..., HandlerErrorCode: InvalidRequest)

本日はこの問題についてのお話です。

いきなり結論

先に結論から言ってしまうと、「サーバーアクセスログを有効にした場合、CDKが『ACLを有効にするCFnテンプレート』を出力する」ことがエラーの原因です。

サーバーアクセスログを有効にした場合、CDKは送信先バケットのプロパティとして、下記CFnテンプレートを出力します。

ただ、このテンプレートは、AccessControlObjectOwnership(=オブジェクト所有者) が ObjectWriter だったりすることからも分かる通り「ACLAccess Control List)が有効」の時のテンプレートになっています。

 {
   "AccessControl": "LogDeliveryWrite",
   "BucketName": "fortune-tmm-auth0-logo-bucket-dev-accesslog",
   "OwnershipControls": {
     "Rules": [
       {
         "ObjectOwnership": "ObjectWriter"
       }
     ]
   },
}

しかしS3のデフォルトは「ACL無効」なので*1、既存バケット(=ACLが無効)に対してこの設定を適用しようとすると、「ACL無効のバケットACL有効時のプロパティを設定しようとしている」となり、エラーになってしまいます。

なお、これはあくまで「既存バケット」の場合であり、新規作成するバケットならエラーは発生しないかもしれませんが、現在はS3バケットのアクセス制御はバケットポリシーを使用することが推奨されているため、「ACL有効」のプロパティを出力するのはあまりよろしくないです。

プロパティを(強引に)書き換える

上記の現象は「ACL有効の設定を強制的に書き換える」ことで対応できます。

具体的には下記の対応をします。

  • AccessControl を削除する
  • ObjectOwnershipBucketOwnerEnforced (=バケット所有者の強制)にする、または OwnershipControls を削除する*2

ただしL2 Constructでは上記を実行できないので、node.defaultChild を使用してL1 Constructに変換した後で実行します。

具体的には、先述のソースの末尾に下記ソースを追加します。

const cfnLogBucket = logBucket.node.defaultChild as s3.CfnBucket;
  
// ObjectOwnershipの上書き。
// もちろんaddPropertyDeletionOverride('OwnershipControls') でもOK  
cfnLogBucket.addPropertyOverride('OwnershipControls.Rules.0.ObjectOwnership', 'BucketOwnerEnforced');
  
// AccessControlの削除
cfnLogBucket.addPropertyDeletionOverride('AccessControl');

なお、addProperty系メソッドでプロパティに配列のキーを指定する方法は、以下のCDK公式ドキュメントを参照してください。
addOverride(path, value)

参考情報:AWS CDKでアクセスログを有効にする方法

AWS CDKでサーバーアクセスログを有効にする場合、下記コードを記載します。

import * as cdk from 'aws-cdk-lib';
import { aws_iam as iam, aws_s3 as s3 } from 'aws-cdk-lib';  
  
// アクセスログを有効にするバケットのバケット名
const departureBucketName = `departure`;
  
// アクセスログの送信先Bucket
const logBucket = new s3.Bucket(this, `LogBucket`, {
  bucketName: 'destinationLogBucket' ,
  removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
});
  
// アクセスログの送信先Bucketのバケットポリシー
logBucket.addToResourcePolicy(
  new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    principals: [new iam.ServicePrincipal('logging.s3.amazonaws.com')],
    actions: ['s3:PutObject'],
    resources: [`arn:aws:s3:::destinationLogBucket/*`],
    conditions: {
      ArnLike: {
        'aws:SourceArn': `arn:aws:s3:::${departureBucketName}`,
      },
      StringEquals: {
         'aws:SourceAccount': <アカウント番号>,
      },
    },
  }),
);
  
// アクセスログを有効にするBucket
const departureBucket = new s3.Bucket(this, `DepartureBucket`, {
  // blockPublicAccessは無くてもいいかも
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS,
  bucketName: departureBucketName,
  publicReadAccess: true,
  removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
  serverAccessLogsBucket: logBucket,
  serverAccessLogsPrefix: 'logs/',
});
   
// アクセスログを有効にするBucketのバケットポリシー
// iam.StarPrincipal()は「Principal: '*'」という定義を作成するメソッド
departureBucket .addToResourcePolicy(
  new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    principals: [new iam.StarPrincipal()],
    actions: ['s3:GetObject'],
    resources: [`arn:aws:s3:::${departureBucketName}/*`],
  }),
);

なお、サーバーアクセスログ送信先バケットに必要なバケットポリシーについては、下記のAWS公式ドキュメントを参考にしてください。
docs.aws.amazon.com

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

*1:2023年4月より、ACLはデフォルトで「無効」となります

*2:ObjectOwnershipのデフォルト値がBucketOwnerEnforcedなので、OwnershipControlsを削除することでもObjectOwnershipをBucketOwnerEnforcedに出来ます

【Node.js】Node.js 22の新機能を確認する

※このブログは、4/30(火) 19:00~開催の「JAWS-UG札幌 オンラインもくもく会 #95」にて記載した内容になっております。

jawsug-sapporo.connpass.com

はじめに

4/24(水)に、Node.jsの最新版であるNode.js 22がリリースされました。

そこで今回は、さっそくNode.js 22の新機能についてチェックしようと思います。(ちなみに、Node.js 22はおそらく10月ごろからLTSになるものと思われます)

参考サイト

新機能一覧

新機能の概要はこちら。

概要 説明 experimental 備考
V8 Update to 12.4 V8エンジンが12.4にアップデートし、それに伴いWASMのガベージコレクションや各種メソッドが追加された × 各種メソッドについては「Pick Up」で説明
Maglev Maglevコンパイラがデフォルトで有効になった ×
Support require()ing synchronous ESM graphs CommonJSの「require」で、ES Moduleのソースを読み込めるようになった ただし条件あり。詳しくは「Pick Up」で説明
Running package.json scripts node.jsから直接package.jsonのscriptを実行できるようになった 詳しくは「Pick Up」で説明
Stream default High Water Mark High Water Mark(stream処理における内部バッファの閾値)の初期値が16KiBから64KiBに増加し、パフォーマンスが向上 ×
Watch Mode Watchモード(node --watch)がexperimentalからstableに ×
WebSocket ブラウザ互換のWebSocketがexperimentalからstableになり、デフォルトで有効に ×
glob and globSync ファイルの検索時に使うglobパターン指定用関数が追加 詳しくは「Pick Up」で説明
Improve performance of AbortSignal creation AbortSignalインスタンス生成時のパフォーマンスが向上 ×

Pick Up

ここからは、一部新機能について紹介します。

V8 Update to 12.4

ブラウザではサポートされていた下記の機能が使用可能になりました。

機能 説明 備考
Array.fromAsync 反復可能(=itelable)オブジェクトから値のみの(=シャローコピーされた)配列を返します
Setオブジェクト 型やプリミティブ/オブジェクトを問わず、いろいろな一意の値を格納できる 「一意の値」なので、重複した値は格納できない
Iterator helpers 反復可能オブジェクト(=Iterator)の各種インスタンスメソッドを使用可能に filter, find, mapなど、Array.prototypeで使用可能なメソッドと同じものが多い

なおこれらについて、ソースコードや説明に関してはMDN Web Docsにかなり詳しく書いてあるので(特にSetクラス)、そちらを参照して下さい。(上表の「機能」にリンクを貼っておきます)

Setオブジェクトのメソッド紹介
  • いずれのメソッドも、戻り値としてSetオブジェクトを返します
  • A, BはどちらもSetオブジェクト
メソッド 戻り値のSetオブジェクトに含まれる値 備考
A.intersection(B) A, B両方に含まれている値 積集合
A.union(B) A, Bいずれかに含まれている値 和集合。なおA. B両方に含まれる値は1つにまとめられます
A.difference(B) AにあってBにない値 差集合
A.symmetricDifference(B) AかBのいずれか一方にしかない値 対象差集合。AとB両方が持つ値は除去されます
Iterator helpersの一部メソッドの紹介

Iterator helpersのメソッドのうち、Array.prototypeが持っていないものを紹介します。

メソッド 説明 備考
drop(limit) 元のIteratorから、先頭limit個分の要素を除去したIterator helperを返却します Arrayで言うArray.slice(limit)
take(limit) 元のIteratorから、先頭limit個分の要素のみを持つIterator helperを返却します Arrayで言うArray.slice(0, limit)
toArray() Iteratorから通常の配列(=Arrayクラスの配列)を返します Array.from(iterator)と同じ
小ネタ:Array.fromAsyncでファイルを読み込む

MDN Web Docsにも記載がある通り、ReadableStreamは非同期反復可能オブジェクトです。
つまり「fs.createReadStream*1で読み込んだストリーム(≒ファイルの内容)をArray.fromAsyncで取得する」という事が可能です。(実用的かどうかは別として)

以下がサンプルソースになります。

const fs = require('fs');  
  
// sample.txtには「I am Node.js 22.」というテキストが書いてある
async function getAsyncIterable() {
  const fileName = 'sample.txt';
  const stream = fs.createReadStream(fileName);
  stream.setEncoding('utf8');
  
  const result = await Array.fromAsync(stream);
  
  // 戻り値は配列なので、配列の要素を取得する
  return result[0];
}
  
(async () => {
  const result = await getAsyncIterable();
  console.log(result);
})();

上記ソースを実行すると、以下の結果が返ってきます。

$ node index.js
I am Node.js 22.

Support require()ing synchronous ESM graphs(Experimental)

CommonJSの「require」にて、ES Module形式のモジュール(関数なりクラスなり)を読み込むことが可能になりました。

ただし読み込み元のES Module形式のファイルは、以下を両方満たしている必要があります。

  • 対象のpackage.jsonに「"type": "module"」の定義がある、または拡張子が「*.mjs」である。(=ES Module形式で書く際のルール)
  • すべての処理が同期処理である(=awaitなど、非同期処理を含んでいない)

また現段階ではこの機能は「Experimental(=実験的)」であり、実行時に --experimental-require-module オプションをつける必要があります。

サンプルソースは以下になります。

// point.mjs
export function myPow(a, b) { return Math.pow(a, b); }
function mySqrt(a) { return Math.floor(Math.sqrt(a)) };
  
export default mySqrt;
// index.js
const required = require('./index.mjs');
  
async function requiredSample() {
  console.log(`mySqrt is ${required.default(9)}`);
  console.log(`myPow is ${required.myPow(2, 3)}`);
  
  const imported = await import('./index.mjs');
  console.log(imported === required);
  console.log(`mySqrt is ${imported.default(25)}`);
  console.log(`myPow is ${imported.myPow(3, 4)}`);
}

(async () => {
  await requiredSample();
})();

上記index.jsは、下記コマンドで実行し、その結果は以下の通りです。

$ node --experimental-require-module index.js
mySqrt is 3
myPow is 8
true
mySqrt is 3
myPow is 8

importedとの比較結果がtrueで、各関数の実行結果も同じなので、requireとimportで全く同じである事が分かると思います。

Running package.json scripts(Experimental)

package.jsonの「scripts」について、いままではnpm/yarnなどパッケージマネージャー経由で実施していましたが、nodeから直接実行できるようになりました。

なお、こちらも現段階ではExperimentalとなっています。

具体的なソース&実行例は以下の通りです。

// package.json
{
  "scripts": {
    "hoge": "echo hogehoge"
  }
}
$ node --run hoge
hogehoge

glob and globSync(Experimental)

何かの処理対象ファイルを指定する際などに使用可能なglobパターンについて、下記関数が追加され、Node.jsだけで完結できるようになりました。

  • fs.glob(pattern[, options], callback)
  • fs.globSync(pattern[, options])
  • fsPromises.glob(pattern[, options])

サンプルソースを下記に示します。(なお、カレントフォルダには以下ファイル&フォルダが存在するものとします)

  • node_modules
  • .gitignore
  • index.js
  • index.mjs
  • package-lock.json
  • package.json
  • sample.txt
// index,js
const fs = require('fs');
const fsp = require('node:fs/promises');
  
// いずれの関数も「package.json」と「package-lock.json」が
// ヒットすることを確認しています。
  
// fs.globとfs.globSyncの戻り値はファイル名の配列。  
// 違いは同期関数かどうかだけです
function getAsyncGlobPathFs() {
  fs.glob('./*kag*.js*', (err, matched) => {
    if (err) throw err;
    console.log(matched);
  });
}
  
function getSyncGlobPath() {
  const matched = fs.globSync('./*kag*.js*')
  console.log(matched);
}
  
// fsPromises.glob戻り値は非同期反復可能オブジェクト(AsyncIterator)なので、  
// ファイル名はそこからさらにArray.fromAsyncなどで取得する必要があります。
async function getAsyncGlobPathFsPromise() {
  const matched  = await Array.fromAsync(fsp.glob('./*kag*.js*'));
  console.log(matched);
}
  
(async () => {
  getAsyncGlobPathFs();
  getSyncGlobPath();
  await getAsyncGlobPathFsPromise();
})();
$ node index.js
[ 'package-lock.json', 'package.json' ]
[ 'package-lock.json', 'package.json' ]
[ 'package-lock.json', 'package.json' ]

個人的には、これが一番実用的かなと思いました。

まとめ

以上、Node.js 22の新機能でした。

今回も機能面・性能面で色々追加されており、Node.jsでの開発がもっと楽になるといいですね。

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

*1:fs.createReadStreamの戻り値fs.ReadStreamは、stream.Readableを継承しています。

【退職エントリー】株式会社DeNAを退職しました

※2024/04/28:諸事情により、一部内容を修正しました。

はじめに

2022年9月から約1年半勤めた株式会社DeNA(以下DeNA)を4/26(金)で退職しましたので、その退職エントリーを書こうと思います。

※正式には5/31(金)退職で、4/26(金)最終出社となりました。(ちなみに私のゴールデンウィークは37連休です)

DeNAで何をやってたの?

肩書き上は「クラウドアーキテクト」として、

  • モバゲーのアプリ開発部署にてAWSの管理や関連サービスのアーキテクチャ設計、
  • IaC(Infrastructure as Code, 業務ではServerless FrameworkやAWS CDK)を用いたインフラ構築、運用管理、
  • 一部開発・テスト(TypeScript/Jest/Node.js)

なんかを担当していました。

その他、DeNAがプラチナスポンサーをつとめるYAPC:2023, YAPC:2024での支援、及び各種イベントでの登壇なんかをやりました

自分はDeNAに就職する前は6年間フリーランスだったのですが、久しぶりに正社員に戻って、色々経験することができました。

特にモバゲーという大規模サービスの運用に携わることができたこと、そしてYAPC:2023, YAPC:2024といった大きな技術カンファレンスにスポンサーとしてブース出展などに携わることができたのは、とても良い経験になりました。

何で辞めようと思ったの?

私がJAWS-UG CDK支部(と名古屋支部)に深く携わっていることもあり、IaC(特にAWS CDK)を用いたインフラ構築・組織でのAWS運用やオブザーバビリティ・SREに非常に興味を持ち、それを深く追求していきたいと考えるようになったからです。

もちろんDeNAでもそれは考えており、昨年の秋くらいから色々それができるよう、自分なりに模索したんですが、色々なタイミングなどがうまくかみ合わず、残念ながら思うような結果が出ませんでした。(そりゃアプリ「開発」部署ですからね...)

とはいえ、同じグループメンバーとの仲は良好でしたし、そこまで「今すぐ辞めてやる」というよりは「どうするか葛藤している」という感じだったので、かなり悩みました。) *1

ただそれでも、今になって振り返ると「ああいうアクションを取ればよかったんじゃないか」「こういう視点が足りなかったんじゃないか」と自分に足りなかった点があると感じる部分は多々あります。(自分なりに結論が出て、いろいろ落ち着いた状態になったから色々アイデアが出てくるのかもしれません)

次は何するの?

(4月~5月は有休消化で)6月からは某アプリにおいて、AWS環境のIaC(AWS CDKやTerraform)を用いたインフラ構築・組織でのAWS運用・オブザーバビリティ・SREといった業務にガッツリ携わります。

業務でソースコード(TypeScriptなど)を書く機会は減ると思いますが、全く書かないことはないでしょうし、個人では引き続きバックエンドアプリエンジニアとして、主にLambdaのソースコードを書くことと思います。

Special Thanks!

株式会社DeNAの関係者の皆さん、1年半お世話になりました。 6年間のフリーランス生活から会社員に戻って、色々新しい経験することができました。ありがとうございました!

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

*1:実際、今でも自分の選択肢が正しかったのか、と色々考えますからね...

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

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

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