実践!AWS CDK #15 IAM ロール
はじめに
今回は IAM ロールを作成します。
簡単に行くと思いきや、意外とハマりどころがあったり学びが多い回でした。
前回の記事はこちら。
AWS 構成図
現状
前回まででこのような環境を構築してきました。
将来
将来的にはこの形にしていきます。
今回作成する IAM ロールは EC2 と RDS にアタッチするものとなります。
設計
プロパティは以下の通りです。
ロール名 | 信頼されたエンティティ | AWS 管理ポリシー |
---|---|---|
devio-stg-role-ec2 | ec2.amazonaws.com | AmazonSSMManagedInstanceCore AmazonRDSFullAccess |
devio-stg-role-rds | monitoring.rds.amazonaws.com | AmazonRDSEnhancedMonitoringRole |
EC2 へのアクセスは AWS Systems Manager の Session Manager から行うことを想定します。よって必要なポリシー AmazonSSMManagedInstanceCore
を付与しています。(セキュリティグループで ssh のポートは開放しません)
また RDS へアクセスするために AmazonRDSFullAccess
ポリシーも付与します。今回は FullAccess 権限を付与していますが、実際の構築では最小権限の原則に従って必要な権限のみを与えてください。
[お詫び]
すみません。これまたしばらく経ってから気づきましたが、RDS インスタンス内の DB にアクセスするだけであれば RDS に関するポリシーは不要でした。
適切なセキュリティグループを設定し、標準のクライアントアプリケーションまたは DB エンジン用のユーティリティを使用することで DB に接続可能です。
GitHub のソースコードからは近々 AmazonRDSFullAccess
を取り除いておきます。
RDS では拡張モニタリングを有効化するため AmazonRDSEnhancedMonitoringRole
ポリシーを付与します。
実装
IAM ロールに関する処理を行うクラスはこちら。
import * as cdk from '@aws-cdk/core'; import { CfnRole, CfnInstanceProfile, PolicyDocument, PolicyStatement, PolicyStatementProps, Effect, ServicePrincipal } from '@aws-cdk/aws-iam'; import { Resource } from './abstract/resource'; interface ResourceInfo { readonly id: string; readonly policyStatementProps: PolicyStatementProps; readonly managedPolicyArns: string[]; readonly roleName: string; readonly assign: (role: CfnRole) => void; } export class IamRole extends Resource { public ec2: CfnRole; public rds: CfnRole; public 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', 'arn:aws:iam::aws:policy/AmazonRDSFullAccess' ], roleName: 'role-ec2', assign: role => this.ec2 = 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' ], roleName: 'role-rds', assign: role => this.rds = role } ]; constructor() { super(); } createResources(scope: cdk.Construct) { for (const resourceInfo of this.resources) { const role = this.createRole(scope, resourceInfo); resourceInfo.assign(role); } this.instanceProfileEc2 = new CfnInstanceProfile(scope, 'InstanceProfileEc2', { roles: [this.ec2.ref], instanceProfileName: this.ec2.roleName }); } private createRole(scope: cdk.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.roleName) }); return role; } }
例によってリソース情報を定義したのち、生成メソッドをループで呼び出しています。
CfnRole インスタンス生成時のパラメーターには assumeRolePolicyDocument
という必須プロパティがあり、このプロパティの型は PolicyDocument となっています。
ポリシーを記述する時の JSON のアレを表現するクラスです。
そして PolicyDocument クラスは statements
というプロパティを持ち、この配列にポリシーの要素 Statement を含める形になります(型は PolicyStatement)。ここに指定する Statement の内容が ResourceInfo で定義している policyStatementProps
に該当します。
また PolicyDocument クラスは fromJson()
という static メソッドを持っています。このメソッドを利用すれば PolicyDocument や PolicyStatement クラスのインスタンス生成処理も不要になるので、JSON が静的な場合はこちらを使うのもいいかもしれません。
const json = { "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }; const policyDocument = PolicyDocument.fromJson(json);
ベタ書きのほうがラクなときもありますよね。
JSON 部分を別ファイルに切り出して、使うときだけロードするというやり方だと JSON の管理もしやすそうです。AWS CDK ならではですね!
IAM ロールを作成したあとは CfnInstanceProfile クラスを利用し EC2 のインスタンスプロファイルも作成しています。
インスタンスプロファイルとは IAM ロールをインスタンスに渡すためのコンテナ だそうです。マネジメントコンソールで EC2 の IAM ロールを作成するときは IAM ロールと同じ名前でこのインスタンスプロファイルが作成されるため、普段は意識する必要はありません。しかし今回のように手動で IAM ロールを作成する場合はインスタンスプロファイルも作る必要があります。
メインのプログラムはこちら。
ハイライト部分を追記しました。
import * as cdk from '@aws-cdk/core'; import { Vpc } from './resource/vpc'; import { Subnet } from './resource/subnet'; import { InternetGateway } from './resource/internetGateway'; import { ElasticIp } from './resource/elasticIp'; import { NatGateway } from './resource/natGateway'; import { RouteTable } from './resource/routeTable'; import { NetworkAcl } from './resource/networkAcl'; import { IamRole } from './resource/iamRole'; export class DevioStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // VPC const vpc = new Vpc(); vpc.createResources(this); ~ 省略 ~ // IAM Role const iamRole = new IamRole(); iamRole.createResources(this); } }
今回はコンストラクタに渡すパラメーターはありません。つまり依存関係無し。(久々)
問題発生
この状態で Cfn のテンプレートを出力すると次のようになってしまいます。
RoleEc2: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: Fn::Join: - "" - - ec2. - Ref: AWS::URLSuffix Version: "2012-10-17" ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore - arn:aws:iam::aws:policy/AmazonRDSFullAccess RoleName: devio-stg-role-ec2
ハイライト部分が不思議なフォーマットになっています。
プログラムでは Principal の Service には ec2.amazonaws.com
を指定しているのですが、amazonaws.com の部分が AWS::URLSuffix
と解釈されてしまっています。
AWS::URLSuffix
ドメインのサフィックスを返します。サフィックスは、通常 amazonaws.com ですが、リージョンによって異なります。たとえば、中国 (北京) リージョンのサフィックスは amazonaws.com.cn です。
一方 RDS のロールに関しては同じ文字列が存在するのですが、同様の現象は発生しません。
Principal: Service: monitoring.rds.amazonaws.com
バグでしょうか?
少々厄介で、テストにも影響してきます。
余談ですが、ec2.amazonaws.com の文字列を作成している部分は Join 関数を使わずとも Sub 関数で次のように書くことができます。
Principal: Service: !Sub ec2.${AWS::URLSuffix}
テスト
テストコードはこちら。
import { expect, countResources, haveResource, haveResourceLike, anything } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import * as Devio from '../../lib/devio-stack'; test('IamRole', () => { const app = new cdk.App(); const stack = new Devio.DevioStack(app, 'DevioStack'); expect(stack).to(countResources('AWS::IAM::Role', 2)); expect(stack).to(haveResourceLike('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ Effect: 'Allow', Principal: { Service: anything() }, Action: 'sts:AssumeRole' }] }, ManagedPolicyArns: [ 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore', 'arn:aws:iam::aws:policy/AmazonRDSFullAccess' ], RoleName: 'undefined-undefined-role-ec2' })); expect(stack).to(haveResourceLike('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: 'undefined-undefined-role-rds' })); expect(stack).to(countResources('AWS::IAM::InstanceProfile', 1)); expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { Roles: anything(), InstanceProfileName: 'undefined-undefined-role-ec2' })); });
以下を確認しています。
- IAM ロールリソースが 2 つあること
- インスタンスプロファイルリソースが 1 つあること
- 各リソースのプロパティが正しいこと
上記の問題により EC2 のロールに関するテストでは Principal > Service の値を anything()
としています。(本当は 'ec2.amazonaws.com'
と書きたい!)
出力されるテンプレートは Fn::Join~ となっているため、'ec2.amazonaws.com' と書いてしまうと結果が一致せずにテストが失敗してしまいます。なんてこった
また今回は初めて haveResourceLike()
というメソッドを使用しています。
haveResource()
ではチェックする対象オブジェクトが更に子のオブジェクトを持っている場合、それは 完全一致でなければならない という制約があります。ここでは親のオブジェクトが AssumeRolePolicyDocument
, 子のオブジェクトがその中身に該当します。
PolicyDocument
クラスは自動的にバージョン情報(Version: "2012-10-17")をテンプレートに付与するようで、haveResource() メソッドを使用すると AssumeRolePolicyDocument の内容に予期しないキー Version が含まれている というメッセージと共にテストが失敗します。
実装していない部分をテストコードで書きたくはないので、haveResourceLike() メソッドを使用して完全一致ではなく部分一致のテストとすることでこの問題を回避します。
デプロイ
今回からデプロイ実行時に以下のような確認が発生します。
$ cdk deploy --no-path-metadata This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening). Please confirm you intend to make the following modifications: IAM Statement Changes ┌───┬────────────────┬────────┬────────────────┬──────────────────────────────────────┬───────────┐ │ │ Resource │ Effect │ Action │ Principal │ Condition │ ├───┼────────────────┼────────┼────────────────┼──────────────────────────────────────┼───────────┤ │ + │ ${RoleEc2.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.${AWS::URLSuffix} │ │ ├───┼────────────────┼────────┼────────────────┼──────────────────────────────────────┼───────────┤ │ + │ ${RoleRds.Arn} │ Allow │ sts:AssumeRole │ Service:monitoring.rds.amazonaws.com │ │ └───┴────────────────┴────────┴────────────────┴──────────────────────────────────────┴───────────┘ IAM Policy Changes ┌───┬────────────┬──────────────────────────────────────────────────────────────────────┐ │ │ Resource │ Managed Policy ARN │ ├───┼────────────┼──────────────────────────────────────────────────────────────────────┤ │ + │ ${RoleEc2} │ arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore │ │ + │ ${RoleEc2} │ arn:aws:iam::aws:policy/AmazonRDSFullAccess │ ├───┼────────────┼──────────────────────────────────────────────────────────────────────┤ │ + │ ${RoleRds} │ arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole │ └───┴────────────┴──────────────────────────────────────────────────────────────────────┘ (NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299) Do you wish to deploy these changes (y/n)?
CFn のテンプレートに IAM リソースが含まれる場合、意図しないリソースが作成されないように注意を促すものになります。
マネジメントコンソール上の以下の部分に該当します。
確認した上でデプロイを実行しましょう。
確認
マネジメントコンソール上でリソースを確認してみましょう。
IAM ロールが 2 つできています。
それぞれの内容も意図通りです。
EC2 のロールではロール名と同名のインスタンスプロファイルあり。
RDS のロールに関してはインスタンスプロファイルはありません。
CloudFormation 版
今回のコードを CFn で書くと以下のようになります。
RoleEc2: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: ec2.amazonaws.com Version: "2012-10-17" ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore - arn:aws:iam::aws:policy/AmazonRDSFullAccess RoleName: devio-stg-role-ec2 RoleRds: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: monitoring.rds.amazonaws.com Version: "2012-10-17" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole RoleName: devio-stg-role-rds InstanceProfileEc2: Type: AWS::IAM::InstanceProfile Properties: Roles: - Ref: RoleEc2 InstanceProfileName: devio-stg-role-ec2
GitHub
今回のソースコードは コチラ です。
おわりに
IAM まわりは奥が深いですね。
ハマらないように気をつけていきましょう。
ポリシー用の JSON を読み込んでオブジェクトに設定できることがわかったのは大きな学びでした。今後も活用していきたいと思います。
次回のお題は「セキュリティグループ
」です。
リンク
- class CfnRole (construct) | AWS CDK API Reference
- class CfnInstanceProfile (construct) | AWS CDK API Reference
- class PolicyDocument | AWS CDK API Reference
- class PolicyStatement | AWS CDK API Reference
- AWS::IAM::Role | AWS CloudFormation User Guide
- AWS::IAM::InstanceProfile | AWS CloudFormation User Guide
- IAM ロール | AWS IAM User Guide
- Amazon EC2 の IAM ロール | AWS EC2 User Guide
- Session Manager のアクセス許可を持つ IAM インスタンスプロファイルを確認または作成する | AWS SSM User Guide
- MySQL データベースエンジンを実行している DB インスタンスへの接続 | Amazon RDS User Guide
- 拡張モニタリングの設定と有効化 | Amazon RDS User Guide
- IAM JSON ポリシーの要素: Statement | AWS IAM User Guide
- 擬似パラメータ参照| AWS CloudFormation User Guide
- Fn::Sub | AWS CloudFormation User Guide
- AWS Identity and Access Management によるアクセスの制御 | AWS CloudFormation User Guide
- EC2にIAMRole情報を渡すインスタンスプロファイルを知っていますか? | DevelopersIO