日次でセキュリティグループの設定を更新するLambdaを作ってみた

「日次でとあるドメインからIPアドレスを取得し、セキュリティグループのアウトバウンドルールに登録する」という機能をLambdaで実装しました。 意外とネットにサンプルがなく、色々勉強になったのでブログ化します。
2023.10.24

前提

  • 「日次でとあるドメインからIPアドレスを取得し、セキュリティグループのアウトバウンドルールに登録する」機能をLambdaで実装すること。
  • 対象となるドメインのIPアドレスは、不定期に更新される。
  • 対象となるセキュリティグループには、本Lambdaから登録したもの以外のアウトバウンドルールが存在する。(※その他のルールに影響があってはならない)
  • Lambdaのランタイムは「Node.js 18.x」とする。

構築

① Lambdaの構築

処理フロー概要

ソース

import { EC2Client, AuthorizeSecurityGroupEgressCommand, DescribeSecurityGroupRulesCommand, RevokeSecurityGroupEgressCommand } from "@aws-sdk/client-ec2";
import * as dns from "dns";

const CLIENT = new EC2Client({ region: "ap-northeast-1" });

export const handler = async () => {
  let domainName = "対象のドメイン名";
  let securityGgroupId = "対象のセキュリティグループID";
  let insertIpPermissions = [];
  let deleteIpPermissions = [];
  let ip = [];

  // 1. ドメイン名からIPアドレスを取得
  await dns.promises.lookup(domainName, { all: true }).then((result) => {
    Object.keys(result).forEach((key) => {
      insertIpPermissions.push(
        {
          "FromPort": 443,
          "IpProtocol": "tcp",
          "IpRanges": [
            {
              "CidrIp": result[key].address + "/32",
              "Description": "XXXXXXXXXX",
            }
          ],
        "ToPort": 443
        }
      );
      ip.push(result[key].address + "/32");
    });
  });

  // 2. セキュリティグループからアウトバウンドルールを取得
  let rules;
  try {
    const DESCRIBE_COMMAND_RESPONSE = await CLIENT.send(
      new DescribeSecurityGroupRulesCommand({
        Filters: [
          {
            Name: "group-id",
            Values: [
              securityGgroupId,
            ],
          },
        ],
      }),
    );
    rules = DESCRIBE_COMMAND_RESPONSE.SecurityGroupRules;
  } catch (err) {
    console.error(err);
    return {
      statusCode: err.$metadata.httpStatusCode,
      body: JSON.stringify("セキュリティグループ情報取得処理でエラーが発生しました。"),
    };
  }

  // 3. 1と2の取得結果を比較し、1で取得したIPアドレスがアウトバウンドルールに登録済かを判定
  let insertFlg = false; 
  let rulesCount = 0;
  Object.keys(rules).forEach((key) => {
    if(rules[key].IsEgress) {
      if(!ip.includes(rules[key].CidrIpv4) && rules[key].Description === "XXXXXXXXXX") {
        insertFlg = true;
      }
      deleteIpPermissions.push(
        {
          "FromPort": 443,
          "IpProtocol": "tcp",
          "IpRanges": [
            {
              "CidrIp": rules[key].CidrIpv4,
              "Description": "XXXXXXXXXX",
            }
          ],
        "ToPort": 443
        }
      );
      rulesCount++;
    } else if (rulesCount === 0) {
      insertFlg = true; 
    }
  });

  // 4. 3の結果未登録であった場合、既存のアウトバウンドルールを削除し、1で取得したIPアドレスをアウトバウンドルールに登録する
  if(insertFlg) {
    if(deleteIpPermissions.length !== 0) {
      try {
        await CLIENT.send(
          new RevokeSecurityGroupEgressCommand({
            "GroupId": securityGgroupId,
            "IpPermissions": deleteIpPermissions
          }),
        );
      } catch (err) {
        console.error(err);
        return {
          statusCode: err.$metadata.httpStatusCode,
          body: JSON.stringify("アウトバウンドルール削除処理でエラーが発生しました。"),
        };
      }
    }

    try {
      await CLIENT.send(
        new AuthorizeSecurityGroupEgressCommand({
          "GroupId": securityGgroupId,
          "IpPermissions": insertIpPermissions
        }),
      );
    } catch (err) {
      console.error(err);
      return {
        statusCode: err.$metadata.httpStatusCode,
        body: JSON.stringify("セキュリティグループ登録処理でエラーが発生しました。"),
      };
    }

    return {
      statusCode: 200,
      body: JSON.stringify("アウトバウンドルールを更新しました。"),
    };
  } else {
    return {
      statusCode: 200,
      body: JSON.stringify("IPアドレスは登録済です。"),
    };
  }
};

② トリガーの設定

LambdaのトリガーにEventBridgeを設定し、日次でLambdaを発火させるスケジュールを定義

ポイント

  • ドメインからIPアドレスを取得する方法としてnslookupコマンドの使用を検討していたのですが、そもそもLambdaの実行環境にnslookupコマンドがなく、おとなしくライブラリを使用するようにしました。詳細な経緯については別の記事にまとめてあります。
    Lambdaの実行環境で使用できるシェルコマンドについて調査しました
  • セキュリティグループから取得・登録するIPはCIDR形式でないと怒られるので、ドメインから取得したIPにコードの中で「/32」をつけています。
  • 本Lambdaによって登録されたルールとそれ以外を区別する為に、登録時にDescriptionを設定するようにしました。
    もうちょっとスマートな方法がないかSDKの仕様書を眺めていたのですが、それ以外の方法がなさそうでした、、、。

最後に

今回はネット上にサンプルが少なく、基本仕様書を見て実装したので思いのほか歯ごたえがありました。
AWS SDK for JavaScript v3の仕様書
また、Node.js18系は「AWS SDK for JavaScript v3」を使用する必要があり、v2 とは書き方が違う部分もあるので、以前のバージョンから移行する際には注意が必要だと思いました。