実践!AWS CDK #32 Secrets Manager Stack

題字・息子たち
2022.02.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

今回は Secrets Manager のスタックを実装していきます。

前回の記事はこちら。

実装

追加するファイルは以下の通り。

├── lib
│   ├── resource
│   │   └── secret.ts
│   └── stack
│       └── secrets-manager-stack.ts
├── test
│   └── stack
│       └── secrets-manager-stack.test.ts

Secrets Manager のスタッククラスの実装はこちら。

lib/stack/secrets-manager-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Secret } from '../resource/secret';

export class SecretsManagerStack extends Stack {
    public readonly secret: Secret;

    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props);

        this.secret = new Secret(this);
    }
}

自前の Secret クラスのインスタンスを生成しています。他のリソースとの依存関係はないため、コンストラクタにスタックオブジェクトのパラメーターはありません。

以下が Secret クラスの実装です。
以前からの変更も特になし。

lib/resource/secret.ts

import { CfnSecret } from "aws-cdk-lib/aws-secretsmanager";
import { Construct } from "constructs";
import { BaseResource } from "./abstract/base-resouce";

export const OSecretKey = {
    MasterUsername: 'MasterUsername',
    MasterUserPassword: 'MasterUserPassword'
} as const;
type SecretKey = typeof OSecretKey[keyof typeof OSecretKey];

interface ResourceInfo {
    readonly id: string;
    readonly description: string;
    readonly generateSecretString: CfnSecret.GenerateSecretStringProperty;
    readonly resourceName: string;
    readonly assign: (secret: CfnSecret) => void;
}

export class Secret extends BaseResource {
    public readonly rdsCluster: CfnSecret;

    private static readonly rdsClusterMasterUsername = 'admin';
    private readonly resources: ResourceInfo[] = [{
        id: 'SecretRdsCluster',
        description: 'for RDS cluster',
        generateSecretString: {
            excludeCharacters: '"@/\\\'',
            generateStringKey: OSecretKey.MasterUserPassword,
            passwordLength: 16,
            secretStringTemplate: `{"${OSecretKey.MasterUsername}": "${Secret.rdsClusterMasterUsername}"}`
        },
        resourceName: 'secret-rds-cluster',
        assign: secret => (this.rdsCluster as CfnSecret) = secret
    }];

    constructor(scope: Construct) {
        super();

        for (const resourceInfo of this.resources) {
            const secret = this.createSecret(scope, resourceInfo);
            resourceInfo.assign(secret);
        }
    }

    public static getDynamicReference(secret: CfnSecret, secretKey: SecretKey): string {
        return `{{resolve:secretsmanager:${secret.ref}:SecretString:${secretKey}}}`;
    }

    private createSecret(scope: Construct, resourceInfo: ResourceInfo): CfnSecret {
        const secret = new CfnSecret(scope, resourceInfo.id, {
            description: resourceInfo.description,
            generateSecretString: resourceInfo.generateSecretString,
            name: this.createResourceName(scope, resourceInfo.resourceName)
        });

        return secret;
    }
}

メインのスタッククラスの実装はこちら。

lib/devio-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Ec2Stack } from './stack/ec2-stack';
import { IamStack } from './stack/iam-stack';
import { SecretsManagerStack } from './stack/secrets-manager-stack';
import { VpcStack } from './stack/vpc-stack';

export class DevioStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // VPC Stack
    const vpcStack = new VpcStack(scope, 'VpcStack', {
      stackName: this.createStackName(scope, 'vpc')
    });

    // IAM Stack
    const iamStack = new IamStack(scope, 'IamStack', {
      stackName: this.createStackName(scope, 'iam')
    });

    // EC2 Stack
    new Ec2Stack(scope, 'Ec2Stack', vpcStack, iamStack, {
      stackName: this.createStackName(scope, 'ec2')
    });

    // Secrets Manager Stack
    new SecretsManagerStack(scope, 'SecretsManagerStack', {
      stackName: this.createStackName(scope, 'secrets-manager')
    });
  }

  private createStackName(scope: Construct, originalName: string): string {
    const systemName = scope.node.tryGetContext('systemName');
    const envType = scope.node.tryGetContext('envType');
    const stackNamePrefix = `${systemName}-${envType}-stack-`;

    return `${stackNamePrefix}${originalName}`;
  }
}

ハイライト部分を追記しています。

テスト

テストコードはこちら。

test/stack/secrets-manager-stack.test.ts

import { App } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { SecretsManagerStack } from '../../lib/stack/secrets-manager-stack';

test('Secrets Manager Stack', () => {
    const app = new App({
        context: {
            'systemName': 'devio',
            'envType': 'stg'
        }
    });
    const secretsManagerStack = new SecretsManagerStack(app, 'SecretsManagerStack');
    const template = Template.fromStack(secretsManagerStack);

    template.resourceCountIs('AWS::SecretsManager::Secret', 1);
    template.hasResourceProperties('AWS::SecretsManager::Secret', {
        Description: 'for RDS cluster',
        GenerateSecretString: {
            ExcludeCharacters: '"@/\\\'',
            GenerateStringKey: 'MasterUserPassword',
            PasswordLength: 16,
            SecretStringTemplate: '{"MasterUsername": "admin"}'
        },
        Name: 'devio-stg-secret-rds-cluster'
    });
});

GitHub

今回のソースコードは コチラ です。

おわりに

スタック分割リファクタイングもいよいよ終盤です。残るは RDS スタックのみ。次回、最終回です。