Published on

AWS CDK starter kit by kotlin - 2 - Project構造

Overview

Gradle Project の中身を見ながら Kotlin と CDK をどう扱っていけば良いのかみてみましょう

ここでは AWS Resource を追加する度に何を変更すれば良いのかを知ることが目標です。

次の要素をみていきます。

前提条件

下記の条件を揃っている方がご活用することを期待しています。

Project 全体構成

IDE-Project-Structure

我々が実際手を加えるのは

  • build.gradle.kts
  • cdk.json
  • src/main/kotlin/ この 3 つです。

CDK より下記の File が生成されますので、Repository には共有しないように.gitignoreに追加しましょう

  • cdk.out/
  • cdk.context.json は CDK 自動生成さ

手を動かす順番

  • 作業する立場で実際どのような流れで考えれば良いのか先にみてみましょう
  1. build.gradle.kts の Dependency に必要な AWS Resource の CDK Library を追加する
  2. 必要な Stack Class を作る
  3. 必要な AWS Resource を Stack Class の中に宣言する
  4. 必要に応じて他の Stack から Variable として AWS Resource を参照する
  5. Stack を Instance 化しApp Instance を Inject する
  6. App:synth()が実行される
  • Starter を用いて基盤さえ整えておけば修正するのは割と簡単な Process ですよね。但しこの Project 構成自体になれる必要はあるので少しずつみていきましょう

CDK Project Setting

  • build.gradle.kts

    • まずは必要な 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 設定にご注意下さい
    • cdk.json

      {
        "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

    1. software.amazon.awscdk.core.App Class にて CDK Application を宣言します
    2. 各所の AWS Resource Class を宣言し、そこに App Instance を渡して Build します
    3. 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 で無くても纏めて見やすいです
      • AWS Console - CloudFormation
  • DefaultOption.defaultStackProps

    • Stack そのもの自体に対して下記を指定します
      • AWS Account
      • AWS Region
      • Tag
    • 個辺は Cross Region 化しない限りあんまり変わることが無いので纏めた Instance で各 Stack に振り分けると楽です
    • Tagging をここで行うと Stack より生成される AWS Resource にも全部適用されるのでぜひ活用しましょう。
  • /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 は作りません
    • const val TARGET_VPC_ID 各種固定変数はここで宣言
      • 既に Deployment 済みである ARN や共有する Naming 等はこちらで宣言しておくと HardCording の間違いが無く便利です
      • .property File を通して各変数を定義するやり方もありますが、基本的には 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 を呼び出す手間が減ります
  • /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 を渡してハマった記憶があります
    • 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 意外にも学ぶことが多いかも知れません。