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

2019.08.22

しばたです。

先月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

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

テンプレート全体を表示

```yaml 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

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

<img class="alignnone size-full wp-image-466074" src="https://dev.classmethod.jp/wp-content/uploads/2019/08/try-windows-cdk-csharp-05.png" alt="" width="849" height="675" />

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

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

### デプロイ

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

```powershell
# 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"の名前の通り、開発ツールとして、アプリケーションエンジニアがインフラ定義もよしなに行える様にする為のものであるというのが私の所感です。