kadowaki のプロフィール画像@kadowaki

投稿日
2023/01/04

AWS CDK を触ってみよう

あらすじ

弊社ではインフラ構築に一部 Terraform を利用しています。しかし Terraform はアプリエンジニアがちょっとした環境を用意するには手軽さに欠けます。そういうわけでか私含め弊社のアプリエンジニアは AWS のウェブコンソールでリソース作成をすることが多いのが現状です。
しかし IaC 化されていないと構成の再現性が保証できませんし、スクラップアンドビルドが手軽に行えなえず構成の改善がしにくくなります。
そこで AWS CDK を試してみたところ学習コストも簡単に扱えそうだったので社内で布教しようと思いました。1

What is AWS CDK

概要

AWS CDK とは AWS Cloud Development Kit の略で、プログラムング言語を用いて AWS リソースの定義や作成を行うことのできるフレームワークです。
AWS にはリソースの定義を管理しリソースの生成破棄を自動化してくれる CloudFormation というサービスがあります。CloudFormation ではリソースを CloudFormation テンプレートと呼ばれる JSON または YAML ファイルによって定義します。
CDK は雑に言ってしまえば CloudFormation テンプレートを作成しリソースの生成破棄を CloudFormation に委譲する道具です。

CDK を用いると CloudFormation テンプレートに相当するものをプログラミング言語における構造体やオブジェクトとして表現できます。
このときデフォルトの設定値を省略できるので CloudFormation テンプレートを直接作成するより記述量が少なくなる点が CDK のメリットの一つです。

構成

CDK のプロジェクトは以下のように App, Stack, Construct という単位で構成されます。
それぞれの概念について簡単に説明します。

AppStacks
AWS CDK 公式ドキュメント より引用

App

ひとつの CDK のプロジェクト (と理解しています)。
一つ以上の Stack を含みます。

Stack

リソースのデプロイの単位です。
一つ以上の Construct を含みます。

Construct

AWS のリソースの単位です。
EC2 インスタンスや S3 バケットなどがそうです。

実際に CDK を触る

ECS の Fargate サービスとして nginx コンテナを動作させることにします。

ここでは VPC と ALB も新規に作成しますが既存のものを使い回すこともできます。

前提条件

  • npm 10.13.0 or later
  • Go 1.18 or later
  • AWS アカウントと適切なロール

CDK のインストール

詳しくは公式のページ参照 (https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html)

npm install -g aws-cdk

色々準備

適当なディレクトリを作成して進めます。ここでは cdk-sample としました。

mkdir cdk-sample
cd cdk-sample
cdk init --language go

必要なライブラリをとってきます。

go get

ブートストラップ

CDK を利用するための IAM ロールやファイルを格納するための S3 バケットなどが作成されます。

cdk bootstrap

コードを書く

./
├── .git
├── .gitignore
├── README.md
├── cdk-sample.go
├── cdk-sample_test.go
├── cdk.context.json
├── cdk.json
├── cdk.out
├── go.mod
└── go.sum

cdk-sample.go を以下のように編集します。

package main

import (
	"github.com/aws/aws-cdk-go/awscdk/v2"
	ec2 "github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
	ecs "github.com/aws/aws-cdk-go/awscdk/v2/awsecs"
	elb "github.com/aws/aws-cdk-go/awscdk/v2/awselasticloadbalancingv2"
	"github.com/aws/constructs-go/constructs/v10"
	"github.com/aws/jsii-runtime-go"
)

type CdkSampleStackProps struct {
	awscdk.StackProps
}

func NewCdkSampleStack(scope constructs.Construct, id string, props *CdkSampleStackProps) awscdk.Stack {
	var sprops awscdk.StackProps
	if props != nil {
		sprops = props.StackProps
	}
	// Stack の作成 (今回は 1 つだけ Stack を作成します)
	stack := awscdk.NewStack(scope, &id, &sprops)

	// 以下 Stack に Construct を作成していきます作成

	// VPC の作成
	vpc := ec2.NewVpc(stack, jsii.String("sampleVpc"), &ec2.VpcProps{
		VpcName: jsii.String("sample-vpc"),
		MaxAzs:  jsii.Number(3),
		SubnetConfiguration: &[]*ec2.SubnetConfiguration{
			// public subnet を一つ作成
			{
				Name:       jsii.String("sampleVpcPublicSubnet"),
				SubnetType: ec2.SubnetType_PUBLIC,
				CidrMask:   jsii.Number(24),
			},
		},
	})

	// ECS クラスタの作成
	cluster := ecs.NewCluster(stack, jsii.String("sampleEcsCluster"), &ecs.ClusterProps{
		ClusterName: jsii.String("sample-cluster"),
		Vpc:         vpc,
	})

	// タスク定義の作成
	taskDef := ecs.NewFargateTaskDefinition(stack, jsii.String("sampleNginxTask"), nil)
	taskDef.AddContainer(jsii.String("sampleNginxContainer"), &ecs.ContainerDefinitionOptions{
		Image: ecs.ContainerImage_FromRegistry(jsii.String("nginx"), nil),
		PortMappings: &[]*ecs.PortMapping{{
			ContainerPort: jsii.Number(80),
			Protocol:      ecs.Protocol_TCP,
		}},
	})

	// ECS サービスの作成
	service := ecs.NewFargateService(stack, jsii.String("sampleFargateService"), &ecs.FargateServiceProps{
		Cluster:        cluster,
		TaskDefinition: taskDef,
		AssignPublicIp: jsii.Bool(true),
	})

	// ALB の作成
	alb := elb.NewApplicationLoadBalancer(stack, jsii.String("sampleAlb"), &elb.ApplicationLoadBalancerProps{
		LoadBalancerName: jsii.String("sample-alb"),
		Vpc:              vpc,
		InternetFacing:   jsii.Bool(true),
	})
	listener := alb.AddListener(jsii.String("sampleListener"), &elb.BaseApplicationListenerProps{
		Port: jsii.Number(80),
	})

	// ECS サービスと ALB のリスナーを紐付ける
	service.RegisterLoadBalancerTargets(&ecs.EcsTarget{
		ContainerName:    service.TaskDefinition().DefaultContainer().ContainerName(),
		ContainerPort:    service.TaskDefinition().DefaultContainer().ContainerPort(),
		NewTargetGroupId: jsii.String("sampleTagetGroup"),
		Listener: ecs.ListenerConfig_ApplicationListener(listener, &elb.AddApplicationTargetsProps{
			Protocol: elb.ApplicationProtocol_HTTP,
		}),
	})

	return stack
}

func main() {
	defer jsii.Close()

	// App の作成
	app := awscdk.NewApp(nil)

	NewCdkSampleStack(app, "CdkSampleStack", &CdkSampleStackProps{
		awscdk.StackProps{
			Env: &awscdk.Environment{
				Account: jsii.String("<AWSアカウントID>"),
				Region:  jsii.String("<リージョン>"),
			},
		},
	})

	app.Synth(nil)
}

各々の構造体や関数の定義は CDK の公式ドキュメントを参照ください。
例えば VPC の設定を持つ ec2.VpcProps という構造体のドキュメントは以下です。設定しなかったもののどのような値がデフォルト値として設定されるか確認してみてください。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.VpcProps.html

作成されるテンプレートの確認

以下のコマンドで CDK が作成する CloudFormation テンプレートを確認できます。

cdk synth

デプロイ

実際にリソースを生成します。

cdk deploy

動作確認

作成した ALB のドメイン名を確認して Nginx のデフォルトページが表示されるか確認してみましょう。
ALB には sample-alb と名前を付けたので、次のコマンドでドメイン名等の情報を確認できます。

aws elbv2 describe-load-balancers --names "sample-alb"

破棄

リソースを破棄します。

cdk destroy

まとめ

かなり簡単にリソースが作成できることがわかったのではないでしょうか。デフォルトの設定値で良ければ記述量もかなり少なく済ませることができますし、リソースの生成破棄も簡単です。開発環境目的では必要なときだけリソースが動いていればよいという状況は多いですが、そうったときには特に便利ではないかと思います。

参考記事


  1. 2022-08-09 に CDK for Terraform が GA になりました。こっちも触ってみたいです。https://aws.amazon.com/blogs/opensource/announcing-cdk-for-terraform-on-aws/
0