AWS CDK を使用して検証環境の構築を試してみた

2023.06.19

はじめに

こんにちは、アノテーション・テクニカルサポートチームの及川です。

テクニカルサポートチームでは、お客様からいただいたご質問内容に回答するため、AWS 公式ドキュメントなどの情報について調査を行う他に、各自(手元)の AWS 検証環境を適宜準備して詳細に調査を行うことがあります。

AWS 検証環境を準備する際に、マネジメントコンソールから 1 つずつクリックして設定を行っても問題ないのですが、サクッと検証環境を準備(構築)することで、調査に重点を置くことができたり、お問い合わせいただいたお客様への回答に向けて円滑に対応を行うことができるなどのメリットが考えられます。

「サクッと検証環境を準備(構築)」する AWS サービスの一つとして、コードでクラウドインフラストラクチャを定義して、それを AWS CloudFormation を通じてデプロイするためのオープンソースのソフトウェア開発フレームワークである AWS CDK があります。

AWS クラウド開発のよくある質問

Q: AWS CDK とは何ですか?

AWS クラウド開発キット (AWS CDK) は、最新のプログラミング言語を使用してクラウドインフラストラクチャをコードとして定義し、それを AWS CloudFormation を通じてデプロイするためのオープンソースのソフトウェア開発フレームワークです。

今回は AWS CDK について学習に活用した資料と、学習した内容を元にした AWS 検証環境の構築について試してみたのでこちらについて簡単にご紹介します。

学習した資料一覧

概ね以下の資料を読み漁り学習を継続してきました。

AWS 公式資料

Udemy 資料

筆者のおすすめ

これから AWS CDK を始める方を対象とする場合は次の AWS 公式ワークショップ資料が下記観点からおすすめだと思います。

おすすめだと感じた観点

  • AWS CDK を手を動かしながら学べる(開発環境の準備~導入・ソースコード作成~デプロイ~リソース削除等)
  • AWS CDK を扱う上で、最低限抑えるべきである必要な TypeScript の基本について説明されている(注: 現在、AWS CDK は TypeScriptの他に、JavaScript、Python、Java、C#、Go をサポートしています。)
  • 必読と言っても過言ではない API Reference · AWS CDK について、どのように読み進めれば良いか等の読み方についても説明されている

学習したことを元に早速コードを書いてみた

学習方法でご紹介した資料などを参考に、自分がこれまでに検証した環境を題材にして、AWS CDK でリソースを構築しようと検討しました。

今回 AWS CDK で構築したリソースは、下記ブログの検証環境となります。

なお、Amazon FSx for NetApp ONTAP(以降 FSx for ONTAP)の AWS CDK でのコードの書き方については、上記で紹介させていただいた資料の他に下記 Github リポジトリのソースコードも参考にさせていただきました(コードの書き方など大変お世話になりました)。

上記の Github リポジトリや、適宜 API Reference · AWS CDK を参考にして記述したコード等を以降に記載します。

一つの .ts ファイルに全てのリソースを記述するパターン

  • 色々と試行錯誤を行って一通り下記の様に一つの .ts ファイルにまとめて記述しました
  • 下記の記述でも必要な各リソースが生成されますが、「視認性」と「可読性」の観点から、リソース毎にファイルを分けて、メインのファイルからリソースごとに定義したファイルの内容を読み込むようにするなどの構成を検討しました
  • 上記の方針より、コード修正(リファクタリング)を行いました(後述)
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import { CfnDynamicReference, CfnDynamicReferenceService } from 'aws-cdk-lib';
import * as fsx from 'aws-cdk-lib/aws-fsx';

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

    /** VPC */
    const vpc = new ec2.Vpc(this, 'Vpc', {
      cidr: '10.0.0.0/16',
      enableDnsHostnames: true,
      enableDnsSupport: true,
      natGateways: 0,
      maxAzs: 1,
      subnetConfiguration: [
        {
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
          cidrMask: 24,
        },
        {
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
          cidrMask: 24,
        },
      ],
    });

    /** SSM IAM Role */
    const ssmIamRole = new iam.Role(this, 'SSM IAM Role', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'AmazonSSMManagedInstanceCore',
        ),
      ],
    });

    /** EC2 インスタンス */
    const instance = new ec2.Instance(this, 'EC2Instance', {
      vpc,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.SMALL,
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      }),
      blockDevices: [
        {
          deviceName: '/dev/sda1',
          volume: ec2.BlockDeviceVolume.ebs(30, {
            volumeType: ec2.EbsDeviceVolumeType.GP3,
          }),
        },
      ],
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
      role: ssmIamRole,
    });

    /** SecurityGroup for FSx for ONTAP */
    const securityGroupFsxOntap = new ec2.SecurityGroup(
      this,
      'Security Group of FSx for ONTAP file system',
      {
        vpc,
      },
    );

    // Ref : https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/limit-access-security-groups.html
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.icmpPing(),
      'Pinging the instance',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(22),
      'SSH access to the IP address of the cluster management LIF or a node management LIF',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(111),
      'Remote procedure call for NFS',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(135),
      'Remote procedure call for CIFS',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(139),
      'NetBIOS service session for CIFS',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcpRange(161, 162),
      'Simple network management protocol (SNMP)',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(443),
      'ONTAP REST API access to the IP address of the cluster management LIF or an SVM management LIF',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(445),
      'Microsoft SMB/CIFS over TCP with NetBIOS framing',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(635),
      'NFS mount',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(749),
      'Kerberos',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(2049),
      'NFS server daemon',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(3260),
      'iSCSI access through the iSCSI data LIF',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(4045),
      'NFS lock daemon',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(4046),
      'Network status monitor for NFS',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(10000),
      'Network data management protocol (NDMP) and NetApp SnapMirror intercluster communication',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(11104),
      'Management of NetApp SnapMirror intercluster communication',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.tcp(11105),
      'SnapMirror data transfer using intercluster LIFs',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.udp(111),
      'Remote procedure call for NFS',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.udp(135),
      'Remote procedure call for CIFS',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.udp(137),
      'NetBIOS name resolution for CIFS',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.udp(139),
      'NetBIOS service session for CIFS',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.udpRange(161, 162),
      'Simple network management protocol (SNMP)',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.udp(635),
      'NFS mount',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.udp(2049),
      'NFS server daemon',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.udp(4045),
      'NFS lock daemon',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.udp(4046),
      'Network status monitor for NFS',
    );
    securityGroupFsxOntap.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.udp(4049),
      'NFS quota protocol',
    );

    /** SecretsManager for FSx for ONTAP */
    const fileSystemSecret = new secretsmanager.Secret(
      this,
      'Secret of FSx for ONTAP file system',
      {
        secretName: '/fsx-for-ontap/file-system/fsxadmin',
        generateSecretString: {
          generateStringKey: 'password',
          passwordLength: 32,
          requireEachIncludedType: true,
          secretStringTemplate: '{"username": "fsxadmin"}',
        },
      },
    );

    /** FileSystem - FSx for ONTAP */
    const cfnFileSystemFsxOntap = new fsx.CfnFileSystem(
      this,
      'CfnFileSystemFSxONTAP',
      {
        fileSystemType: 'ONTAP',
        subnetIds: vpc.selectSubnets({
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        }).subnetIds,
        securityGroupIds: [securityGroupFsxOntap.securityGroupId],
        storageCapacity: 1024,
        ontapConfiguration: {
          deploymentType: 'SINGLE_AZ_1',
          throughputCapacity: 128,
          fsxAdminPassword: new CfnDynamicReference(
            CfnDynamicReferenceService.SECRETS_MANAGER,
            `${fileSystemSecret.secretArn}:SecretString:password`,
          ).toString(),
        },
        storageType: 'SSD',
        tags: [
          {
            key: 'Name',
            value: 'CfnFileSystemFSxONTAP',
          },
        ],
      },
    );

    /** SVM - FSx for ONTAP */
    const svmName = 'SvmFSxForOntap';
    const svm = new fsx.CfnStorageVirtualMachine(this, 'SVM', {
      fileSystemId: cfnFileSystemFsxOntap.ref,
      name: svmName,
      rootVolumeSecurityStyle: 'MIXED',
      tags: [
        {
          key: 'Name',
          value: svmName,
        },
      ],
    });

    /** FSx for ONTAP Volume */
    const volumeName = 'VolumeFSxForOntap';
    const junctionPath = '/vol1';
    new fsx.CfnVolume(this, 'Volume', {
      name: volumeName,
      ontapConfiguration: {
        junctionPath,
        sizeInMegabytes: '102400',
        storageVirtualMachineId: svm.ref,
        storageEfficiencyEnabled: 'true',
        securityStyle: 'UNIX',
      },
      tags: [
        {
          key: 'Name',
          value: volumeName,
        },
      ],
      volumeType: 'ONTAP',
    });
  }
}

コード修正(リファクタリング)してみた

修正後のディレクトリのツリー構造

ディレクトリのツリー構造について抜粋して載せます。

bin
└── cdk_test-fsx_ontap.ts
lib
├── cdk_test-fsx_ontap-stack.ts # メインの .ts ファイル
└── resources # 作成するリソース毎にファイルを分けて作成
    ├── ec2Instance.ts
    ├── fileSystemFsxOntap.ts
    ├── secretsManagerFsxOntap.ts
    ├── securityGroupFsxOntap.ts
    ├── ssmIamRoleEc2Instance.ts
    ├── svmFsxOntap.ts
    ├── volumeFsxOntap.ts
    └── vpc.ts

リソース毎に定義した .ts ファイルをメインの .ts ファイルに読み込んで記述するパターン

メインの .ts ファイルだけ載せていますが、最終的には下記の通りとなりました。

(1つのファイルにまとめて記述する先程のコードよりかは、少しスッキリしました。)

本ソースコードの詳細については、こちらのリンクからご参照いただければ幸いです。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { createVpc } from './resources/vpc';
import { createEc2 } from './resources/ec2Instance';
import { createIam } from './resources/ssmIamRoleEc2Instance';
import { createSecurityGroupFsxOntap } from './resources/securityGroupFsxOntap';
import { createSecretsManagerFsxOntap } from './resources/secretsManagerFsxOntap';
import { createCfnFileSystem } from './resources/fileSystemFsxOntap';
import { createCfnStorageVirtualMachine } from './resources/svmFsxOntap';
import { createCfnVolume } from './resources/volumeFsxOntap';

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

    /** VPC */
    const vpc = createVpc(this);

    /** SSM IAM Role for EC2 Instance */
    const ssmIamRole = createIam(this);

    /** EC2 インスタンス */
    const ec2Instance = createEc2(this, vpc, ssmIamRole);

    /** SecurityGroup for FSx for ONTAP */
    const securityGroupFsxOntap = createSecurityGroupFsxOntap(this, vpc);

    /** SecretsManager for FSx for ONTAP */
    const secretsManagerFsxOntap = createSecretsManagerFsxOntap(this);

    /** FileSystem - FSx for ONTAP */
    const cfnFileSystemFsxOntap = createCfnFileSystem(
      this,
      vpc,
      securityGroupFsxOntap,
      secretsManagerFsxOntap,
    );

    /** SVM - FSx for ONTAP */
    const cfnStorageVirtualMachineFsxOntap = createCfnStorageVirtualMachine(
      this,
      cfnFileSystemFsxOntap,
    );

    /** Volume - FSx for ONTAP */
    const CfnVolumeFsxOntap = createCfnVolume(
      this,
      cfnStorageVirtualMachineFsxOntap,
    );
  }
}

動作確認

作成されたリソース

後述の Cloud9 環境での各コマンドの実行により下記のとおりリソースが作成されました。

Cloud9 環境でのコマンド実行

以下は Cloud9 環境にて git clone してから cdk deploy まで実行した例を示しています。

cm-xxxxx.xxxxx:~/environment $ git clone https://github.com/masakioikawa0503/cdk-test-FsxOntap.git

Cloning into 'cdk-test-FsxOntap'...
remote: Enumerating objects: 32, done.
remote: Counting objects: 100% (32/32), done.
remote: Compressing objects: 100% (28/28), done.
remote: Total 32 (delta 5), reused 30 (delta 3), pack-reused 0
Receiving objects: 100% (32/32), 76.00 KiB | 9.50 MiB/s, done.
Resolving deltas: 100% (5/5), done.

cm-xxxxx.xxxxx:~/environment $ cd cdk-test-FsxOntap/
cm-xxxxx.xxxxx:~/environment/cdk-test-FsxOntap (main) $ npm install 

(中略)

cm-xxxxx.xxxxx:~/environment/cdk-test-FsxOntap (main) $ cdk diff

(中略)

cm-xxxxx.xxxxx:~/environment/cdk-test-FsxOntap (main) $ cdk deploy

(中略)

Do you wish to deploy these changes (y/n)? y
CdkTestFsxOntapStack: deploying... [1/1]
CdkTestFsxOntapStack: creating CloudFormation changeset...

 ✅  CdkTestFsxOntapStack

✨  Deployment time: 2194.67s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxxxx:stack/CdkTestFsxOntapStack/b5198da0-0b3f-11ee-abf2-0672535c7cbb

✨  Total time: 2203.06s

以上の結果、指定した各リソースは無事に作成されており、題材にしたブログの検証も行えました。

クリーンアップ

一通り検証が済んだら、予期しない請求を防ぐために、下記コマンドで必ず CDK スタックを削除してください。

cm-xxxxx.xxxxx:~/environment/cdk-test-FsxOntap (main) $ cdk destroy

Are you sure you want to delete: CdkTestFsxOntapStack (y/n)? y

続いて、[Amazon FSx] -> [バックアップ] より、確認できるバックアップファイルも使用しないのであれば削除してください。

まとめ

今回の経験を通して、意図したリソースを AWS CDK により構築するためのソースコードをどのように記載すれば良いのか、どのようにAPI Reference · AWS CDK のドキュメントを読み進めれば良いのか等の勘所を少しだけ押さえることができました。

一方で、AWS サービスの全てのリソースを AWS CDK により構築して試したわけではないものの、本記事のようにいろいろ試している中でますます学習意欲が向上してきたため、引き続きいろいろなパターンの構築を試していきたいと思います。

この記事が少しでも誰かのお役にたてば幸いです。

参考資料

アノテーション株式会社について

アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社 WEB サイトをご覧ください。