EBSデフォルト暗号化をカスタムリソースで実施〜Node.js編〜
以下の業務要件があったため、EBSデフォルト暗号化をカスタムリソース(Node.js)で実装することになりました。
- リソースの作成や設定値の変更は、コンソールやCLIからを行わずIaCで管理する
- ランタイムには、Node.jsを使用する
Pythonを用いた実装は、以下の記事が参考になります。
早速、ソースコードの記載から始めていきます。
後半では、詰まったポイントも記載するので、興味がある方は一読してください。
ソースコード
AWSTemplateFormatVersion: "2010-09-09"
Description: Template to enable EBS default encryption across all regions.
Resources:
# Lambda-backed custom resources
EnableEBSDefaultEncryption:
Type: Custom::EnableEBSDefaultEncryptionFunction
Properties:
ServiceToken: !GetAtt EnableEBSDefaultEncryptionFunction.Arn
# Lambda 関数
EnableEBSDefaultEncryptionFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: enable-ebs-default-encryption-function
Description: Lambda for enable EBS default encryption
Role: !GetAtt EnableEBSDefaultEncryptionRole.Arn
Runtime: nodejs18.x
Handler: index.handler
Timeout: 900
Code:
ZipFile: |
const {
EC2Client,
EnableEbsEncryptionByDefaultCommand,
DescribeRegionsCommand
} = require("@aws-sdk/client-ec2");
const response = require("cfn-response");
exports.handler = async function(event, context) {
console.log('[START] enable_encryption_ebs');
const ec2 = new EC2Client({});
if (event.RequestType ==='Delete') {
await response.send(event, context, response.SUCCESS);
return
}
try {
const regionsResponse = await ec2.send(new DescribeRegionsCommand({}));
const regions = regionsResponse.Regions.map(region => region.RegionName);
await Promise.all(regions.map(async (region) => {
const regionalEC2 = new EC2Client({ region });
await regionalEC2.send(new EnableEbsEncryptionByDefaultCommand({}));
console.log(`${region}: EBS default encryption enabled`);
}))
await response.send(event, context, response.SUCCESS);
console.log(`[END] enable_encryption_ebs`);
} catch (error) {
console.error(error);
await response.send(event, context, response.FAILED);
}
};
Tags:
- Key: Name
Value: enable-ebs-default-encryption-function
# Lambda 関数用の IAM ロール
EnableEBSDefaultEncryptionRole:
Type: AWS::IAM::Role
Properties:
RoleName: enable-ebs-default-encryption-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: "lambda.amazonaws.com"
Action: "sts:AssumeRole"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: EbsEncryptionByDefaultPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "ec2:GetEbsEncryptionByDefault"
- "ec2:EnableEbsEncryptionByDefault"
- "ec2:DescribeRegions"
Resource: "*"
詰まったポイント
CommonJSとES Modulesの違い
そもそもNode.jsにはCommonJSとES Modulesといったものがあります(Node.js初心者の筆者は全く知りませんでした)。
※筆者と同様にCommonJSとES Modulesについてよくわかってない方は以下の記事がお勧めです!
LambdaでランタイムをNode.jsバージョン18以降を選択した場合、
デフォルトでindex.mjs(ES Modules)となります。
そのことを知らずにCommonJS仕様でコードを記載していると以下のエラーが発生
{
"errorType": "ReferenceError",
"errorMessage": "require is not defined in ES module scope, you can use import instead",
"trace": [
"ReferenceError: require is not defined in ES module scope, you can use import instead",
" at file:///var/task/index.mjs:1:22",
" at ModuleJob.run (node:internal/modules/esm/module_job:218:25)",
" at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)",
" at async _tryAwaitImport (file:///var/runtime/index.mjs:1008:16)",
" at async _tryRequire (file:///var/runtime/index.mjs:1057:86)",
" at async _loadUserApp (file:///var/runtime/index.mjs:1081:16)",
" at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)",
" at async start (file:///var/runtime/index.mjs:1282:23)",
" at async file:///var/runtime/index.mjs:1288:1"
]
}
要は、「CommonJSモジュールシステムの機能である"require"をESモジュール(ECMAScriptモジュール)で使用しているぞ!」って怒られているわけです。
そこで筆者は、ES Module仕様でコードを書き直しました。
その結果、Lambdaで問題なくコードが動き、喜びながらCloudFormationに乗せて実行すると何故かエラーに。。。
調べていくとCloudFormaitonで書いたコードはindex.js(CommonJS)として生成されるため、うまく動いてくれなかったみたいです(Node.js初心者には厳しすぎる仕様。。。)
さらに厄介だったのが、Lambdaのコードによるエラーが発生した場合、CloudFormationの処理が終わらない&スタックを削除できないといった事象が発生しました。
(この時、死ぬほど焦っていました)
カスタムリソースのエラー時はスタックを削除できるまで2時間かかる
スタック自体の書き方に問題がある場合は、すぐにロールバック→削除処理が実行されますが、カスタムリソース(Lambdaのコード)に問題がある場合、「CREATE_IN_PROGRES」から動きません。
これは、Lambdaからcfn-responseでカスタムリソースの応答オブジェクトを正常に返せていないことが原因です
CloudFormationのデフォルトタイムアウト時間である1時間、「CREATE_IN_PROGRES」の状態が続きました。
1時間経ち、ようやく削除されると思いきや
次は、「DELETE_IN_PROGRESS」の状態が1時間続きました。。。
(これも先ほどと同様の理由です)
以下に公式が対処法について記載していますので、同様のエラーにハマった方はご参照ください。
まとめ
- 実装工数はかかりますが、カスタムリソースとして実装するため、環境構築に安定感が生まれます
- Node.jsでカスタムリソースを実装する場合は、CommonJSかES Modulesについて意識する必要があります
- スタックの作成中にLambdaの中身でエラーが発生した場合は落ち着いて以下の対応を取ってください
- 2時間待ってLambdaを削除 -> スタックの削除
- スタックの処理を待っている間に別のリソース名に書き換えて、別のスタックを作成