TOP>コラム一覧>CDK で Custom resources を作成する

CDK で Custom resources を作成する

はじめに

こんにちは、CTCの山本です。
本記事では、Custom Resource Provider Frameworkを用いたCDKでカスタムリソース実装方法の解説していきたいと思います。

具体的な内容は以下になります。

Custom Resources について

Custom Resources とは

Custom Resources (カスタムリソース) とは、CloudFormationからの「Create/Update/Delete」イベントをLambda や SNS に関連付けて処理する仕組みを実装することでCloudFormationを拡張する仕組みとなります。
CloudFormation のリソースタイプに存在しなかったり、独自の処理でプロビジョニングを行いたい場合は、カスタムリソースの仕組みを用いてリソースの構成管理ができます。

参考: AWS/Documentation/AWS CloudFormation/User Guide/Custom resources

【参考】 CloudFormation レジストリ

また、カスタムリソースとは別に、CloudFormation レジストリという仕組みもあります。
こちらはLambdaやSNSの関連付けが不要、ドリフト検出をサポートする等、AWS が提供するリソースタイプと同じような使用かできるといった利点があるため、しっかりと構築する場合はこちらが候補になるでしょう。

CloudFormationの拡張方法

CloudFormation拡張方法 説明
Custom Resources(カスタムリソース) CloudFormationからの「Create/Update/Delete」イベントをLambda や SNS に関連付けて処理する仕組みを実装することで拡張する仕組み。
CloudFormation レジストリ AWS アカウント で使用できるリソース、モジュール、フックの拡張を管理できる仕組み。AWS が提供するリソースタイプと同じように使用できるようになる。

CDK における Custom Resources

CDK では、以下のCustom Resourcesの実装手段があります。

CDK における Custom Resourcesの実装方法

カスタムリソースプロバイダーの実装方法 特徴
SNS Topic
  • SNSを使用してカスタムリソースプロバイダーを一から実装する方法
  • カスタムリソースの処理が AWS Lambda で実行できない事情がある時のみ使用を検討することを推奨
AWS Lambda Function
  • Lambdaを使用してカスタムリソースプロバイダーを一から実装する方法
  • CDK では、基本的に後述するCustom Resource Provider Frameworkを使用することを推奨している
core.CustomResourceProvider class
  • カスタムリソースプロバイダーを実装するための低レベルなAPIを提供するフレームワーク
  • カスタムリソースのConstruct Library を構成するために存在しており、通常のユースケースでの使用は非推奨。
Custom Resource Provider Framework (推奨)
  • カスタムリソースプロバイダーを実装するための高レベルなAPIを提供するフレームワーク
  • タイムアウト処理やCloudFormationへの応答処理等をフレームワーク側が管理
  • CDKでは、カスタムリソースを実装する場合、こちらの利用を推奨している。

CDKでは、カスタムリソースプロバイダーに必要な処理の実装支援を提供するCustom Resource Provider Frameworkの使用が推奨されています。

参考: AWS CDK Reference Documentation / Custom Resources

Provider Framework を用いたCDKの実装方法

Custom Resource Provider Framework を用いたカスタムリソースの実装方法について基本的な内容を説明します。

本記事では、以下のProvider Frameworkの公式ドキュメントを参考に要点を嚙み砕いて説明するとともに、正確であることを心掛けてはおりますが、 より詳細かつ正確で最新な情報を求める方は直接公式ドキュメントを参照して頂けますと幸いです。

参考: AWS CDK Reference Documentation / Provider Framework

Provider Framework を用いたCDKコードの構成

Provider Framework を用いたCDKの実装イメージは以下になります。

Provider Frameworkを利用する際、CDKでは以下の要素を実装する必要があります。

  • Lambda (onEvent handler)
  • Provider Construct
  • CustomResource

以下、各実装要素に絞って説明していきます。
記事後半にてサンプルコードがありますので、合わせてご覧ください。

Lambda (onEvent handler)

リソースのライフサイクルイベント(Create / Update/ Delete)に対応する処理をLambdaで実装します。

実装に当たって、最低限、抑えて置く必要がある要素に絞って説明します。
(※実装する処理の内容によっては以下の限りではありませんが、一般的な使用方法の観点で説明します。)

onEvent 処理の実装のポイント

  • 「Create / Update/ Delete」の情報は Lambdaの入力イベントのオブジェクトに含まれるRequestTypeから取得できるため、このフラグに応じて、イベントを判別します。
  • 要求されるリソースの状態(CustomResourceで定義したproperties)は入力イベントのオブジェクトに含まれるResourcePropertiesから取得できるため、この内容に合わせて、管理するリソースの状態を変更する処理を実装していきます。
  • 戻り値のオブジェクトのPhysicalResourceIdパラメータには、生成したリソースの物理IDを返すようにします。※物理リソースIDについては後述で説明
  • 「Update/ Delete」イベントでは、入力イベントのオブジェクトに含まれるPhysicalResourceIdを取得して、このリソースに対して更新・削除するように処理を実装します。

※本記事における該当箇所のコード例はこちら

Provider Construct

Provider Frameworkを使用して、カスタムリソースプロバイダーを作成します。
Provider Frameworkにより、CloudFormationとのやり取り等の大部分の処理が管理されるため、実装を大幅に簡略化できます。

Provider Frameworkにより管理される処理

  • AWS CloudFormation への応答処理
  • ハンドラーの戻り値の検証と実装支援
  • タイムアウトの管理
  • 物理リソース ID の管理

最低限、onEvent handler Lambdaを関連付けるだけで、カスタムリソースプロバイダーが作成できます。

※本記事における該当箇所のコード例はこちら

CustomResource

カスタムリソースのコンストラクトを作成します。
最低限、以下の処理を実装します。

実装のポイント

  • カスタムリソースプロバイダーをserviceTokenに指定する
  • カスタムリソースプロバイダーに渡すプロパティをpropertiesに指定する。

※本記事における該当箇所のコード例はこちら

物理リソースIDについての補足説明

Physical Resource IDs(物理リソースID)とは

物理リソースIDはCloudFormationで管理されるリソースのIDです。
公式ドキュメントを要約すると、物理リソースIDは以下のような説明がされています。

  • カスタムリソースプロバイダーをserviceTokenに指定する
  • Createオペレーションで、明示的に物理リソースIDを返さない場合は、フレームワークにより、リクエストIDが設定される
  • Updateオペレーションで、デフォルトの処理としては、現行の物理リソースIDを返す。
  • Updateオペレーションで、 onEventが現行の物理リソースIDと異なる物理リソースIDを返した場合、CloudFormation上、置換処理として扱う。

すなわち、物理リソースIDはUpdate、Delete イベントの処理の中で、更新・削除対象のリソースのキー情報としての役割で使用します。
そのため、実装する処理にもよりますが、管理対象リソースに対応するSDKのAPIのキーとなる情報を設定するのがよいでしょう。
大抵は管理対象のリソースのARNやリソースID等を物理リソースIDとして設定する形になるかと思います。

なお、論理IDと物理リソースIDはCloudFormationのマネージドコンソール上から確認できます。

物理リソースIDと更新処理

前述でも触れましたが、Update処理で返却する物理リソースIDによって、CloudFormationが発行するオペレーション処理が変わりますので、実装時にどちらの処理が適切か留意して実装する必要があります。

  • Updateイベントの中で、プロバイダーが既存と同じ物理リソースIDを返した場合は、通常の更新処理となる。
  • Updateイベントの中で、プロバイダーが既存と異なる物理リソースIDを返した場合、更新が置換処理の扱いとなり、CloudFormationは続けて既存リソースに対するDeleteオペレーションを発行する挙動となる。

図にすると以下のようなイメージです。

エラー時の処理の考慮

CloudFormationの処理の中でエラーが発生すると、ロールバック処理が実行されますので、ロールバック動作にも留意して実装する必要があります。
Provider Framework のドキュメントを読み解くと、Provider Frameworkでは、エラー時の動作は以下のようになると説明されています。

Create イベントで失敗した場合

  • リソースの生成に失敗した場合、プロバイダーフレームワークはDeleteオペレーションを無視する。
  • 必要に応じて、エラーハンドリングとして、クリーンアップ処理を実装する必要がある。

Update イベントで失敗した場合

  • リソースの更新に失敗した場合、CloudFormationからResourcePropertiesOldResourcePropertiesの内容が入れ替えられたUpdate オペレーションが発行される。
  • 異なる物理リソースIDが返した後(置換アップデートの後)、更新に失敗した場合は、クリーンアップの為、新しく作成したリソースに対する Delete オペレーションが発行される。

Delete イベントで失敗した場合

  • リソースの削除に失敗した場合、CloudFormationはリソースを放棄する。

カスタムリソースの処理として、他のコンストラクトで管理するリソースとの複数の関連や、複数のAPIを実行するような処理を検討する場合は、多くのユースケースに対応する必要性が発生してしまいます。
個人的な所感ですが、カスタムコンストラクトの処理の中で多くのことを行わず、可能な限りシンプルに設計するのが良いかと思います。

AwsCustomResource コンストラクト

AWS API を1つだけ呼び出すような処理のカスタムリソースを作成したい場合、Custom Resource Provider Frameworkでは、AwsCustomResource コンストラクトが用意されています。
AwsCustomResource コンストラクトを使用する場合、イベント処理の実装をフレームワーク側が支援してくれるため、Lambdaのハンドラ処理のコードの実装が不要になります。

※本記事における該当箇所のコード例はこちら

Provider Framework を使用したCDKのサンプル実装

本記事では、CDK (Typescript)で Provider Framework を用いた簡単な実装(ECR プライベートレジストリのスキャン設定のプロビジョニング)を実演したいと思います。
ECR プライベートレジストリのスキャン設定の変更は公式のCloudFormation リソースタイプでサポートされていないため、こちらを題材にしました。

注意!
本記事のコードを実行すると、ECR プライベートレジストリのスキャン設定が変更されるとともに、Inspector がActive になり、利用料金が発生する可能性があります。
本記事のコードを実行する場合は、内容を理解した上で自己責任で実施をお願いします。

実装対象の説明

ECR プライベートレジストリのスキャン設定は以下の2つの要素で構成されます。

  • スキャンタイプ
  • リポジトリフィルタ

仕様の検討

対応するSDKの調査

プロビジョニング処理はLambda(Node.js)で処理するものとして、ECR プライベートレジストリのスキャン設定に対応するSDKのライブラリを調べます。

SDKのリファレンスによるとPutRegistryScanningConfigurationCommandで処理できそうです。

AWS SDK for JavaScript v3 / PutRegistryScanningConfigurationCommand

各ライフサイクルイベントの処理を考える

次に、「Create/Update/Delete」イベントに対してどのような処理を実装するか検討します。
本記事では、次のように実装するものとします。

ライフサイクルイベント 処理
Create 「指定されたスキャンタイプ」、「リポジトリフィルタ」の状態にECR プライベートレジストリのスキャン設定を変更する。
Update 「指定されたスキャンタイプ」、「リポジトリフィルタ」の状態にECR プライベートレジストリのスキャン設定を変更する。
Delete 「スキャンタイプ = "Basic"」、「リポジトリフィルタ = 未設定」の状態(出荷時の状態)に変更する。

CDK の実装

CDKプロジェクトの作成

以下を実行して、CDKプロジェクトを作成します。


  mkdir cdk-cr-ecr-registry
  cd cdk-cr-ecr-registry
  cdk init app --language typescript

また、以下も必要となるため、追加でインストールします。


  npm install -D @types/aws-lambda # lambdaの型定義
  npm i -S @aws-sdk/client-ecr # AWS SDKのclient-ecr モジュール

ディレクトリ構成

コードのディレクトリ構成とファイル名は以下のようにします。


  .
  ├── bin
  │   └── cdk-cr-ecr-registry.ts // プログラムのエントリーポイント
  └── lib
      ├── cdk-cr-ecr-registry-stack.ts // スタックを定義
      └── construct
          ├── ecr-registry-scanning-configuration-construct.ts // ECR プライベートレジストリのスキャン設定を扱うカスタムコンストラクトを定義
          └── src
              └── index.ts // カスタムリソースのイベントハンドラ (lambda) を実装 

エントリーポイント (bin/cdk-cr-ecr-registry.ts)

  • CDKコードのエントリーポイントとなります。
  • 後述するスタックCdkCrEcrRegistryStackを呼び出しています。

  #!/usr/bin/env node
  import "source-map-support/register";
  import * as cdk from "aws-cdk-lib";
  import { CdkCrEcrRegistryStack } from "../lib/cdk-cr-ecr-registry-stack";
  
  const app = new cdk.App();
  new CdkCrEcrRegistryStack(app, "CdkCrEcrRegistryStack", {});

スタック (lib/cdk-cr-ecr-registry-stack.ts)

  • スタックCdkCrEcrRegistryStackの定義となります。
  • カスタムコンストラクトEcrRegistryScanningConfigurationConstructにECR プライベートレジストリのスキャン設定を渡してリソースを変更します。

  import * as cdk from "aws-cdk-lib";
  import { Construct } from "constructs";
  import { EcrRegistryScanningConfigurationConstruct } from "./construct/ecr-registry-scanning-configuration-construct2";
  
  export class CdkCrEcrRegistryStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
      super(scope, id, props);
  
      new EcrRegistryScanningConfigurationConstruct(
        this,
        "EcrRegistryScanningConfiguration",
        {
          scanType: "ENHANCED",
          rules: [
            {
              scanFrequency: "CONTINUOUS_SCAN",
              repositoryFilters: [{ filter: "prd-*", filterType: "WILDCARD" }],
            },
            {
              scanFrequency: "SCAN_ON_PUSH",
              repositoryFilters: [{ filter: "dev-*", filterType: "WILDCARD" }],
            },
          ],
        }
      );
    }
  }

カスタムコンストラクト ( lib\construct\ecr-registry-scanning-configuration-construct.ts)

  • ECR プライベートレジストリのスキャン設定を扱うカスタムコンストラクトの定義となります。
  • 内部でProvider ConstructCustomResourceLambda (onEvent handler)を作成し、カスタムリソースを実装します。

  import { Construct } from "constructs";
  import * as cdk from "aws-cdk-lib";
  import * as cr from "aws-cdk-lib/custom-resources";
  import * as iam from "aws-cdk-lib/aws-iam";
  import * as nodejsLambda from "aws-cdk-lib/aws-lambda-nodejs";
  import * as lambda from "aws-cdk-lib/aws-lambda";
  import * as path from "path";
  import { ResourceProperties } from "./src/index";
  
  export class EcrRegistryScanningConfigurationConstruct extends Construct {
    constructor(scope: Construct, id: string, props: ResourceProperties) {
      super(scope, id);
  
      // IAM Policy を作成
      const policyStatement = new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: [
          "ecr:PutRegistryScanningConfiguration",
          "inspector2:ListAccountPermissions",
          "inspector2:Disable",
          "inspector2:Enable",
          "iam:CreateServiceLinkedRole",
        ],
        resources: ["*"],
      });
      // カスタムリソースのイベントハンドラ (Lambda Function)を作成
      const lambdaFunction = new nodejsLambda.NodejsFunction(
        this,
        "OnEventHandler",
        {
          runtime: lambda.Runtime.NODEJS_18_X,
          entry: path.join(__dirname, "./src/index.ts"),
          handler: "handler",
          initialPolicy: [policyStatement],
        }
      );
      // カスタムリソースプロバイダーを作成
      const provider = new cr.Provider(this, "Provider", {
        onEventHandler: lambdaFunction,
      });
      // カスタムリソース作成
      new cdk.CustomResource(this, "CustomResource", {
        serviceToken: provider.serviceToken,
        properties: props,
      });
    }
  }

onEvent handler (lib\construct\src\index.ts)

  • Lambda (onEvent handler)の実装となります。
  • SDKを呼びだして、ECR プライベートレジストリのスキャン設定を変更します。
  • 更新対象にIDは必要ないので、物理IDは特に設定していません。

  import {
    ECRClient,
    PutRegistryScanningConfigurationCommand,
    PutRegistryScanningConfigurationCommandInput,
    PutRegistryScanningConfigurationCommandOutput,
    ScanType,
    RegistryScanningRule,
  } from "@aws-sdk/client-ecr";
  import { CdkCustomResourceHandler } from "aws-lambda";
  
  // ResourcePropertiesの期待する入力の型を定義
  export interface ResourceProperties {
    scanType: ScanType;
    rules: RegistryScanningRule[];
  }
  
  const client = new ECRClient({});
  
  /** Lambdaのエントリーポイント */
  export const handler: CdkCustomResourceHandler = async (event) => {
    // ResourcePropertiesに更新対象のプロパティが含まれる。
    const properties: ResourceProperties = {
      scanType: event.ResourceProperties.scanType,
      rules: event.ResourceProperties.rules,
    };
    // RequestTypeにイベントフラグが含まれる。
    // RequestTypeに応じて各処理を実装。
    switch (event.RequestType) {
      case "Create":
        await onCreate(properties);
        return {};
      case "Update":
        await onUpdate(properties);
        return {};
      case "Delete":
        await onDelete();
        return {};
      default:
        throw new Error("Failed");
    }
  };
  
  /**Create の処理 */
  async function onCreate(
    props: ResourceProperties
  ): Promise {
    const command = new PutRegistryScanningConfigurationCommand(props);
    const result = await client.send(command);
    return result;
  }
  
  /**Update の処理 */
  async function onUpdate(
    props: ResourceProperties
  ): Promise {
    const command = new PutRegistryScanningConfigurationCommand(props);
    const result = await client.send(command);
    return result;
  }
  
  /**Delete の処理 */
  async function onDelete(): Promise {
    // Delete処理では、デフォルト設定に戻す。
    const input: PutRegistryScanningConfigurationCommandInput = {
      rules: [],
      scanType: "BASIC",
    };
    const command = new PutRegistryScanningConfigurationCommand(input);
    const result = await client.send(command);
    return result;
  }

動作確認

「Create/Update/Delete」イベントの各処理が正常に処理されるか確認します。

「Create/Update/Delete」イベントの正常系の動作に関しては、正常に動作しているようです。
なお、本記事では記載していませんが、異常系の処理(ロールバック時の処理等)についても動作確認をする必要があります。

AwsCustomResource で実装した場合

前述のカスタムリソースは単純にPutRegistryScanningConfigurationCommandを使用するだけなので、実は、
AwsCustomResourceを使用してさらに簡単に実装することが可能です。

AwsCustomResource のコード

  • 前述のecr-registry-scanning-configuration-construct.tsを以下のコードで置換すると同様の処理がAwsCustomResourceで実現できます。
  • また、Lambda (onEvent handler)処理のlib\construct\src\index.tsも不要です。

  import { Construct } from "constructs";
  import * as cr from "aws-cdk-lib/custom-resources";
  import * as iam from "aws-cdk-lib/aws-iam";
  import { PutRegistryScanningConfigurationCommandInput } from "@aws-sdk/client-ecr";
  
  export class EcrRegistryScanningConfigurationConstruct extends Construct {
    constructor(
      scope: Construct,
      id: string,
      props: PutRegistryScanningConfigurationCommandInput
    ) {
      super(scope, id);
      // IAMポリシー
      const policyStatement = new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: [
          "ecr:PutRegistryScanningConfiguration",
          "inspector2:ListAccountPermissions",
          "inspector2:Disable",
          "inspector2:Enable",
          "iam:CreateServiceLinkedRole",
        ],
        resources: ["*"],
      });
      // カスタムリソースを作成
      new cr.AwsCustomResource(this, "CustomResource", {
        onCreate: {
          action: "PutRegistryScanningConfigurationCommand",
          service: "ECR",
          parameters: props,
          physicalResourceId: cr.PhysicalResourceId.of("ecr"),//本ケースでは、物理IDは必要ないので適当に指定。
        },
        onUpdate: {
          action: "PutRegistryScanningConfigurationCommand",
          service: "ECR",
          parameters: props,
        },
        onDelete: {
          action: "PutRegistryScanningConfigurationCommand",
          service: "ECR",
          parameters: {
            rules: [],
            scanType: "BASIC",
          },
        },
        policy: cr.AwsCustomResourcePolicy.fromStatements([policyStatement]),
      });
    }
  }

まとめ

CDKでは、Provider Frameworkがカスタムリソースのコントロールの大部分を管理してくれるため、カスタムリソースを比較的に簡単に実装できます。
また、プロビジョニングの各処理が1つのSDKのAPIのみで完結するのであれば、AwsCustomResourceを使用することで、Lambdaの実装自体も不要となります。
しかしながら、今回は簡単な例でしたので動作に特に問題ありませんでしたが、参照関係を含んだり、複雑な処理を含む処理のカスタムリソースを作成する場合、様々なリソースの状態を考慮して実装する必要があります。
中途半端な実装のカスタムリソースをプロダクション環境に導入した場合、最悪、ロールバックに失敗し、スタックが壊れる可能性もありますので、入念にテストしてから導入しましょう。

CTCは、AWSのビジネス利活用に向けて、お客様のステージに合わせた幅広い構築・運用支援サービスを提供しています。
経験豊富なエンジニアが、ワンストップかつ柔軟にご支援します。
ぜひ、お気軽にお問い合わせください。

お問い合わせ



【著者プロフィール】

山本 和輝(やまもと かずき)

伊藤忠テクノソリューションズ株式会社 クラウドアーキテクト

クラウドネイティブ技術を活用したシステム基盤の提案・構築や内製化支援等の案件を担当。

TOP>コラム一覧>CDK で Custom resources を作成する

pagetop