WindowsでAWS CDK(C#)の開発環境を整えてみた

しばたです。

先月TypeScriptとPythonでの利用がGAしたAWS Cloud Development Kit (CDK)ですが、Developers.IOで静かな?ブームとなっている様で多くの社員が記事を書いています。
このブームに乗る形で私もWindows環境で言語にC#を選び試してみました。

内容的には先日梶原が書いたこちらの記事の影響を受けており、そこそこ内容が被るかもしれません。

WindowsでAWS CDK(Java)の開発環境を整えてみた

注意事項

本日(2019年8月22日)時点でTypeScriptとPythonでの利用はGAしてますが、C#での利用はDeveloper Previewです。 本記事の内容は将来的に変更される可能性がある点はご留意ください。

参考資料

参考資料として.NET(C#)のCDKが登場した際のAWS Developers Blogの記事とGitHubリポジトリのドキュメントを使用しました。

AWS Developers Blogの記事について、当時はC#の言語サポートがdotnetという形で記載されていますが、現在はcsharpと改善されていますので適宜差し替えてご覧ください。

# 当時の記述 
cdk init app --language dotnet

# 現在の記述 : C# は csharp に改善
cdk init app --language csharp

また、基本的な手順としては以下の公式ドキュメントが役に立ちます。

環境構築

今回は私の開発PC(64bit版 Windows 10 May 2019 Update (1903))を検証環境とし、C#でCDKを利用するに際して以下のツールをインストールしています。

  • Node.js
  • AWS CDK CLI
  • .NET Core SDK

また、開発環境としてGit + Visual Studio Code + C#拡張を使用しています。
本記事では上述のツールのインストール手順は紹介しますがVisual Studio Codeの設定については触れません。Gitを使いC#のコードをよしなにコンパイルできる環境がある前提で話を進めていきます。

Node.js

CDKの基本となるcdkコマンドはNode.js製ですのでWindowsにNode.jsをインストールします。
Node.js 8.11以上のバージョンである必要があります。
今回は執筆時点の最新バージョンであるNode.js 12.8.1をインストールしました。

PowerShellコンソールを開き、以下のコマンドを実行すると既定の設定でサイレントインストールできます。

# MSIインストーラーをダウンロードしてサイレントインストール
$msiPath = Join-Path $env:TEMP 'node-v12.8.1-x64.msi'
Invoke-WebRequest -Uri 'https://nodejs.org/dist/v12.8.1/node-v12.8.1-x64.msi' -OutFile $msiPath
# 全てデフォルト設定のままサイレントインストール
msiexec.exe /i $msiPath /passive

インストール後コンソールを再起動するとPATH環境変数が更新され、nodeコマンドやnpmコマンドが実行可能になります。

C:\> node --version
v12.8.1
C:\> npm --version
6.10.2

AWS CDK CLI

cdkコマンドを利用するためにAWS CDK CLIをnpmからインストールします。
PowerShellコンソールから以下のコマンドを実行します。

# npmからインストール
npm i -g aws-cdk

今回はVer.1.5.0がインストールされました。

C:\> cdk --version
1.5.0 (build c020efa)

.NET Core SDK

C#のコードをコンパイルするために.NET Core SDKをインストールします。
要件としては.NET Core 2.0以降である必要があります。
今回は執筆時点で最新のインストーラー版SDKをインストールしました。

PowerShellコンソールを開き、以下のコマンドを実行するとサイレントインストールできます。
インストーラー版SDKはマシン全体に設定が反映されるため要管理者権限です。

# 現在最新の.NET Core SDKのバージョンを取得
$commitHash, $version = -split (Invoke-RestMethod -Uri https://dotnetcli.azureedge.net/dotnet/Sdk/Current/latest.version)
# MSIインストーラーをダウンロードしてサイレントインストール
$msiPath = Join-Path $env:TEMP "dotnet-sdk-$version-win-x64.exe"
Invoke-WebRequest -Uri "https://dotnetcli.azureedge.net/dotnet/Sdk/$version/dotnet-sdk-$version-win-x64.exe" -OutFile $msiPath
& $msiPath /install /passive

今回はVer.2.2.301がインストールされました。

C:\> dotnet --version
2.2.301

AWS認証情報

CDKでは認証情報にAWS CLIと同じ~/.aws/configおよび~/.aws/credentialsファイル(Windowsの場合は%USERPROFILE%\.aws\config%USERPROFILE%\.aws\credentials)を使用します。
設定すべき内容はAWS CLIと同様で、プロファイル設定をするとcdkコマンドの--profileパラメーターで使用することができます。

# ~/.aws/config 設定例
[profile test-profile]
region=ap-northeast-1

# ~/.aws/credentials 設定例
[test-profile]
aws_access_key_id=AKIAI44QH8DHBEXAMPLE
aws_secret_access_key=je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY

注意事項

CDKのドキュメントに例示されている様に、アクセスキーの機密情報も~/.aws/configにまとめて記述することもできますが、この場合は空ファイルで良いので~/.aws/credentialsファイルを作成しておいてください。
~/.aws/credentialsが無いとcdkコマンド実行時に

AWS region must be configured either when you configure your CDK stack or through the environment

といったエラーを吐いてしまいます。

以上で環境構築は完了です。

やってみた

ここから実際にCDKを使ってみます。

プロジェクト作成

最初にCDKのプロジェクトを作成します。
任意のディレクトリ(本記事ではC:\temp\cdksample)に移動しcdk init appコマンドを実行すると空のプロジェクトを作成できます。
言語はC#を使いたいので--language csharpも合わせて指定します。

cdk init app --language csharp

画面表示が一部文字化けし改行コードの自動変換が入るもののディレクトリ配下に必要なファイルは生成されます。

カレントディレクトリにcdk.jsonが生成され、C#ソース一式はsrcディレクトリ配下に生成されます。
treeコマンドの結果以下の様になります。

C:.
│  .gitignore
│  add-project.hook.d.ts
│  cdk.json
│  README.md
│
└─src
    │  Cdksample.sln
    │
    └─Cdksample
            Cdksample.csproj
            CdksampleStack.cs
            Program.cs

ソースコードについて

生成されたソースはエントリポイントであるProgram.csと空のスタックであるCdksampleStack.csがあり、それぞれ以下の様になっています。

  • Program.cs
using Amazon.CDK;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Cdksample
{
    class Program
    {
        static void Main(string[] args)
        {
            var app = new App(null);
            new CdksampleStack(app, "CdksampleStack", new StackProps());
            app.Synth();
        }
    }
}
  • CdksampleStack.cs
using Amazon.CDK;

namespace Cdksample
{
    public class CdksampleStack : Stack
    {
        public CdksampleStack(Construct parent, string id, IStackProps props) : base(parent, id, props)
        {
            // The code that defines your stack goes here
        }
    }
}

C#のCDKにはまだ公式のサンプルソースが公開されていないため、本記事では単純にVPCを1つだけ定義する様にCdksampleStack.csを修正します。

  • 修正した CdksampleStack.cs
using Amazon.CDK;
using Amazon.CDK.AWS.EC2;

namespace Cdksample
{
    public class CdksampleStack : Stack
    {
        public CdksampleStack(Construct parent, string id, IStackProps props) : base(parent, id, props)
        {
            // Create a VPC
            var vpc = new Vpc(this, "SampleVPC", new VpcProps {
                Cidr = "172.18.0.0/16"
            });
        }
    }
}

ソースのビルド + CloudFormation Templateの生成(synth)

ソースを修正したらビルドしてアセンブリを生成します。
こちらは普通のC#のビルドですのでdotnet buildコマンドで行います。
ソリューションファイルCdksample.sln.\src\フォルダにありますので実際に実行するコマンドは以下の様になります。

# ソースのビルドは普通のC#のビルドと同じ
# ソリューションファイルが src フォルダにあるので dotnet build src コマンドを実施
dotnet build src

エラー無くビルドが完了すればOKです。
続けて、cdk synthコマンドを実行することでC#のアセンブリからCloudFormatoin Templateを生成します。

# cdk synthコマンドでCloudFormatoin Templateを生成
cdk synth

今回の例で出力されるテンプレートの全容は以下となります。

テンプレート全体を表示
Resources:
  SampleVPC676AFAA6:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 172.18.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/Resource
  SampleVPCPublicSubnet1SubnetFF189553:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 172.18.0.0/18
      VpcId:
        Ref: SampleVPC676AFAA6
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC/PublicSubnet1
        - Key: aws-cdk:subnet-name
          Value: Public
        - Key: aws-cdk:subnet-type
          Value: Public
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet1/Subnet
  SampleVPCPublicSubnet1RouteTableD74013E6:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: SampleVPC676AFAA6
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC/PublicSubnet1
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet1/RouteTable
  SampleVPCPublicSubnet1RouteTableAssociation0E1E38CA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: SampleVPCPublicSubnet1RouteTableD74013E6
      SubnetId:
        Ref: SampleVPCPublicSubnet1SubnetFF189553
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet1/RouteTableAssociation
  SampleVPCPublicSubnet1DefaultRouteD62243DA:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: SampleVPCPublicSubnet1RouteTableD74013E6
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: SampleVPCIGW11D9AA06
    DependsOn:
      - SampleVPCVPCGW9A31D44E
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet1/DefaultRoute
  SampleVPCPublicSubnet1EIP7C23471C:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet1/EIP
  SampleVPCPublicSubnet1NATGatewayB71B54D1:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - SampleVPCPublicSubnet1EIP7C23471C
          - AllocationId
      SubnetId:
        Ref: SampleVPCPublicSubnet1SubnetFF189553
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC/PublicSubnet1
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet1/NATGateway
  SampleVPCPublicSubnet2SubnetAF202FA8:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 172.18.64.0/18
      VpcId:
        Ref: SampleVPC676AFAA6
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC/PublicSubnet2
        - Key: aws-cdk:subnet-name
          Value: Public
        - Key: aws-cdk:subnet-type
          Value: Public
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet2/Subnet
  SampleVPCPublicSubnet2RouteTable63C2EDC5:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: SampleVPC676AFAA6
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC/PublicSubnet2
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet2/RouteTable
  SampleVPCPublicSubnet2RouteTableAssociation9C39EBDB:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: SampleVPCPublicSubnet2RouteTable63C2EDC5
      SubnetId:
        Ref: SampleVPCPublicSubnet2SubnetAF202FA8
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet2/RouteTableAssociation
  SampleVPCPublicSubnet2DefaultRoute45289F69:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: SampleVPCPublicSubnet2RouteTable63C2EDC5
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: SampleVPCIGW11D9AA06
    DependsOn:
      - SampleVPCVPCGW9A31D44E
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet2/DefaultRoute
  SampleVPCPublicSubnet2EIPA80228F3:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet2/EIP
  SampleVPCPublicSubnet2NATGateway2775C8CB:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - SampleVPCPublicSubnet2EIPA80228F3
          - AllocationId
      SubnetId:
        Ref: SampleVPCPublicSubnet2SubnetAF202FA8
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC/PublicSubnet2
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PublicSubnet2/NATGateway
  SampleVPCPrivateSubnet1SubnetB2AF079C:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 172.18.128.0/18
      VpcId:
        Ref: SampleVPC676AFAA6
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC/PrivateSubnet1
        - Key: aws-cdk:subnet-name
          Value: Private
        - Key: aws-cdk:subnet-type
          Value: Private
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PrivateSubnet1/Subnet
  SampleVPCPrivateSubnet1RouteTable11FF53B5:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: SampleVPC676AFAA6
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC/PrivateSubnet1
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PrivateSubnet1/RouteTable
  SampleVPCPrivateSubnet1RouteTableAssociation42FCDD78:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: SampleVPCPrivateSubnet1RouteTable11FF53B5
      SubnetId:
        Ref: SampleVPCPrivateSubnet1SubnetB2AF079C
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PrivateSubnet1/RouteTableAssociation
  SampleVPCPrivateSubnet1DefaultRoute8F7F8183:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: SampleVPCPrivateSubnet1RouteTable11FF53B5
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: SampleVPCPublicSubnet1NATGatewayB71B54D1
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PrivateSubnet1/DefaultRoute
  SampleVPCPrivateSubnet2Subnet1A9D7D61:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 172.18.192.0/18
      VpcId:
        Ref: SampleVPC676AFAA6
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC/PrivateSubnet2
        - Key: aws-cdk:subnet-name
          Value: Private
        - Key: aws-cdk:subnet-type
          Value: Private
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PrivateSubnet2/Subnet
  SampleVPCPrivateSubnet2RouteTable8996254C:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: SampleVPC676AFAA6
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC/PrivateSubnet2
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PrivateSubnet2/RouteTable
  SampleVPCPrivateSubnet2RouteTableAssociation13E6A7CE:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: SampleVPCPrivateSubnet2RouteTable8996254C
      SubnetId:
        Ref: SampleVPCPrivateSubnet2Subnet1A9D7D61
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PrivateSubnet2/RouteTableAssociation
  SampleVPCPrivateSubnet2DefaultRouteE89BC1AA:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: SampleVPCPrivateSubnet2RouteTable8996254C
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: SampleVPCPublicSubnet2NATGateway2775C8CB
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/PrivateSubnet2/DefaultRoute
  SampleVPCIGW11D9AA06:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: CdksampleStack/SampleVPC
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/IGW
  SampleVPCVPCGW9A31D44E:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId:
        Ref: SampleVPC676AFAA6
      InternetGatewayId:
        Ref: SampleVPCIGW11D9AA06
    Metadata:
      aws:cdk:path: CdksampleStack/SampleVPC/VPCGW
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Modules: aws-cdk=1.5.0,@aws-cdk/aws-cloudwatch=1.5.0,@aws-cdk/aws-ec2=1.5.0,@aws-cdk/aws-iam=1.5.0,@aws-cdk/aws-ssm=1.5.0,@aws-cdk/core=1.5.0,@aws-cdk/cx-api=1.5.0,@aws-cdk/region-info=1.5.0,jsii-runtime=DotNet/4.0.30319.42000

ここで余談なのですが、VCPの定義だけを明示した場合は暗黙的に2つのPublicサブネット、2つのPrivateサブネット、インターネットゲートウェイ(IGW)、NAT Gateway、ルートテーブルなどが生成されます。
いわゆるベストプラクティスに沿った高可用性ネットワークをよしなに作ってくれる感じです。

(上記テンプレートのネットワーク構成図)

もちろんサブネット等のリソースを明示した場合は暗黙で作成されるものが減りますが、例えばPublicサブネットを記述した場合はIGWをセットで、Privateサブネットを記述した場合はNAT Gatewayをセットで生成するなど、ネットワーク構成をできる限り意識させないつくりになっている様です。

デプロイ

生成されるCloudFormation Templateに問題が無さそうであれば、cdk deployコマンドを使い実環境に反映させます。
--profileパラメーターで認証に使用するプロファイルを指定します。

# CloudFormationスタックを作成し実環境に反映
cdk deploy --profile [your-profile]

(進捗状況の表示がずれてますが処理に問題はありません)

エラー無くコマンドが完了すればOKです。
マネジメントコンソールを確認すると以下の様にCloudFormation Stackが作成され、各種リソースが生成されています。

(キリがないのでこのくらいで...)

その他コマンド

この他にcdk diffコマンドでデプロイされた環境とローカル環境の差分を取得できます。

# デプロイされた環境とローカル環境の差分を取得
cdk diff --profile [your-profile]

cdk destroyコマンドでCloudFormatinスタックの削除を行うことができます。

# CloudFormationスタックの削除
cdk destroy --profile [your-profile]

所感

ざっとこんな感じです。

今回はC#で試してみましたが、CDKにはライブラリに対するデザインガイドラインが存在しており言語を問わずリソース定義はコンストラクタで行うのが基本となっています。
C#ではオブジェクト初期化子といった記法があるので多少は楽ができるものの、純粋にAWSリソースを定義する言語としてはそこまで使い勝手が良いと感じなかったのが率直な気持ちです。
私個人としてはリソース定義は宣言的な言語で書く方が好みなのでYAMLで直接CloudFormation Templateを書く方が楽だと感じました。
C#のCDKの使いどころとしては「C#でアプリケーションを開発するアプリケーションエンジニアがいつも通りの記述感でインフラ定義もしたい」といった場合にあるのかと思います。

また、今回のVPCの例の様に暗黙で構成されるリソースがありネットワーク構成を意識させない様な作りになっている点を見るに、CDKを単純なプロビジョニングツールとして見ては駄目だと感じました。
"Cloud Development Kit"の名前の通り、開発ツールとして、アプリケーションエンジニアがインフラ定義もよしなに行える様にする為のものであるというのが私の所感です。