ぶらっ記ぃ

日本語の練習をしています

ScalaからAWS CDKを叩いてインフラを構築する

モチベーション

先日、TypeScriptとPython向けに AWS Cloud Development Kit がGAになりました。

aws.amazon.com

現在GAではないのですが、JavaもこのAWS CDKの対象言語となっています。

じゃあ、JavaでできるならScalaでもできるよね?(決まり文句)
Scalaでインフラコードを書けるのは嬉しそう。

というわけでScalaからAWS CDKを叩けるか試してみました。

セットアップ

AWS CDKのインストール

npm install -g aws-cdk

Ammoniteのインストール

ammonite.io

今回はScalaスクリプトを使ってAWS CDKのAPIを叩いてみます。

以下は執筆時の最新版をインストールしています。

sudo sh -c '(echo "#!/usr/bin/env sh" && curl -L https://github.com/lihaoyi/Ammonite/releases/download/1.6.9/2.13-1.6.9) > /usr/local/bin/amm && chmod +x /usr/local/bin/amm' && amm

実行ファイルをプロジェクト下に置いておくと、他の人がAmmoniteをインストールする必要がなくなるのでおすすめ。

sudo cp /usr/local/bin/amm /path/to/your_project/amm

Scalaスクリプトの実装

AWS CDKのAPIを叩くScalaスクリプトを実装します。

リソースの定義などは公式のサンプルなどを参考にしてください。

// 依存ライブラリを追加
import $ivy.`software.amazon.awscdk:core:1.0.0.DEVPREVIEW`
import $ivy.`software.amazon.awscdk:s3:1.0.0.DEVPREVIEW`
import software.amazon.awscdk.core.{App, Construct, Stack, StackProps}
import software.amazon.awscdk.services.s3.{Bucket, BucketProps}

class ExampleStack(parent: Construct, id: String, props: StackProps = null) extends Stack(parent, id, props) {
  new Bucket(this, "aws-cdk-scala-script-example-bucket", BucketProps.builder().build())
}

// CDKアプリケーションを定義
val app = new App()

// CloudFormationのスタックを作成。appに追加。
new ExampleStack(app, "example-stack")

// CloudFormationのテンプレートを生成を明示的に行う。
// 本来は暗黙的に生成を行うようでこれはワークアラウンド。Issueが上がっている。
// https://github.com/aws/jsii/issues/456
app.synth()

cdk.json でCDKアプリケーションの実行方法を設定

今回はScalaスクリプトがCDKアプリケーションということになるので、先程のスクリプトをAmmoniteで実行するよう設定します。

{
  "app": "./amm aws-cdk-example.sc"
}

CDKアプリケーションで作成したスタックをデプロイする

スタックの一覧

cdk list ( or ls ) コマンドでCDKアプリケーションで定義したスタックの一覧が表示できます。

ここでは、上記で設定したScalaスクリプトコンパイルと実行が行われます。

$ cdk ls
Compiling /home/blacky/projects/scala/aws-cdk-scala-script/aws-cdk-example.sc
example-stack

cdk.out ディレクトリ以下にはCDKアプリケーションから作成されたテンプレートなどができています。

tree cdk.out/
cdk.out/
├── cdk.out
├── example-stack.template.json
└── manifest.json

0 directories, 3 files

$ cat cdk.out/example-stack.template.json
{
  "Resources": {
    "awscdkscalascriptexamplebucketD9CE1DF9": {
      "Type": "AWS::S3::Bucket",
      "UpdateReplacePolicy": "Retain",
      "DeletionPolicy": "Retain",
      "Metadata": {
        "aws:cdk:path": "example-stack/aws-cdk-scala-script-example-bucket/Resource"
      }
    },
    "awscdkscalaD5F5E654": {
      "Type": "AWS::IAM::User",
      "Metadata": {
        "aws:cdk:path": "example-stack/aws-cdk-scala/Resource"
      }
    },
    "awscdkscalaDefaultPolicyF833299D": {
      "Type": "AWS::IAM::Policy",
      "Properties": {
        "PolicyDocument": {
          "Statement": [
            {
              "Action": [
                "s3:GetObject*",
                "s3:GetBucket*",
                "s3:List*",
                "s3:DeleteObject*",
                "s3:PutObject*",
                "s3:Abort*"
              ],
              "Effect": "Allow",
              "Resource": [
                {
                  "Fn::GetAtt": [
                    "awscdkscalascriptexamplebucketD9CE1DF9",
                    "Arn"
                  ]
                },
                {
                  "Fn::Join": [
                    "",
                    [
                      {
                        "Fn::GetAtt": [
                          "awscdkscalascriptexamplebucketD9CE1DF9",
                          "Arn"
                        ]
                      },
                      "/*"
                    ]
                  ]
                }
              ]
            }
          ],
          "Version": "2012-10-17"
        },
        "PolicyName": "awscdkscalaDefaultPolicyF833299D",
        "Users": [
          {
            "Ref": "awscdkscalaD5F5E654"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "example-stack/aws-cdk-scala/DefaultPolicy/Resource"
      }
    }
  }
}

スタックのデプロイ

cdk deploy でスタックのデプロイを行います。

$ cdk deploy example-stack # 今回スタックはひとつなので `example-stack` は省略可
example-stack: deploying...
example-stack: creating CloudFormation changeset...
 0/3 | 18:42:21 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata | CDKMetadata
 0/3 | 18:42:22 | CREATE_IN_PROGRESS   | AWS::S3::Bucket    | aws-cdk-scala-script-example-bucket (awscdkscalascriptexamplebucketD9CE1DF9)
 0/3 | 18:42:24 | CREATE_IN_PROGRESS   | AWS::S3::Bucket    | aws-cdk-scala-script-example-bucket (awscdkscalascriptexamplebucketD9CE1DF9) Resource creation Initiated
 0/3 | 18:42:24 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated
 1/3 | 18:42:24 | CREATE_COMPLETE      | AWS::CDK::Metadata | CDKMetadata
 2/3 | 18:42:44 | CREATE_COMPLETE      | AWS::S3::Bucket    | aws-cdk-scala-script-example-bucket (awscdkscalascriptexamplebucketD9CE1DF9)
 3/3 | 18:42:46 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | example-stack

 ✅  example-stack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:012345678912:stack/example-stack/a7f50010-xxxx-yyyy-zzzz-012345678912

スタックのデプロイと適用ができました!
実際にリソースが作成されているか確かめてみましょう。

f:id:Nomad_Blacky:20190714193705p:plain

確かにS3バケットが作成されているのがわかります!

スタックの更新

せっかくなので他のリソースも作ってみましょう。
Scalaスクリプトを書き換えます。

import $ivy.`software.amazon.awscdk:core:1.0.0.DEVPREVIEW`
import $ivy.`software.amazon.awscdk:s3:1.0.0.DEVPREVIEW`
import $ivy.`software.amazon.awscdk:iam:1.0.0.DEVPREVIEW` // 追加
import software.amazon.awscdk.core.{App, Construct, Stack, StackProps}
import software.amazon.awscdk.services.iam.User
import software.amazon.awscdk.services.s3.{Bucket, BucketProps}

class ExampleStack(parent: Construct, id: String, props: StackProps = null) extends Stack(parent, id, props) {
  val bucket = new Bucket(this, "aws-cdk-scala-script-example-bucket", BucketProps.builder().build())

  // ユーザーの定義
  val user = new User(this, "aws-cdk-scala")
  // バケットへのアクセスを許可する
  bucket.grantReadWrite(user)
}

val app = new App()

new ExampleStack(app, "example-stack")

app.synth()

書き換えたら、 cdk diff を実行してみましょう。
これは現在のスタックの状態との差分を表示するコマンドです。

$ cdk diff example-stack
Stack example-stack
IAM Statement Changes
┌───┬────────────────────────────────────────────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────────────────────────┬──────────────────────┬───────────┐
│   │ Resource                                                                               │ Effect │ Action                                                                   │ Principal            │ Condition │
├───┼────────────────────────────────────────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────────┼──────────────────────┼───────────┤
│ + │ ${aws-cdk-scala-script-example-bucket.Arn}                                             │ Allow  │ s3:Abort*                                                                │ AWS:${aws-cdk-scala} │           │
│   │ ${aws-cdk-scala-script-example-bucket.Arn}/*                                           │        │ s3:DeleteObject*                                                         │                      │           │
│   │                                                                                        │        │ s3:GetBucket*                                                            │                      │           │
│   │                                                                                        │        │ s3:GetObject*                                                            │                      │           │
│   │                                                                                        │        │ s3:List*                                                                 │                      │           │
│   │                                                                                        │        │ s3:PutObject*                                                            │                      │           │
└───┴────────────────────────────────────────────────────────────────────────────────────────┴────────┴──────────────────────────────────────────────────────────────────────────┴──────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)

Resources
[+] AWS::IAM::User aws-cdk-scala awscdkscalaD5F5E654
[+] AWS::IAM::Policy aws-cdk-scala/DefaultPolicy awscdkscalaDefaultPolicyF833299D

追加したリソースとIAMのポリシーの詳細が表示されているのがわかります。

変更を確認したらデプロイしましょう。

$ cdk deploy example-stack
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬────────────────────────────────────────────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────────────────────────┬──────────────────────┬───────────┐
│   │ Resource                                                                               │ Effect │ Action                                                                   │ Principal            │ Condition │
├───┼────────────────────────────────────────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────────┼──────────────────────┼───────────┤
│ + │ ${aws-cdk-scala-script-example-bucket.Arn}                                             │ Allow  │ s3:Abort*                                                                │ AWS:${aws-cdk-scala} │           │
│   │ ${aws-cdk-scala-script-example-bucket.Arn}/*                                           │        │ s3:DeleteObject*                                                         │                      │           │
│   │                                                                                        │        │ s3:GetBucket*                                                            │                      │           │
│   │                                                                                        │        │ s3:GetObject*                                                            │                      │           │
│   │                                                                                        │        │ s3:List*                                                                 │                      │           │
│   │                                                                                        │        │ s3:PutObject*                                                            │                      │           │
└───┴────────────────────────────────────────────────────────────────────────────────────────┴────────┴──────────────────────────────────────────────────────────────────────────┴──────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)

Do you wish to deploy these changes (y/n)? y
example-stack: deploying...
example-stack: creating CloudFormation changeset...
 0/3 | 19:03:03 | CREATE_IN_PROGRESS   | AWS::IAM::User     | aws-cdk-scala (awscdkscalaD5F5E654)
 0/3 | 19:03:04 | CREATE_IN_PROGRESS   | AWS::IAM::User     | aws-cdk-scala (awscdkscalaD5F5E654) Resource creation Initiated
0/3 Currently in progress: awscdkscalaD5F5E654
 1/3 | 19:03:40 | CREATE_COMPLETE      | AWS::IAM::User     | aws-cdk-scala (awscdkscalaD5F5E654)
 1/3 | 19:03:42 | CREATE_IN_PROGRESS   | AWS::IAM::Policy   | aws-cdk-scala/DefaultPolicy (awscdkscalaDefaultPolicyF833299D)
 1/3 | 19:03:44 | CREATE_IN_PROGRESS   | AWS::IAM::Policy   | aws-cdk-scala/DefaultPolicy (awscdkscalaDefaultPolicyF833299D) Resource creation Initiated
 2/3 | 19:03:52 | CREATE_COMPLETE      | AWS::IAM::Policy   | aws-cdk-scala/DefaultPolicy (awscdkscalaDefaultPolicyF833299D)
 2/3 | 19:03:54 | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack | example-stack
 3/3 | 19:03:55 | UPDATE_COMPLETE      | AWS::CloudFormation::Stack | example-stack

 ✅  example-stack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:012345678912:stack/example-stack/a7f50010-xxxx-yyyy-zzzz-012345678912

f:id:Nomad_Blacky:20190714193734p:plain

無事、IAMユーザとポリシーの設定ができました!

まとめ

ScalaAWSのインフラ作成ができるようになりました。激アツですね!
TerraformのようなHCLの書き方を覚える必要がないですし、慣れた言語やIDEの恩恵を受けながらガツガツインフラを書けるのは夢がありそうです。

ただし、気になるところはいくつかあって、

  • AWS以外のインフラも併せて作成する場合には素直にTerraformなどを使ったほうが良さそう。
  • Javaのビルダーパターンによるリソースの定義が若干鬱陶しい
  • AWS CDK Javaはまだ開発段階
    • コンパイル時にリソース定義の誤り(必須パラメータが足りないなど)は検出されない。
    • バリデーションに抜けがあって、スタックの適用時にエラーになる。

など、これからに期待という感じでしょうか。

今回の成果物

github.com

余談

AWS CDK Javaはjsiiというライブラリ使ってJavaからTypeScriptのモジュールを呼び出しているらしいです。

github.com