AWS CDK × Scala + sbtでAWSサービスをデプロイ + ローカル上に環境構築する

## はじめに
AWS CDK × Scala + sbtでCloudformationのスタックやローカルにテスト環境を構築する方法をまとめてみた記事になります。
誰かの助けになれば嬉しいです。

## 対象読者
・AWS CDK × Scala + sbtを使って AWSサービスをデプロイしたいと考えている方
・CDKで出力した Cloudformationから localStack上に AWSサービスの環境を構築したい方

## 実行環境
・scala: 2.13.3
・sbt: 1.3.13
・aws-cdk: 1.57.0
・localStack: 0.10.0
・awscli-local: 0.7
・node: 12.18.3

## サンプルリポジトリ
https://github.com/hiroyuki-nishi/aws-cdk-sbt-sample

## あわせて読むといいドキュメント
AWS CDK Developer Guide
https://docs.aws.amazon.com/cdk/latest/guide/home.html

AWS CDK API Reference
https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html


## インストール
まずは、AWS CDKのcdkコマンドを実行できるようにaws-cdkをインストールします。

```.sh
npm i aws-cdk@1.57.0 -g
```

## built.sbtを作成する
次に、AWS CDKの cdkコマンドを実行できるように build.sbtを整えていきます。
root 直下にプロジェクトを作成してもいいのですが、sbt はマルチプロジェクトを作成できるので
今回は別途 cdk用のプロジェクトを作成しました。 

```.scala:build.sbt
lazy val commonSettings = Seq(
  organization := "sample",
  scalaVersion := "2.13.3",
  scalacOptions := Seq(
    "-deprecation",
    "-feature"
  ),
  scalafmtOnCompile in ThisBuild := true,
  test in assembly := {}
)

lazy val cdkDependencies = Seq(
  "software.amazon.awscdk" % "core" % awsCdkVersion,
  "software.amazon.awscdk" % "apigateway" % awsCdkVersion,
  "software.amazon.awscdk" % "lambda" % awsCdkVersion,
  "software.amazon.awscdk" % "s3" % awsCdkVersion,
  "software.amazon.awscdk" % "dynamodb" % awsCdkVersion,
  "software.amazon.awscdk" % "stepfunctions" % awsCdkVersion,
  "software.amazon.awscdk" % "stepfunctions-tasks" % awsCdkVersion,
  "software.amazon.awscdk" % "ecr" % awsCdkVersion,
  "software.amazon.awscdk" % "ecs-patterns" % awsCdkVersion,
  "software.amazon.awscdk" % "ec2" % awsCdkVersion,
  "software.amazon.awscdk" % "codecommit" % awsCdkVersion,
  "software.amazon.awscdk" % "codebuild" % awsCdkVersion,
  "software.amazon.awscdk" % "codepipeline" % awsCdkVersion,
  "software.amazon.awscdk" % "codepipeline-actions" % awsCdkVersion
)

... (略)

lazy val cdk = (project in file("modules/cdk"))
.settings(commonSettings: _*)
.settings(
  libraryDependencies ++= cdkDependencies
)
```

## cdk.jsonを変更する

cdkをsbtで起動するために、**"app"**に書かれているものをsbt用に変更します。
別プロジェクト(cdk)として作成しているので起動は **cdk/run** を指定
また、後でローカル上に環境を構築したいのでsbt runが実行される際に出力される情報などを出力されないようにパラメータを追加しておきます。


また、**マルチアカウント**で運用されている場合、環境変数などを指定したい場合があると思いますので
cdk.jsonに必要そうな regionなどのキーを追加します。

```.json:cdk.json
{
  "app": "sbt --error 'set showSuccess := false' cdk/run",
  "context": {
    "@aws-cdk/core:enableStackNameDuplicates": "true",
    "aws-cdk:enableDiffNoFail": "true",
    "local": {
      "env": "local",
      "region": "us-east-1",
      "account_id": "000000000000"
    },
    "dev": {
      "env": "dev",
      "region": "ap-northeast-1",
      "account_id": "xxxxxxxxxxxxx"
    }
  }
}
```

## CDKでスタックを作成する
実際に AWS CDKを使ってスタック作成していきます。
ディレクトリ構成はこのような感じです。

```
├── README.md
├── build.sbt
├── buildspec.yml
├── cdk.json
├── modules
│   └── cdk
│       └── src
│           └── main
│               └── scala
│                   ├── CdkApp.scala (Mainクラス)
│                   ├── CodePipeLineStack.scala (CodeCommit+CodeBuild+CodePipelineのスタック)
│                   ├── DynamoDBStack.scala (DynamoDB用のスタック)
│                   ├── EcrStacks.scala(ECR用のスタック)
│                   ├── FargateStack.scala(Fargate用のスタック)
│                   ├── LambdaStack.scala(Lambda用のスタック)
│                   ├── S3Stack.scala(S3用のスタック)
│                   ├── SqsStack.scala(SQS用のスタック)
│                   └── StepFunctionsStack.scala(StepFunctions用のスタック)
├── tmp
│   
└── version.sbt
```

例えば、CDKで SQSのスタックを作成したい場合
Mainクラスとなる **CdkApp.scala**に SQSを作成する Stackクラスを定義します。

```.scala:CdkApp.scala
object CdkApp {
  def main(args: Array[String]): Unit = {
    val app = new App
    new SqsStack(app, "sqs", null)
    app.synth
  }
}
```

次に、SQSを作成するスタックには Stackクラスを継承したクラスを作成していきます。
AWS SDK v2 のように Builderパターンで **Props**を書いてスタックを作ればOKです。


```.scala:SqsStack.scala
import software.amazon.awscdk.core.{Construct, Stack, StackProps}
import software.amazon.awscdk.services.sqs.{Queue, QueueProps}
import software.amazon.awscdk.core.Duration

class SqsStack(val scope: Construct, val id: String, val props: StackProps)
    extends Stack(scope, id, props) {
  private val env = this.getNode.tryGetContext("env").asInstanceOf[String]
  private val testSqs = new Queue(
    this,
    s"test-sign-sqs-$env", // スタック内で一意となるidを指定
    QueueProps
      .builder()
      .queueName(s"test-sign-$env") // キュー名
      .visibilityTimeout(Duration.seconds(180)) // 可視性タイムアウト
      .build()
  )
}
```

定義するパラメータなどは、yamlなどでCloudfomarionを書く際のドキュメントを読むなり
AWS CDK API Referenceを参照しながら書いていきましょう。

・[AWS Cloudformation: AWS::SQS::Queue
](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html)・[AWS CDK API](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html)

## ローカル上に環境を構築してみる
CDKを使い、プロファイル指定などをしてAWS上にデプロイする際は以下のコマンドで実行できます。

```.sh
cdk deploy sqs --profile <任意のプロファイル> -c env=dev 
```

また、実際にデプロイする前に、ローカル上で環境を構築して動作確認などができるように **localStack** を利用してローカル上に環境を構築してテストできるようにしてみました。

-----

コマンドを毎回打つのが面倒なので、package.jsonなどに以下のような scriptを定義します。

```.json:package.json
"create-stack:sqs": "cdk synth sqs --c env=local > tmp/sqs.template.yml; awslocal cloudformation create-stack --stack-name local-sqs-stack --template-body file://./tmp/sqs.template.yml",
"delete-stack:sqs": "awslocal cloudformation delete-stack --stack-name local-sqs-stack"
```

あとは、localStackをdockerで起動しておき
以下のnpm run コマンドを実行すれば、ローカル上に SQSが作成されると思います。

```.sh
npm run create-stack:sqs
```

作成した SQSを削除したい場合は以下のコマンドを実行してください。

```.sh
npm run delete-stack:sqs
```

## まとめ
普段、Cloudformatonをyamlなどで書き慣れていないといきなりCDKで書くのはシンドイかなと思いつつも
慣れ親しんだ、言語（Scala、Typescript、Pythonなど)で書けてデプロイまでできるので中々いい感じじゃないかなと・・・

現場からは以上です！