IAMロールが多すぎる？CDKでのCodePipeline設計を見直すポイント

AWS CDK（Cloud Development Kit）は、AWSリソースの定義やデプロイを効率的に行える便利なツールです。しかしその便利さゆえに、リソース数が増えすぎてしまうという課題に直面したため、本記事では特にCDKでCodePipelineを作成する際のIAMロール数を節約する方法について解説します。

※AWS CDKの詳細について知りたい方は、[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html)をご覧ください。

# 所属部署でのCDK活用事例

私の所属する部署では、管轄下にあるAWSリソースをCDKで管理することを推進しています。この取り組みによって、以下のような多くの恩恵を受けています：

- リソース構成や変更履歴の見える化：コードベースでリソースが定義されているため、どのようなリソースがどんな目的で作成されたが一目でわかります。
- デプロイの自動化：手動での操作を最小限に抑え、自動化されたデプロイパイプラインを構築しています。
- コードベースでの変更依頼が可能：チケットを切る代わりに、プルリクエスト（PR）を作成することで変更を提案でき、レビューを通してスムーズに反映できます。

このように、CDKの導入は大きなメリットをもたらしましたが、一方で新たな課題も浮かび上がってきました。

# 課題：CDKで作成されるロール数の肥大化

同部署では、CloudFormationテンプレートで記載されたレガシーなリソースをデプロイするためのCodePipelineも同様にCDKで作成しています。

**CloudFormationでCodePipelineを作成する場合**は、各ステージのアクションで実行される操作に十分な権限を持つサービスロールを自分で作成し、それをCodePipelineに紐付ける必要があります。
一方、**CDKでCodePipelineを作成する場合**は、必要なIAMロールを自動で作成して紐付けてくれるため、非常に便利です。

しかし、この仕組みには一つ問題があります。**アクションごとに個別のIAMロールが作成されるため、ロール数がどんどん増えて行ってしまう**のです。

### 具体例

CloudFormationテンプレートを元にスタックをデプロイするためのCodePipelineを、CDKで作成することを考えます。

```typescript
    const deployStage = this.pipeline.addStage({
      stageName: 'Deploy',
    });
    // 各スタックについてChangeSetの作成・実行用のアクションを作成
    for (const stack of props.stacks) {
      deployStage.addAction(
        new codepipeline_actions.CloudFormationCreateReplaceChangeSetAction({
          actionName: `CreateChangeSet_${stack.name}`,
          stackName: stack.name,
          changeSetName: `${stack.name}-ChangeSet`,
          templatePath: sourceOutput.atPath(stack.templateFilePath),
          adminPermissions: true,
          region: stack.region,
          runOrder: 1
        }),
      );
    }
    deployStage.addAction(
      new codepipeline_actions.ManualApprovalAction({
        actionName: 'ManualApproval',
        additionalInformation:
          '変更があったstackについて、changesetの内容を確認して問題無ければ承認してください。',
        runOrder: 2,
      }),
    );
    for (const stack of props.stacks) {
      deployStage.addAction(
        new codepipeline_actions.CloudFormationExecuteChangeSetAction({
          actionName: `ExecuteChangeSet_${stack.name}`,
          stackName: stack.name,
          changeSetName: `${stack.name}-ChangeSet`,
          region: stack.region,
          runOrder: 3
        }),
      );
    }
```

この場合、それぞれのアクションを作成するタイミングで、スタック毎に以下のロールが同時に作成されます：

1. ChangeSet作成用のロール
2. ChangeSet実行用のロール
3. スタックに紐付けるサービスロール

**スタックが10個あると、合計で30個のIAMロールが作成されます**。このようなロールの肥大化は、管理やアカウント制限の観点から避けたいところです。

# 解決策：共用のIAMロールを使用する

CDKでは、CodePipelineの多くのアクションクラスがroleというプロパティを持っています（参考: ActionPropertiesに関する[ドキュメント](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_codepipeline.ActionProperties.html)）。このプロパティを利用すれば、最初に共用のIAMロールを作成し、それをそれぞれのアクションで使い回すことができます。

```typescript
    // ChangeSetを作成するアクションのための共用ロール
    const createChangeSetActionRole = new iam.Role(
      this,
      'CreateChangeSetActionRole',
      {
        assumedBy: new iam.AccountPrincipal(cdk.Stack.of(this).account),
      },
    );
    // ChangeSetを実行するアクションのための共用ロール
    const executeChangeSetActionRole = new iam.Role(
      this,
      'ExecuteChangeSetActionRole',
      {
        assumedBy: new iam.AccountPrincipal(cdk.Stack.of(this).account),
      },
    );
    const deployStage = this.pipeline.addStage({
      stageName: 'Deploy',
    });
    for (const stack of props.stacks) {
      deployStage.addAction(
        new codepipeline_actions.CloudFormationCreateReplaceChangeSetAction({
          actionName: `CreateChangeSet_${stack.name}`,
          stackName: stack.name,
          changeSetName: `${stack.name}-ChangeSet`,
          templatePath: sourceOutput.atPath(stack.templateFilePath),
          adminPermissions: true,
          region: stack.region,
          runOrder: 1,
          // 作成した共用ロールをroleプロパティに指定
          role: createChangeSetActionRole
        }),
      );
    }
    deployStage.addAction(
      new codepipeline_actions.ManualApprovalAction({
        actionName: 'ManualApproval',
        additionalInformation:
          '変更があったstackについて、changesetの内容を確認して問題無ければ承認してください。',
        runOrder: 2,
      }),
    );
    for (const stack of props.stacks) {
      deployStage.addAction(
        new codepipeline_actions.CloudFormationExecuteChangeSetAction({
          actionName: `ExecuteChangeSet_${stack.name}`,
          stackName: stack.name,
          changeSetName: `${stack.name}-ChangeSet`,
          region: stack.region,
          runOrder: 3,
          // 作成した共用ロールをroleプロパティに指定
          role: executeChangeSetActionRole,
        }),
      );
    }
```

これにより、必要なロール数を大幅に削減できます。

# ロールの権限は自動で付与される

ここで重要なのは、渡すロールの権限を事前に設定しておく必要がないという点です。CDKはアクションごとに必要な最小限の権限をロールに付与してくれます。

CloudFormationExecuteChangeSetActionならCloudFormationのChangeSet実行権限、S3DeployActionならS3への操作権限が自動で設定されます。この仕組みにより、無駄のない最小権限の運用が可能です。

たとえば、上記コード例のChangeSetを実行するアクション用の共用ロールについて、cdk synthした際の出力は以下のようになります。
（リソースの論理IDについて、本来であればprefixにスタック名、suffixにランダムなIDが付与されますが、簡単のため省略しています）

```yaml
  ExecuteChangeSetActionRole: 
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS: arn:aws:iam::***:root
        Version: "2012-10-17"
  ExecuteChangeSetActionRoleDefaultPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action:
              - cloudformation:DescribeChangeSet
              - cloudformation:DescribeStackEvents
              - cloudformation:DescribeStacks
              - cloudformation:ExecuteChangeSet
            Condition:
              StringEqualsIfExists:
                cloudformation:ChangeSetName: stack-hoge-ChangeSet
            Effect: Allow
            Resource: arn:aws:cloudformation:ap-northeast-1:***:stack/stack-hoge/*
          - Action:
              - cloudformation:DescribeChangeSet
              - cloudformation:DescribeStackEvents
              - cloudformation:DescribeStacks
              - cloudformation:ExecuteChangeSet
            Condition:
              StringEqualsIfExists:
                cloudformation:ChangeSetName: stack-fuga-ChangeSet
            Effect: Allow
            Resource: arn:aws:cloudformation:ap-northeast-1:***:stack/stack-fuga/*
          # 以降、スタックの分だけ権限が作成される
        Version: "2012-10-17"
      PolicyName: ExecuteChangeSetActionRoleDefaultPolicy
      Roles:
        - Ref: ExecuteChangeSetActionRole
```

コード側では定義していない、ExecuteChangeSetActionRoleDefaultPolicyというIAMポリシーが作成され、操作内容・操作対象ともに必要最小限の範囲で設定された権限が紐付けられていることが分かります。

# サービスロールの共用化も可能

一部のアクションでは、`role`以外にもロールを受け取るプロパティが用意されていることがあります。
Changeset作成用のアクション（CloudFormationCreateReplaceChangeSetAction）のdeploymentRoleプロパティがそれに当たります（詳細は[ドキュメント](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_codepipeline_actions.CloudFormationCreateReplaceChangeSetActionProps.html#deploymentrole)を参照）。
これは、スタックに紐付けるサービスロールを指定するものです。

デフォルトではこれもスタックごとに新しいものが作成されますが、これまでと同様に、共用のロールを作成して渡すこともできます。この場合は、必要な権限を事前にロールに対して紐付けておく必要があります。

```typescript
    // スタックに紐付けるための共用のサービスロール
    const administratorActionRole = new iam.Role(
      this,
      'AdministratorActionRole',
      {
        assumedBy: new iam.ServicePrincipal('cloudformation.amazonaws.com'),
        inlinePolicies: {
          FullAccess: new iam.PolicyDocument({
            statements: [
              new iam.PolicyStatement({
                actions: ['*'],
                resources: ['*'],
              }),
            ],
          }),
        },
      },
    );
    ...
    const deployStage = this.pipeline.addStage({
      stageName: 'Deploy',
    });
    for (const stack of props.stacks) {
      deployStage.addAction(
        new codepipeline_actions.CloudFormationCreateReplaceChangeSetAction({
          actionName: `CreateChangeSet_${stack.name}`,
          stackName: stack.name,
          changeSetName: `${stack.name}-ChangeSet`,
          templatePath: sourceOutput.atPath(stack.templateFilePath),
          adminPermissions: true,
          region: stack.region,
          runOrder: 1,
          role: createChangeSetActionRole,
          // 作成した共用ロールをdeploymentRoleプロパティに指定
          deploymentRole: administratorActionRole,
        }),
      );
    }
    ...
```

# まとめ

AWS CDKは強力で便利なツールですが、その便利さゆえに発生するリソースの肥大化には注意が必要です。今回はその中でも、CodePipelineを作成する際のIAMロール数を節約する方法について解説させていただきました。

今回紹介した共用ロールを活用する方法を取り入れることで、IAMリソースの管理を効率化し、より良いCDKライフを送ることができるでしょう。CDKでCodePipelineを利用する機会があれば、ぜひ試してみてください！