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

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

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