組織は、ベストプラクティスに従ってアプリケーションを実行できるよう、クラウドインフラストラクチャに対してコンプライアンスルールを適用しています。AWS Config を活用して、内部のガイドラインに従って構成された設定に対する、全体的なコンプライアンス状況を確認します。この確認は、AWS アカウントでクラウドリソースを作成した後で行なわれます。
この記事では、AWS アカウントでクラウドリソースを作成する前に、AWS CDK Aspects を利用してベストプラクティスに従っているかどうかをチェックしたり、従うための調整を行なう方法を紹介します。
AWS Cloud Development Kit (AWS CDK) は、TypeScript、Python、Java、.NET などの馴染み深いプログラミング言語を使ってクラウドアプリケーションリソースを定義できるオープンソースのソフトウェア開発フレームワークです。インフラストラクチャを定義するためにプログラミング言語の表現力を活用することで開発プロセスを加速させ、開発者体験を向上させることができます。
AWS Config は、AWS リソースの構成を評価・監査できるサービスです。AWS リソースの構成を継続的に監視・記録し、記録された構成が望ましい状態かどうかの評価を自動化できます。非準拠リソースが見つかった場合は、そのリソースを自動または手動で望ましい状態に修正できます。
AWS Config は、お客様がコンプライアンスに準拠した状態で AWS 上でワークロードを実行できるように支援しますが、前もって非準拠リソースを検知し、コンプライアンスに準拠したリソースのみをプロビジョニングしたいと考えるお客様も存在します。リソースの構成にはお客様にとって非常に重要なものもあるため、最初からコンプライアンスに準拠していない限りリソースをプロビジョニングしないと考える場合があります。たとえば次のような構成です。
- Amazon S3 バケットは、パブリックアクセスを許可せずに作成する必要がある
- Amazon S3 バケットの暗号化を有効にする必要がある
- データベース削除保護を有効にする必要がある
CDK Aspects
CDK Aspects は、特定のスコープ内のすべての construct に対して共通の操作を適用する方法です。Aspect は、construct の状態について何らかの検証 (すべてのバケットが暗号化されていることを保証する、など) を行なったり、タグを追加するなど construct に対する修正を行なうことができます。
Aspect は、以下に示す IAspect
インターフェイスを実装するクラスです。Aspect は Visitor パターンを採用しているため、既存のオブジェクト構造を修正することなく新しい操作を追加できます。オブジェクト指向プログラミングやソフトウェア工学において、Visitor デザインパターンとは、オブジェクト構造からその上で動作するアルゴリズムを分離する手法のことです。
interface IAspect {
visit(node: IConstruct): void ;
}
cdk deploy
を呼び出すと、AWS CDK アプリケーションは以下のようにライフサイクルフェーズが遷移します。これらのフェーズは下の図でも表現されています。CDK アプリケーションのライフサイクルについての詳細については、こちら のページを参照してください。
- Construction
- Preparation
- Validation
- Synthesis
- Deployment
CDK Aspect は Preparation フェーズで関係してきます。この Preparation フェーズでは、construct の最終的な状態を設定するための最後の変更ラウンドが実行されます。Preparation フェーズは自動的に実行されます。すべての construct は、内部に Preparation フェーズ中に呼び出され適用される Aspect のリストを持っています。次のメソッドを呼び出すことで、指定したスコープにカスタム Aspect を追加できます。
Aspects.of(myConstruct).add(new SomeAspect(...));
上記のメソッドを呼び出すと、construct は内部の Aspect リストに カスタム Aspect を追加します。CDK アプリケーションが Preparation フェーズを処理するとき、AWS CDK は 指定した construct およびその配下の construct について上位から下位の順で、Aspect オブジェクトの visit
メソッドを呼び出します。visit
メソッドは、construct に対して任意の変更を適用できます。
CDK Aspects を利用してリソース構成のコンプライアンスをチェックする方法・準拠させる方法
次のセクションでは、クラウド リソースをプロビジョニングする際のいくつかの一般的なユースケースにおいて、CDK Aspects を実装する方法をご紹介します。CDK Aspects は拡張可能です。追加のルールを実装することで、ユースケースに合わせて拡張できます。
以下のコードでは、Aspects を利用してベストプラクティスに照らし合わせて検証するクラウドリソースを作成しています。
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
export class AwsCdkAspectsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props ?: cdk.StackProps) {
super(scope, id, props);
//Create a VPC with 3 availability zones
const vpc = new ec2.Vpc(this, 'MyVpc', {
maxAzs: 3,
});
//Create a security group
const sg = new ec2.SecurityGroup(this, 'mySG', {
vpc: vpc,
allowAllOutbound: true
})
//Add ingress rule for SSH from the public internet
sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'SSH access from anywhere')
//Launch an EC2 instance in private subnet
const instance = new ec2.Instance(this, 'MyInstance', {
vpc: vpc,
machineImage: ec2.MachineImage.latestAmazonLinux2(),
instanceType: new ec2.InstanceType('t3.small'),
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
securityGroup: sg
})
//Launch MySQL rds database instance in private subnet
const database = new rds.DatabaseInstance(this, 'MyDatabase', {
engine: rds.DatabaseInstanceEngine.mysql({
version: rds.MysqlEngineVersion.VER_8_0
}),
vpc: vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
deletionProtection: false
})
//Create an s3 bucket
const bucket = new s3.Bucket(this, 'MyBucket')
}
}
このセクションでは、Aspects を利用して以下のベストプラクティスに照らし合わせてリソースを検証するユースケースとコードを示しています。
- VPC の CIDR 範囲は特定の CIDR IP から始まる必要がある
- セキュリティグループには public ingress ルールを設定してはいけない
- EC2 インスタンスは許可されたインスタンスタイプのみが利用可能である
- S3 バケットの暗号化を有効にする必要がある
- S3 バケットのバージョン管理を有効にする必要がある
- RDS インスタンスでは削除保護を有効にする必要がある
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Stack, IAspect, Annotations, Tokenization } from 'aws-cdk-lib';
import { IConstruct } from 'constructs';
//Verify VPC CIDR range
export class VPCCIDRAspect implements IAspect {
public visit(node: IConstruct) {
if (node instanceof ec2.CfnVPC && node.cidrBlock) {
if (! node.cidrBlock.startsWith('192.168.')) {
Annotations.of(node).addError('VPC does not use standard CIDR range starting with "192.168."');
}
}
}
}
//Verify public ingress rule of security group
export class SecurityGroupNoPublicIngressAspect implements IAspect {
public visit(node: IConstruct) {
if (node instanceof ec2.CfnSecurityGroup) {
checkRules(Stack.of(node).resolve(node.securityGroupIngress));
}
function checkRules (rules : Array<ec2.CfnSecurityGroup.IngressProperty>) {
if(rules) {
for (const rule of rules.values()) {
if (! Tokenization.isResolvable(rule) && (rule.cidrIp == '0.0.0.0/0' || rule.cidrIp == '::/0')) {
Annotations.of(node).addError('Security Group allows ingress from public internet.');
}
}
}
}
}
}
//Verify instance type of EC2 instance
export class EC2ApprovedITAspect implements IAspect {
public visit(node: IConstruct) {
const its = ['x', 'z', 'p', 'g', 'i', 't']
if (node instanceof ec2.CfnInstance && node.instanceType) {
const instanceType = node.instanceType ;
if (its.some(its => instanceType.startsWith(its))) {
Annotations.of(node).addError('EC2 Instance is not using approved instance type.');
}
}
}
}
//Verify that bucket versioning is enabled
export class BucketVersioningAspect implements IAspect {
public visit(node: IConstruct): void {
if (node instanceof s3.CfnBucket) {
if (! node.versioningConfiguration
|| (! Tokenization.isResolvable(node.versioningConfiguration)
&& node.versioningConfiguration.status !== 'Enabled')) {
Annotations.of(node).addError('S3 bucket versioning is not enabled.');
}
}
}
}
//Verify that bucket has server-side encryption enabled
export class BucketEncryptionAspect implements IAspect {
public visit(node: IConstruct): void {
if (node instanceof s3.CfnBucket) {
if (! node.bucketEncryption) {
Annotations.of(node).addError('S3 bucket encryption is not enabled.');
}
}
}
}
//Verify that DB instance deletion protection is enabled
export class RDSDeletionProtectionAspect implements IAspect {
public visit(node: IConstruct) {
if (node instanceof rds.CfnDBInstance) {
if (! node.deletionProtection) {
Annotations.of(node).addError('RDS DB instance deletion protection is not enabled.');
}
}
}
}
Aspect を作成したら、特定のスコープにそれらを追加します。スコープとして App、Stack、Construct が指定できます。以下の例では、すべての Aspect を Stack のスコープに追加します。
import * as cdk from 'aws-cdk-lib';
import { Aspects } from 'aws-cdk-lib';
import { AwsCdkAspectsStack } from '../lib/cdk-aspect-stack';
import { BucketEncryptionAspect, BucketVersioningAspect,
EC2ApprovedITAspect, RDSDeletionProtectionAspect,
SecurityGroupNoPublicIngressAspect, VPCCIDRAspect } from '../lib/cdk-aspect-rules';
const app = new cdk.App();
const stack = new AwsCdkAspectsStack(app, 'MyApplicationStack');
Aspects.of(stack).add(new VPCCIDRAspect());
Aspects.of(stack).add(new SecurityGroupNoPublicIngressAspect());
Aspects.of(stack).add(new EC2ApprovedITAspect());
Aspects.of(stack).add(new RDSDeletionProtectionAspect());
Aspects.of(stack).add(new BucketEncryptionAspect());
Aspects.of(stack).add(new BucketVersioningAspect());
app.synth();
上記のコードで Aspect が追加された状態で cdk deploy
を呼び出すと、次のような内容が出力されます。エラーを解決しリソースをルールに準拠させないかぎり、デプロイは実行されません。
コンプライアンスチェックだけではなく、Aspects を利用してリソースに共通の変更を加えることもできます。たとえば、タグに対応したすべてのリソースに必須のタグを設定できます。こうした機能を実現するために CDK Aspects を実装している例として Tags があります。
以下のようなコードを実装することで、Construct のスコープ内のすべてのリソースとその配下のリソース (タグに対応したリソースのみ) に、タグを追加したり削除したりできます。
Tags.of(myConstruct).add('key', 'value');
Tags.of(myConstruct).remove('key');
以下は、Stack のスコープ内で作成されたすべてのリソースに Department タグを追加する例です。
Tags.of(stack).add('Department', 'Finance');
開発者の皆さんには、Aspect の利用してインフラストラクチャリソースに動的な変更を加えることは避けることをお勧めします。そのような実装をすると、Synthesis フェーズで Stack に意図しない変更が発生してしまう可能性があります。IaC としての決定性が弱まり、CDK コードが唯一の信頼できる情報源ではなくなってしまいます。
(翻訳者補足: 外部から取得したパラメータを設定値として利用するなどの動的な変更をリソースに加えることは可能なかぎり避けて、決定論的な記述を心がけましょう。これは IaC の一般的なベストプラクティスです。Aspects を利用してリソースに変更を加える場合は、万が一意図しない変更が発生してしまったときの影響範囲が大きくなってしまう可能性があるため、とくにこの点を注意しましょう。)
追加の推奨事項
CDK Aspects は、開発者が選択したプログラミング言語を使用して、インフラストラクチャの構成をベストプラクティスに従うようにしたり、チェックしたりするための方法です。AWS CloudFormation Guard (cfn-guard) は、コンプライアンス管理者がシンプルな policy-as-code 言語でポリシーを記述し、ベストプラクティスを強制できるようにします。Aspects は CloudFormation テンプレートの生成前の Preparation フェーズで適用されますが、cfn-guard は CloudFormation テンプレートの生成後、Deployment フェーズの前に適用されます。開発者は CI/CD パイプラインの一部として Aspects または cfn-guard、あるいはその両方を使って非準拠リソースのデプロイを停止できます。ただし、非準拠リソースのデプロイを防ぎコンプライアンスを「強制する」意図が強い場合は、 CloudFormation Guard が適していると言えるでしょう。
cdk-nag は、AWS CDK Aspects を利用して多くのルールを実装し、パッケージとして提供するオープンソースプロジェクトです。たとえば AWS Solutions、HIPPA、NIST 800-53 などのパッケージがあります。このプロジェクトを利用すると、すでにパッケージ化されているルールを使って、CDK アプリケーションがベストプラクティスに従っているかどうかをチェックできます。評価したくないルールがある場合は、パッケージの中の一部のルールの評価を抑制できます。
まとめ
インフラストラクチャの構築に AWS CDK を利用している場合は、リソースが作成される前にベストプラクティスに従うように、Aspects を使い始めることができます。CloudFormation テンプレートでインフラストラクチャを管理している場合は、この ブログ を読めば、CloudFormation テンプレートを AWS CDK に移行する方法がわかります。移行した後は、CDK Aspects を利用して、リソースがベストプラクティスに従っているかを、リソースの作成前に評価できます。
(翻訳者補足: 2024 年 4 月現在では、IaC 管理外のリソースもしくはデプロイ済みの CloudFormation スタックを CDK アプリケーションに移行できる CDK Migrate コマンドが利用できます。)
著者について
本記事は 2021/10/07 に投稿された Align with best practices while creating infrastructure using CDK Aspects を翻訳したものです。翻訳は Solutions Architect : 国兼 周平 (Shuhei Kunikane) が担当しました。