LoginSignup
0
0

EBSデフォルト暗号化をカスタムリソースで実施〜Node.js編〜

Posted at

EBSデフォルト暗号化をカスタムリソースで実施〜Node.js編〜

以下の業務要件があったため、EBSデフォルト暗号化をカスタムリソース(Node.js)で実装することになりました。

  • リソースの作成や設定値の変更は、コンソールやCLIからを行わずIaCで管理する
  • ランタイムには、Node.jsを使用する

Pythonを用いた実装は、以下の記事が参考になります。

早速、ソースコードの記載から始めていきます。
後半では、詰まったポイントも記載するので、興味がある方は一読してください。

ソースコード

EnableEBSDefaultEncryption.yaml
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を削除 -> スタックの削除
    • スタックの処理を待っている間に別のリソース名に書き換えて、別のスタックを作成
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0