コンテンツにスキップ

CDK オーバーライド

Attention

⚠ CloudFormation テンプレートのオーバーライドは高度な機能であり、スタックのデプロイが正常にデプロイされない原因となる可能性があります。 注意して使用してください!

Copilot は Manifest で指定した設定を使用して CloudFormation テンプレートを生成します。 一方で、 Manifest では、全ての CloudFormation プロパティが設定可能となってはいません。 例えば、ワークロードコンテナのために、Ulimits を 設定したい場合がありますが、そのプロパティは Manifest では公開されていません。

yamlpatchcdk によるオーバーライドでは、CloudFormation テンプレートの_任意_のプロパティやリソースを追加、削除、置換できます。

YAML パッチよりも CDK オーバーライドを使用するのはどの様な場合ですか?

どちらのオプションも Copilot Manifest によって表面化されない機能にアクセスして設定するという"ガラスを壊す"仕組みです。

プログラミング言語の表現力が必要な場合、YAML パッチよりも AWS Cloud Development Kit (CDK) オーバーライドをお勧めします。 CDK を使えば、CloudFormation テンプレートに安全かつ強力な修正が行えます。

始め方

copilot [noun] override コマンドを実行すると、CDK を使って CloudFormation テンプレートを拡張できます。 例えば、copilot svc override コマンドにより、 Load Balanced Web Service のテンプレートを更新します。

コマンドは、以下の様な構造で、copilot/[name]/override 配下に新しい CDK アプリケーションを作成します。

.
├── bin/
│   └── override.ts
├── .gitignore
├── cdk.json
├── package.json
├── README.md
├── stack.ts
└── tsconfig.json

stack.ts を編集するとこから開始します。例えば、 copilot svc override を利用して、 ECS サービスのプロパティをオーバーライドする場合、次の様な stack.ts ファイルが生成されので、修正をします。

import * as cdk from 'aws-cdk-lib';
import { aws_ecs as ecs } from 'aws-cdk-lib';

export class TransformedStack extends cdk.Stack {
    constructor (scope: cdk.App, id: string, props?: cdk.StackProps) {
        super(scope, id, props);
         this.template = new cdk.cloudformation_include.CfnInclude(this, 'Template', {
            templateFile: path.join('.build', 'in.yaml'),
        });
        this.appName = template.getParameter('AppName').valueAsString;
        this.envName = template.getParameter('EnvName').valueAsString;

        this.transformService();
    }

    // TODO: implement me.
    transformService() {
      const service = this.template.getResource("Service") as ecs.CfnService;
      throw new error("not implemented");
    }
}

どの様な仕組みでしょうか?

上記の stack.ts ファイルを見ればわかるとおり、Copilot は cloudformation_include module を使用します。これは、CDK が提供している、変換を支援する為のライブラリです。 このライブラリは "Import or migrate an existing AWS CloudFormation template" ガイドで推奨されています。 このライブラリにより Copilot Manifest によって表示されないリソースに対して、 L1 constructs としてアクセスできる様になります。 CfnInclude オブジェクトは、隠された .build/in.yaml CloudFormation テンプレートから初期化されます。 これが Copilot と CDK のコミュニケーション方法です。 Copilot は .build/ ディレクトリ配下に Manifest より生成された CloudFormation テンプレートを出力します。それを cloudformation_include ライブラリで解析し、CDK コンストラクトに変換します。

copilot [noun] package または copilot [noun] deploy を実行するたびに、Copilot はまず Manifest ファイルから CloudFormation テンプレートを作成します。それを CDK アプリケーションに渡して、プロパティをオーバーライドします。

デプロイの前に CDK の変更を確認するために、package または deploy コマンドで --diff フラグを使用することを強くお勧めします。

実行例

次の例は Elastic IP アドレスを Network Load Balancer へ割り当てる為に Load Balanced Web Service の nlb リソースを修正しています。

この例で、以下の方法を確認できます。

  • リソースのプロパティ削除
  • 新しいリソースの作成
  • 既存リソースのプロパティの修正
stack.ts のサンプルを見る
import * as cdk from 'aws-cdk-lib';
import * as path from 'path';
import { aws_elasticloadbalancingv2 as elbv2 } from 'aws-cdk-lib';
import { aws_ec2 as ec2 } from 'aws-cdk-lib';

interface TransformedStackProps extends cdk.StackProps {
    readonly appName: string;
    readonly envName: string;
}

export class TransformedStack extends cdk.Stack {
    public readonly template: cdk.cloudformation_include.CfnInclude;
    public readonly appName: string;
    public readonly envName: string;

    constructor (scope: cdk.App, id: string, props: TransformedStackProps) {
        super(scope, id, props);
        this.template = new cdk.cloudformation_include.CfnInclude(this, 'Template', {
            templateFile: path.join('.build', 'in.yml'),
        });
        this.appName = props.appName;
        this.envName = props.envName;
        this.transformPublicNetworkLoadBalancer();
    }

    /**
     * transformPublicNetworkLoadBalancer removes the "Subnets" properties from the NLB,
     * and adds a SubnetMappings with predefined elastic IP addresses.
     */
    transformPublicNetworkLoadBalancer() {
        const elasticIPs = [new ec2.CfnEIP(this, 'ElasticIP1'), new ec2.CfnEIP(this, 'ElasticIP2')];
        const publicSubnets = cdk.Fn.importValue(`${this.appName}-${this.envName}-PublicSubnets`);

        // Apply the override.
        const nlb = this.template.getResource("PublicNetworkLoadBalancer") as elbv2.CfnLoadBalancer;
        nlb.addDeletionOverride('Properties.Subnets');
        nlb.subnetMappings = [{
            allocationId: elasticIPs[0].attrAllocationId,
            subnetId: cdk.Fn.select(0, cdk.Fn.split(",", publicSubnets)),
        }, {
            allocationId: elasticIPs[1].attrAllocationId,
            subnetId: cdk.Fn.select(1, cdk.Fn.split(",", publicSubnets)),
        }]
    }
}

次の例では、 プロダクション環境など特定の環境だけのためにプロパティを追加する方法を紹介します。

stack.ts のサンプルを見る
import * as cdk from 'aws-cdk-lib';
import * as path from 'path';
import { aws_iam as iam } from 'aws-cdk-lib';

interface TransformedStackProps extends cdk.StackProps {
    readonly appName: string;
    readonly envName: string;
}

export class TransformedStack extends cdk.Stack {
    public readonly template: cdk.cloudformation_include.CfnInclude;
    public readonly appName: string;
    public readonly envName: string;

    constructor (scope: cdk.App, id: string, props: TransformedStackProps) {
        super(scope, id, props);
        this.template = new cdk.cloudformation_include.CfnInclude(this, 'Template', {
            templateFile: path.join('.build', 'in.yml'),
        });
        this.appName = props.appName;
        this.envName = props.envName;
        this.transformEnvironmentManagerRole();
    }

    transformEnvironmentManagerRole() {
        const environmentManagerRole = this.template.getResource("EnvironmentManagerRole") as iam.CfnRole;
        if (this.envName === "prod") {
            let assumeRolePolicy = environmentManagerRole.assumeRolePolicyDocument
            let statements = assumeRolePolicy.Statement
            statements.push({
                 "Effect": "Allow",
                 "Principal": { "Service": "ec2.amazonaws.com" },
                 "Action": "sts:AssumeRole"
            })
        }
    }
}

次の例ではリソースを削除する方法を示しています。具体的には Copilot が作成し Service のログを保持するデフォルトのロググループです。

stack.ts のサンプルを見る
import * as cdk from 'aws-cdk-lib';
import * as path from 'path';

interface TransformedStackProps extends cdk.StackProps {
    readonly appName: string;
    readonly envName: string;
}

export class TransformedStack extends cdk.Stack {
    public readonly template: cdk.cloudformation_include.CfnInclude;
    public readonly appName: string;
    public readonly envName: string;

    constructor(scope: cdk.App, id: string, props: TransformedStackProps) {
        super(scope, id, props);
        this.template = new cdk.cloudformation_include.CfnInclude(this, 'Template', {
        templateFile: path.join('.build', 'in.yml'),
        });
        this.appName = props.appName;
        this.envName = props.envName;
        // Deletes the default log group resource.
        this.template.node.tryRemoveChild("LogGroup")
    }
}