ScalaからAWS CDKを叩いてインフラを構築する
モチベーション
先日、TypeScriptとPython向けに AWS Cloud Development Kit がGAになりました。
現在GAではないのですが、JavaもこのAWS CDKの対象言語となっています。
じゃあ、JavaでできるならScalaでもできるよね?(決まり文句)
Scalaでインフラコードを書けるのは嬉しそう。
というわけでScalaからAWS CDKを叩けるか試してみました。
セットアップ
AWS CDKのインストール
npm install -g aws-cdk
Ammoniteのインストール
今回は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
スタックのデプロイと適用ができました!
実際にリソースが作成されているか確かめてみましょう。
確かに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
無事、IAMユーザとポリシーの設定ができました!
まとめ
ScalaでAWSのインフラ作成ができるようになりました。激アツですね!
TerraformのようなHCLの書き方を覚える必要がないですし、慣れた言語やIDEの恩恵を受けながらガツガツインフラを書けるのは夢がありそうです。
ただし、気になるところはいくつかあって、
- AWS以外のインフラも併せて作成する場合には素直にTerraformなどを使ったほうが良さそう。
- Javaのビルダーパターンによるリソースの定義が若干鬱陶しい
- AWS CDK Javaはまだ開発段階
- コンパイル時にリソース定義の誤り(必須パラメータが足りないなど)は検出されない。
- バリデーションに抜けがあって、スタックの適用時にエラーになる。
など、これからに期待という感じでしょうか。
ウッ… pic.twitter.com/wUSa7uYg8w
— blac_k_ey (@blac_k_ey) July 13, 2019
あーっ…こういうのコンパイルで弾いてほしかった… pic.twitter.com/Z6QM4OWWWh
— blac_k_ey (@blac_k_ey) July 13, 2019
ふぇぇ…バリデーションエラーどころかランタイムエラーでたよ… pic.twitter.com/imuisg6HNP
— blac_k_ey (@blac_k_ey) July 13, 2019
今回の成果物
余談
AWS CDK Javaはjsiiというライブラリ使ってJavaからTypeScriptのモジュールを呼び出しているらしいです。