- Published on
AWS CDK starter kit by kotlin - 2 - Project構造
Overview
Gradle Project の中身を見ながら Kotlin と CDK をどう扱っていけば良いのかみてみましょう
ここでは AWS Resource を追加する度に何を変更すれば良いのかを知ることが目標です。
次の要素をみていきます。
前提条件
下記の条件を揃っている方がご活用することを期待しています。
- 前回の記事を実行した方
Project 全体構成

我々が実際手を加えるのは
- build.gradle.kts
- cdk.json
- src/main/kotlin/ この 3 つです。
CDK より下記の File が生成されますので、Repository には共有しないように.gitignoreに追加しましょう
- cdk.out/
- cdk.context.json は CDK 自動生成さ
手を動かす順番
- 作業する立場で実際どのような流れで考えれば良いのか先にみてみましょう
- build.gradle.ktsの Dependency に必要な AWS Resource の CDK Library を追加する
- 必要な Stack Class を作る
- 必要な AWS Resource を Stack Class の中に宣言する
- 必要に応じて他の Stack から Variable として AWS Resource を参照する
- Stack を Instance 化しAppInstance を Inject する
- App:synth()が実行される
- Starter を用いて基盤さえ整えておけば修正するのは割と簡単な Process ですよね。但しこの Project 構成自体になれる必要はあるので少しずつみていきましょう
CDK Project Setting
- まずは必要な Library と構成を知るために Build 設定をみてみましょう 
- Plugin 設定 - plugins { kotlin("jvm") version "1.8.0" application }- CDK は CLI Application なので実行のために Application Pluginを活用します。 
- Dependency 設定 - val AWS_CDK_VERSION = "1.193.0" val AWS_SDK_VERSION = "2.20.35" dependencies { implementation("software.amazon.awscdk:cdk:0.36.1") implementation("software.amazon.awscdk:ec2:$AWS_CDK_VERSION") // implementation("software.amazon.awscdk:s3:$AWS_CDK_VERSION") implementation("software.amazon.awscdk:cloudwatch:$AWS_CDK_VERSION") // implementation("software.amazon.awscdk:ecs:$AWS_CDK_VERSION") // implementation("software.amazon.awscdk:ecs-patterns:$AWS_CDK_VERSION") // implementation("software.amazon.awscdk:iam:$AWS_CDK_VERSION") // implementation("software.amazon.awscdk:elasticloadbalancing:$AWS_CDK_VERSION") // implementation("software.amazon.awscdk:ecr:$AWS_CDK_VERSION") testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.2") testImplementation("org.apache.logging.log4j:log4j-core:2.8.2") testImplementation("org.apache.logging.log4j:log4j-slf4j-impl:2.1") testImplementation("software.amazon.awssdk:cloudwatch:$AWS_SDK_VERSION") testImplementation("software.amazon.awssdk:cloudwatchlogs:$AWS_SDK_VERSION") testImplementation("software.amazon.awssdk:cloudwatchevents:$AWS_SDK_VERSION") }- Group 名software.amazon.awscdkが CDK 関連 Package です。必要な AWS Resource ごとに分かれているので生成しようと思う Resource ごとに Library を追加しながら開発を行います
- 全ての Library を追加する楽ではありますが初期起動に時間がかかるし、Project の新規 Member の Input が余計に多くなるので必要なものだけ残していきましょう
- testImplementation は Optional であります。- 一般的には AWS Resource に反映する Provisioning は別の Tool(Ansible 等)を活用すると思いますが、簡単な設定やテストなら SDK を活用することも良いです。
- CDK はあくまで Resource 生成が目的であるので、その Resource 自体を扱う際には IAM 設定にご注意下さい
 
 
- Group 名
- { "app": "./gradlew run", "build": "./gradlew build -x test", "watch": { "include": "src/main/**" } }- 基本的にどの Command を実行すれば CDK Application が起動するのか?への答えがこの File の趣旨です。
- Field: app- CLI Application の起動するための Command で、Gradle にて Application Plugin を指定したため run task で起動させます。
- 複雑なアプリケーションで別の起動 Library が活用されているのであれば、適宜変更して下さい。
- この設定だけでも動作します
 
- Field: build- Build 指定をしないと度々変更が反映されず実行する時があるので、Gradle の Build task を明示的に指定します。
- その際にはテストは不要なので Skip させましょう。
 
- Field: watch- Application なので変更内容を自動的に反映することも可能です。
- ただ、一度実行すると数分はかかる場合が多いため個人的にはあんまり使わないものであります
 
 
 
CDK Code
  package com.sa201
  import com.sa201.helper.DefaultOption
  import com.sa201.helper.Dictionary
  import com.sa201.stack.CloudWatchStack
  import com.sa201.stack.InfraStack
  import software.amazon.awscdk.core.App
  fun main(args: Array<String>) {
      val app = App()
      InfraStack(app, Dictionary.genStackName("infra"), DefaultOption.defaultStackProps)
      CloudWatchStack(app, Dictionary.genStackName("cloudwatch"), DefaultOption.defaultStackProps)
      app.synth()
  }
- CDK Application の Life Cycle - software.amazon.awscdk.core.AppClass にて CDK Application を宣言します
- 各所の AWS Resource Class を宣言し、そこに App Instance を渡して Build します
- App Instance の synth()Method を実行し Clou d Formation を生成します
 
- この後の作業については CDK CLI が行っていることなのでこの Project 自体の内容とは Scope 外です 
- App Instance はすることが決まっており我々は App Instance を引き継いて AWS Resource を宣言するだけの簡単な仕組みになっています - CloudFormation Template を学ぶために使って時間を考えると Learning Curve がどれほどゆるくなったのか実感します
 
- ここでは Stack 単位で区別していますが、BestPractice とかでも無いので規模によって適宜 Package Styling を行って下さい 
- Dictionary.genStackName("infra")- ここはDictionary Classを用いって Stack に Prefix を加えます
- AWS Resource 自体は CDK が任意で名前を決めてくれったりしますが、Stack に関してはここで宣言する Naming が Unique なものになります
- 同じ環境に複数の人が Deployment したり、Stack を纏めて見やすくする時のために一定の Prefix を付けることをオススメします- SA201-という Prefix が付いているので Nested Stack で無くても纏めて見やすいです
 
 
- DefaultOption.defaultStackProps- Stack そのもの自体に対して下記を指定します- AWS Account
- AWS Region
- Tag
 
- 個辺は Cross Region 化しない限りあんまり変わることが無いので纏めた Instance で各 Stack に振り分けると楽です
- Tagging をここで行うと Stack より生成される AWS Resource にも全部適用されるのでぜひ活用しましょう。
 
- Stack そのもの自体に対して下記を指定します
- /src/main/kotlin/com/sa201/helper/Dictionary.kt - package com.sa201.helper object Dictionary { private const val stackNamePrefix = "SA201-" fun genStackName(name: String) = stackNamePrefix + name val defaultTagging = mapOf<String, String>( "Department" to "SRE", "User" to "Greg", "Service" to "SystemMaintenance" ) const val TARGET_VPC_ID = "vpc-65ff500d" }- Main.ktで扱った内容と重複した内容は外します
- Tag は Key/Value 指定なのでここでは Map で宣言して活用します- Tag 用 Class が別途に存在しますがいざ Stack に Parameter として渡す時は Pair<String, String>へ変更する必要があるためわざわざ Tag Instance は作りません
 
- Tag 用 Class が別途に存在しますがいざ Stack に Parameter として渡す時は 
- const val TARGET_VPC_ID各種固定変数はここで宣言- 既に Deployment 済みである ARN や共有する Naming 等はこちらで宣言しておくと HardCording の間違いが無く便利です
- .propertyFile を通して各変数を定義するやり方もありますが、基本的には HardCording して Dictionary Class ようなものに纏めておけば良いと思います- 下記の点を考慮しました- Property を Loading する Process が加わるのが煩わしいこと
- property を良く変更するような使い方をするアプリケーションでは無い
- jar ファイルとして配布するものでは無いためその場で変更して適用出来る
 
 
- 下記の点を考慮しました
 
 
- /src/main/kotlin/com/sa201/stack/InfraStack.kt - package com.sa201.stack import com.sa201.helper.Dictionary import software.amazon.awscdk.core.CfnOutput import software.amazon.awscdk.core.Construct import software.amazon.awscdk.core.Stack import software.amazon.awscdk.core.StackProps import software.amazon.awscdk.services.ec2.Vpc import software.amazon.awscdk.services.ec2.VpcLookupOptions class InfraStack(scope: Construct, id: String, props: StackProps): Stack(scope, id, props) { val vpc = Vpc.fromLookup(this, "vpc", VpcLookupOptions.builder().vpcId(Dictionary.TARGET_VPC_ID).build()) init { CfnOutput.Builder.create(this, "vpc-az") .exportName("vpc-az") .value(vpc.availabilityZones.joinToString(", ")) .build() } }- InfraStack Class の存在理由- ここは言い換えると Deployment 済みの AWS Resource の Import 専用 Stack です
- 決まっているものでは無いですがここで Import しておけば必要な時 Variable として他の Stack で参照して使いやすいので良く使う Pattern です
- 外部 Resource はここをまず見れば良いという認識があると便利です
 
- software.amazon.awscdk.core.Stackとは?- CloudFormation は結果的に Stack を作るためであり、全ての始まりはここになります
- 言葉どおり Stack そのもの自体を宣言するもので、Stack 初期化のために必要なものを Parameter として渡します
 
- .fromLookup- ARN や特定の ID を通して AWS Resource を Import する時使用します。
- ここで vpc という variable を
 
- init- 態々 Variable 化する必要が無いもの(他で参照必要性が無いもの)をここで宣言する場合があります
- CDK の Class によっては Builder が Build した後でしか Inject 出来ない Parameter がある場合もあります- その時は init を通して宣言すると各種の Builder instance が Build された後に実行されるので別途の Method を呼び出す手間が減ります
 
 
 
- InfraStack Class の存在理由
- /src/main/kotlin/com/sa201/stack/CloudWatchStack.kt - package com.sa201.stack import software.amazon.awscdk.core.* import software.amazon.awscdk.services.logs.LogGroup import software.amazon.awscdk.services.logs.LogStream import software.amazon.awscdk.services.logs.RetentionDays class CloudWatchStack(scope: Construct, id: String, props: StackProps): Stack(scope, id, props) { val logGroup = LogGroup.Builder.create(this, "log-test-group") .logGroupName("log-test-group") .retention(RetentionDays.ONE_DAY) .removalPolicy(RemovalPolicy.DESTROY) .build() val logStream = LogStream.Builder.create(this, "debugging-stream") .logStreamName("debugging-stream") .logGroup(logGroup) .build() init { CfnOutput.Builder.create(this, "region") .exportName("region") .value(props.env!!.region) .build() CfnOutput.Builder.create(this, "logGroupName") .exportName("logGroupName") .value(logGroup.logGroupName) .build() CfnOutput.Builder.create(this, "logStream") .exportName("logStream") .value(logStream.logStreamName) .build() } }- .Builder.create(this, "log-test-group")- thisの出現- Construct と呼ばれるものには Stack Instance を渡します- Stack の Construct は Main.ktで宣言した App Instance になります
- ここで最初 Stack Constructor にある scope variable を渡してハマった記憶があります
 
- Stack の Construct は 
 
- Construct と呼ばれるものには Stack Instance を渡します
- CfnOutput は CloudFormation の Output を定義- 必ずこれを出す必要は無いですが、Output File で何かしら活用したい時はここで宣言すれば良いです
- 割と JSON で出しておけば便利なケースが度々あるので、使い方を覚えておくと良いですね
 
 
最後に
IaC 自体は概念は簡単ですけどいざ実行すると色んな壁にぶつかると思います。
ただ、人は忘れ物がちだしほとんどの障害は Human Error なので Static なやり方になるほど事前に防げる確率が高まります。
Java/Kotlin になれている私にはこの CDK 構成が良かったですが、開発と距離があったり(Operator とか)すると割と Kotlin 自体に壁が高いかも知れません。
その場合は Typescript とかより簡単な Language を選んだ方が良いかも知れません。ただ、DevOps チームで Application が Kotlin であれば Kotlin/CDK の組み合わせは選択しない理由がむしろ無いと思います。
Static な Language だと Builder の使い方や Type の Documentation が良く出来ているので IDE だけで簡単にかけるのはメリットではありますが、やはり開発になれている必要はあると思うので Junior Developer には CDK 意外にも学ぶことが多いかも知れません。