実践!AWS CDK #29 IAM Stack

題字・息子たち
2021.12.20

はじめに

スタック分割リファクタリングの第 2 弾!
IAM スタックの実装です。

前回の記事はこちら。

実装

IAM スタックで管理するリソースは現状 IAM ロールのみです。よって今回は以下の 3 ファイルを追加していきます。

├── lib
│   ├── resource
│   │   └── role.ts
│   └── stack
│       └── iam-stack.ts
└── test
    └── stack
        └── iam-stack.test.ts

まずは IAM スタッククラスの実装から。

lib/stack/iam-stack.ts

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

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

        // Role
        new Role(this);
    }
}

非常にシンプルです。自前の Role クラスを生成しているだけ。
将来的に IAM の ユーザーポリシー といったリソースを作成したい場合は、このクラスにそれらの生成処理を記述していくことになります。

そして以下が Role クラスの実装です。

lib/resource/role.ts

import { Construct } from 'constructs';
import { CfnRole, CfnInstanceProfile, PolicyDocument, PolicyStatement, PolicyStatementProps, Effect, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { BaseResource } from './abstract/base-resouce';

interface InstanceProfileInfo {
    readonly id: string;
    readonly assign: (instanceProfile: CfnInstanceProfile) => void;
}

interface ResourceInfo {
    readonly id: string;
    readonly policyStatementProps: PolicyStatementProps;
    readonly managedPolicyArns: string[];
    readonly resourceName: string;
    readonly instanceProfile?: InstanceProfileInfo;
    readonly assign: (role: CfnRole) => void;
}

export class Role extends BaseResource {
    public readonly ec2: CfnRole;
    public readonly rds: CfnRole;
    public readonly instanceProfileEc2: CfnInstanceProfile;

    private readonly resources: ResourceInfo[] = [
        {
            id: 'RoleEc2',
            policyStatementProps: {
                effect: Effect.ALLOW,
                principals: [new ServicePrincipal('ec2.amazonaws.com')],
                actions: ['sts:AssumeRole']
            },
            managedPolicyArns: [
                'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore'
            ],
            resourceName: 'role-ec2',
            instanceProfile: {
                id: 'InstanceProfileEc2',
                assign: instanceProfile => (this.instanceProfileEc2 as CfnInstanceProfile) = instanceProfile
            },
            assign: role => (this.ec2 as CfnRole) = role
        },
        {
            id: 'RoleRds',
            policyStatementProps: {
                effect: Effect.ALLOW,
                principals: [new ServicePrincipal('monitoring.rds.amazonaws.com')],
                actions: ['sts:AssumeRole']
            },
            managedPolicyArns: [
                'arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole'
            ],
            resourceName: 'role-rds',
            assign: role => (this.rds as CfnRole) = role
        }
    ];

    constructor(scope: Construct) {
        super();

        for (const resourceInfo of this.resources) {
            const role = this.createRole(scope, resourceInfo);
            resourceInfo.assign(role);

            const instanceProfileInfo = resourceInfo.instanceProfile;
            if (instanceProfileInfo) {
                const instanceProfile = this.createInstanceProfile(scope, instanceProfileInfo, role);
                instanceProfileInfo.assign(instanceProfile);
            }
        }
    }

    private createRole(scope: Construct, resourceInfo: ResourceInfo): CfnRole {
        const policyStatement = new PolicyStatement(resourceInfo.policyStatementProps);

        const policyDocument = new PolicyDocument({
            statements: [policyStatement]
        });

        const role = new CfnRole(scope, resourceInfo.id, {
            assumeRolePolicyDocument: policyDocument,
            managedPolicyArns: resourceInfo.managedPolicyArns,
            roleName: this.createResourceName(scope, resourceInfo.resourceName)
        });

        return role;
    }

    private createInstanceProfile(scope: Construct, instanceProfileInfo: InstanceProfileInfo, role: CfnRole): CfnInstanceProfile {
        const instanceProfile = new CfnInstanceProfile(scope, instanceProfileInfo.id, {
            roles: [role.ref],
            instanceProfileName: role.roleName
        });

        return instanceProfile;
    }
}

基本は本シリーズの #15 IAM ロール と同じ実装です。なんとなく、インスタンスプロファイルの生成処理を別メソッドに切り出しました。(多少の改善?)

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

lib/devio-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { IamStack } from './stack/iam-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
    new VpcStack(scope, 'VpcStack', {
      stackName: this.createStackName(scope, 'vpc')
    });

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

  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}`;
  }
}

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

IAM スタックには VPC スタックとの依存関係はないので、特にパラメーターを渡すことなく IAM スタッククラスを生成しています。

テスト

こちらは特に以前からの変更は無し。
いつも通り、リソースの数とプロパティをチェックしています。

test/stack/iam-stack.test.ts

import { App } from 'aws-cdk-lib';
import { Match, Template } from 'aws-cdk-lib/assertions';
import { IamStack } from '../../lib/stack/iam-stack';

test('Iam Stack', () => {
    const app = new App({
        context: {
            'systemName': 'devio',
            'envType': 'stg'
        }
    });
    const iamStack = new IamStack(app, 'IamStack');
    const template = Template.fromStack(iamStack);

    // Role
    template.resourceCountIs('AWS::IAM::Role', 2);
    template.hasResourceProperties('AWS::IAM::Role', {
        AssumeRolePolicyDocument: {
            Statement: [{
                Effect: 'Allow',
                Principal: {
                    Service: Match.anyValue()
                },
                Action: 'sts:AssumeRole'
            }]
        },
        ManagedPolicyArns: [
            'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore'
        ],
        RoleName: 'devio-stg-role-ec2'
    });
    template.hasResourceProperties('AWS::IAM::Role', {
        AssumeRolePolicyDocument: {
            Statement: [{
                Effect: 'Allow',
                Principal: {
                    Service: 'monitoring.rds.amazonaws.com'
                },
                Action: 'sts:AssumeRole'
            }]
        },
        ManagedPolicyArns: [
            'arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole'
        ],
        RoleName: 'devio-stg-role-rds'
    });
    template.resourceCountIs('AWS::IAM::InstanceProfile', 1);
    template.hasResourceProperties('AWS::IAM::InstanceProfile', {
        Roles: Match.anyValue(),
        InstanceProfileName: 'devio-stg-role-ec2'
    });
});

デプロイ

IAM スタックを単体でデプロイする場合は以下のコマンドを実行します。

$ cdk deploy IamStack

これで VPC スタックは作成されず、IAM スタックのみが作成されることになります。デプロイ範囲を絞ることができるというスタック分割の利点です。

GitHub

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

おわりに

今回の修正によって、本シリーズの CDK プロジェクトで初めて複数スタックを管理するようになりました。
これまでは一括ですべてのリソースがデプロイされていたため、特定リソースのデプロイ結果を確認したい場合は他のリソースのデプロイを待つかプログラムをコメントアウトしてデプロイさせないよう手を加える必要がありました。しかしこのようにスタックを分割し、デプロイ範囲を絞ることで結果の確認もしやすくなると思います。

リンク