このページは https://github.com/kazurayam/NeoGOF のREAMDEドキュメントを日本語で書き直したものです。
概要
GradleとShellとAWS CLIとCloudFormation: この4つのソフトウェアツールをうまく組み合わせて使えばJava/Groovy/Kotlinの開発者の仕事が楽になります。この4つの組み合わせを Neo GOF (new Gang of Four) と名付けます。
解決すべき問題
- わたしは One Manプロジェクトつまり独りで全部作るプロジェクトをいくつも持っている。Java/Groovy/Kotlin言語でアプリケーションを書くだけでなく、それを動かすための環境をAWS上に構築するのも自分でやる。分業しない。
- わたしは ビルドツール Gradle を使って
継続的デリバリ
をやりたい。$ gradle deploy
とコマンド一発で、JavaやKotlinで書いた
AWS Lambda関数を稼働させるのに必要なすべての作業をGradleによる制御の元で実行したい。 - Javaアプリケーションをコンパイルしてテストしてjarファイルにまとめる作業をいつものようにGradleでやりたい。
- Lambda関数として実行するjarファイルをAWS S3バケットに配置する必要があるが、S3バケットを作る作業も自動化したい。
- Javaコードを修正変更するたびにjarファイルをS3にアップロードし直さなければならない。アップロードの作業を自動化したい。
- S3にかぎらず様々なAWSリソース (Lambda関数、CloudWatchイベント、
IAMロール、 CloudFront、Route53、AWS CertificateManager、SQS、SNSなど)をプロビジョンするのに**Cloud Formation**を使いたい。ただしGradleからCloudFormationを動かしたい。
さてここで問題です。Gradleの build.gradle
スクリプトからAWS CloudFormationを動かしたいが、どういう方法を使うのがいいか?
解決方法
2つ、やり方を見つけた。
- GradleとShellとAWS CLIとCloudFormationを組み合わせるやり方: Gradleの
build.gradle
スクリプトがExec
タスクで外部のシェルスクリプトを実行する。シェルスクリプトawscli-cooked.sh
が
AWS CLIを仲立ちとしてCloudFormationを実行する。 - Gradleのカスタムプラグイン jp.classmethod.awsを使うというやり方:
このプラグインはAWSリソースを操作するGradleタスクをたくさん提供するが、その中にCloudFormationも含まれている。
数日勉強した末に前者の方法つまり有名な4つのツールをうまく組み合わせるやり方が後者のプラグインを使う方法よりも良いという結論に達した。
以下で、まずNeo GOFツールセットについて説明する。そのあとでプラグインに関してわかったことも説明する。
前提条件
- Java 8以上
- Bashシェル。Windows環境では
Git for Windowsをインストールしてそれに同梱されている"Git Bash"を使えばいい. - 自分が使えるAWSアカウント
- 自分が使えるIAM User、十分な権限が付与されていると前提する.
-
AWS CLIがインストール済みかつ設定済みであること。
default
プロファイルが~/.aws/credentials
ファイルの中に作られていて自分が使えるIAMユーザと対応づけられてること - わたしはMacで試した。WindowsやLinuxでも同じように動くはずだが未検証。
プロジェクトの構造
NeoGOFプロジェクトは5つのサブプロジェクトを含むGradleマルチプロジェクトです。
$NeoGOF
├─app
├─subprojectA
├─subprojectB
├─subprojectC
└─subprojectD
コマンドラインでコマンドを投入するときは、まずプロジェクトのルートディレクトリにcdして、それから ./gradlew
コマンドを実行します。例えばrootProjectのbuild.gradleに定義されたhelloタスクを実行すると、サブプロジェクトのbuild.gradleファイルにそれぞれ定義されたhelloタスクが呼び出されます。こんなふうに:
$ cd $NeoGOF
$ ./gradlew -q hello
Hello, app
Hello, subprojectA
Hello, subprojectB
Hello, subprojectB
Hello, subprojectD
Hello, rootProject
もちろんサブプロジェクトのタスクを個別に実行することも可能で、その場合はタスク名を :<subPorjectName>:<taskName>
という形式で指定します。.
例えばこうする。
$ cd $NeoGOF
$ ./gradlew -q :subprojectA:hello
Hello, subprojectA
自分で試すとき注意すべきこと
NeoGOFプロジェクトのReleaseページからZIPファイルをダウンロードして自分の手元で試すことができます。
S3バケット名はグローバルにユニークでなければならない
あなたがNeoGOFプロジェクトのコードを自分のPC上で試すとき、一つだけ修正しなければならない箇所があります。
gradle.properties
ファイルにこんなふうに書いてある。
S3BucketNameA=bb4b24b08c-20200406-neogof-a
これはNeoGOFプロジェクトが作るS3バケットの名前を指定しています。
S3バケットの名前は皆さんご存知の通りグローバルに一意でなければならない。bb4b24b08c
という文字で始まる名前のS3バケットは私のためのものです。これと同じ名前であなたがS3バケットを作ろうとするとエラーになります。だからあなたは gradle.properties
ファイルを修正し、別の名前を与えることが必要です。どんな名前でもいいのですが、先頭のbb4b24b08c
の部分を別の文字列に置換するのでどうでしょうか。
あなたを他の人から識別する文字列を生成するワンライナー
自分のためのS3バケットにグローバルにユニークな名前を与えるために、あなたを他の人から識別する文字列を作り出したい。どうしましょう? 下記に紹介するワンライナーをbashシェルで実行すれば暗号化された(おそらくglobalにユニークな)10文字を生成することができます。あなたのAWSアカウントのIDを入力としてmd5ハッシュして得られたちょっと長い文字列の先頭10個を切り取っています。ただしAWS CLIがインストール済みであることを前提しています。
$ aws sts get-caller-identity --query Account | md5sum | cut -c 1-10
シェルスクリプト をchmod +xで実行可能に
もう一つ注意すべきことがあります。Neo GOFプロジェクトにふたつシェルスクリプトが含まれています。
これらシェルスクリプトが実行可能でなければなりません。もしそうでなかったら下記のようにしてchmodしてください。
$ cd $NeoGOF
$ chmod +x ./awscli-cooked.sh
$ chmod +x ./subprojectD/awscli-cooked.sh
説明その1: Neo GOF ツールセット
NeoGOFプロジェクトをZIPから取り出してあなたのPCの適当なところへ配置してください。
例えばわたしは
/Users/myname/github/NeoGOF
というディレクトリを作りました。以下の説明ではこのディレクトリを $NeoGOF
という記号で短く表記します。
ともかく動かしてみよう
コマンドラインでこうします。
$ cd $NeoGOF
$ ./gradlew -q deploy
うまく行けば下記のようなコンソール出力が得られるでしょう。
neogof-0.1.0.jar has been built
created /Users/myname/github/NeoGOF/subprojectD/build/parameters.json
{
"StackId": "arn:aws:cloudformation:ap-northeast-1:84**********:stack/StackD/99bd96c0-78c9-11ea-b8e1-060319ee749a"
}
Neo GOF project has been deployed
ここで 84**********
と表記したのはわたしのAWSアカウントのIDである12桁の数字です。あなたが自分で試みた場合はここに別の数字12桁が表示されるでしょう。
コマンドラインで ./gradlw deploy
を実行した結果として、appサブプロジェクトのなかでjarファイルが作成されるとともに、下記2つのAWSリソースが割り当てられます。
- S3バケット、 名前は
bb4b24b08c-20200406-neogof-d
- IAMロール、 名前は
NeoGofRoleD
隠れたところでたくさんの処理が行われます。順を追って説明しましょう。
コマンドラインでgradlew deploy
とやってGradleの用語でいうdeploy
タスクを実行しています。このdeploy
タスクは
NeoGOF/build.gradle
の中に記述されています。
task deploy(dependsOn: [
":app:build",
":subprojectD:createStack"
]) {
doLast {
println "Neo GOF project has been deployed"
}
}
このdeploy
タスクはふたつの子タスクを呼び出します。:app:build
と:projectD:createStack
です。そのあとでメッセージを1行出力して終わります。もちろん子タスクを直接、別々に実行することもできます。つまりこうすればいい。
$ cd $NeoGOF
$ ./gradlew :app:build
...
$ ./gradlew :subprojectD/createStack
...
サブプロジェクトapp
は小さなGradleプロジェクトでjava
プラグインを使っています。
app
サブプロジェクトにはJavaクラス
example.Hello
が含まれています。このクラスは
com.amazonaws.services.lambda.runtime.RequestHandler
をimplementしています。ということはexample.Hello
クラスはAWS Lambda関数として動作できる
ように作られています。
app
サブプロジェクトのbuild
タスクはJavaソースをコンパイルしてjarファイルを作ります。app
プロジェクトは典型的なGradleプロジェクトであって珍しいところはすこしもありません。
サブプロジェクト subprojectD
の createStack
タスクはCloudFormationを使ってS3バケットとIAMロールを作ります。
まとめるならば、ルートプロジェクトの deploy
タスクは Gradleに組み込まれた機能(Javaアプリをビルドすること)と拡張としての機能(AWS CloudFormationを駆動すること)をごく自然に組み上げてGradleでコマンド一発で実行できるようしている わけです。
:subprojectD:createStack タスクがどう作られているか
subprojectD/build.gradle
ファイルにおいて createStack
タスクがどのように定義されているのでしょうか?次のように定義されています。
task createStack {
doFirst {
copy {
from "$projectDir/src/cloudformation"
into "$buildDir"
include 'parameters.json.template'
rename { file -> 'parameters.json' }
expand (
bucketName: "${S3BucketNameD}"
)
}
println "created ${buildDir}/parameters.json"
}
doLast {
exec {
workingDir "${projectDir}"
commandLine './awscli-cooked.sh', 'createStack'
}
}
}
createStack
タスクは2つのことをします。
最初にcopy
タスクを実行します。CloudFormationのTemplateに対してパラメータの値を指定するファイルをここで準備しています。
srcディレクトリの下にあるテンプレートを元にbuild/parameters.json
ファイルを作るのですが、テンプレートの中に書かれた$bucketName
のようなシンボルを実際の値に置き換えています。rootProject/gradle.properties
ファイルに書かれた値を取り出しています.
parameter.json
ファイルがどのように準備されるか、具体的にみてみましょう。
テンプレート $projectDir/src/cloudformation/parameters.json.template
は下記のように変数を含んでいます。
[
{
"ParameterKey": "S3BucketName",
"ParameterValue": "${bucketName}"
}
]
代入したい値はGradleの設定ファイル $projectDir/gradle.properties
のなかに書いてあります。
...
S3BucketNameD=bb4b24b08c-20200406-neogof-d
...
さてcopyタスクが実行されると $buildDir/parameters.json
が生成されます。中身はこんなふう。
[
{
"ParameterKey": "S3BucketName",
"ParameterValue": "bb4b24b08c-20200406-neogof-d"
}
]
シェルスクリプト awscli-cooked.sh
に定義された関数 sub_createStack
が$buildDir/parameters.json
ファイルをCloudFormationへ渡します。
このようにしてGradleの世界で決定された設定値をCloudFormationのテンプレートへのパラメータとして引き渡すことができました。いいですね。
次にexec
タスクを実行します。 execタスクはbashスクリプトファイルを awscli-cooked.sh
を実行しますが、サブコマンドとして createStack
を渡しています。
createStackサブコマンドに対応する部分を見てみましょう。
sub_createStack() {
aws cloudformation create-stack --template-body $Template --parameters $Parameters --stack-name $StackName --capabilities CAPABILITY_NAMED_IAM
}
AWS CLIを知っている開発者ならばシェル関数 sub_createStack
が何をするかひと目でわかるでしょう。sub_createStack
はAWS CLIを仲立ちとしてCloudFormationサービスを呼び出し、CloudFormationスタックを作ろうとします。
シェルスクリプト awscli-cooked.sh
には createStack
の他にdeleteStack
、 describeStacks
、 validateTemplate
などいくつかのタスクが定義されています。これらタスクはみな1行だけで、AWS CLIを仲立ちとしてCloudFormationサービスを呼び出しますが、AWS CLIに対するオプションやパラメータをシェルスクリプト のなかで適切に記述することができています。
シンプルでわかりやすいでしょう。いかが?
説明その2: Gradle AWS Plugin
ブラウザでGradle Plugins Repositoryを開き aws
とキーワードを指定して検索すれば、AWSリソースを操作することを目的として開発されたGradleプラグインがいくつもリストアップされます。その中からわたしは jp.classmethod.aws を選択しました。このプラグインについてわたしが試したことを説明します。
subprojectA: S3専用のタスクでS3バケットを作る
コマンドラインで下記のコマンドを投入します。
$ cd $NeoGOF
$ ./gradlew :subprojectA:createBucket
すると私のAWSアカウントにおいて新しいS3バケットが作られました。subprojectA/build.gradle ファイルに createBucketタスクの定義が書いてあります。
I have the following task definition:
task createBucket(type: CreateBucketTask) {
bucketName "${S3BucketNameA}"
region "${Region}"
ifNotExists true
}
CreateBucketTask
とはGradleプラグイン jp.classmethod.aws が提供するクラスです。その名の通りAWS SDK for JavaのS3 APIを利用してS3バケットを作ることだけをするGradleタスクです。
subprojectB: CloudFormation専用のタスクでS3バケットを作る
コマンドラインで下記のコマンドを投入します。
$ cd $NeoGOF
$ ./gradlew :subprojectB:awsCfnMigrateStack
S3バケットがひとつ新しく作られます。
subprojectA/build.gradleファイルに下記のような記述があります。
cloudFormation {
stackName 'StackB'
stackParams([
S3BucketName: "${S3BucketNameB}"
])
capabilityIam true
templateFile project.file("src/cloudformation/B-template.yml")
}
// awsCfnMigrateStack task is provided by the gradle-aws-plugin
// awsCfnDeleteStack task is provided by the gradle-aws-plugin
jp.classmethod.awsによって awsCfnMigrateStack
タスクが提供され実行可能になります。awsCfnMigrateStack
タスクを実行するとCloudFormationスタックが生成され、Templateファイルの記述にしたがって様々なAWSリソースのプロビジョニングが行われます。
src/cloudformation/B-template.yml
ファイルがCloudFormationスタックに与えられるテンプレートです。その中にこう書いてあります。
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${S3BucketName}
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
これはCouldFormationでS3バケットを作る時の標準的なコードです。
subprojectC: CloudFormation専用のタスクでIAMロールを作ろうとして失敗した
コマンドラインで下記のようにやってみましょう。
$ cd $NeoGOF
$ ./gradlew :subprojectC:awsCfnMigrateStack
実際にやってみるとエラーになりました。こんなふうに。
stack subprojectC not found
> Task :subprojectC:awsCfnMigrateStack FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':subprojectC:awsCfnMigrateStack'.
> Requires capabilities : [CAPABILITY_NAMED_IAM] (Service: AmazonCloudFormation; Status Code: 400; Error Code: InsufficientCapabilitiesException; Request ID: c1abb0f1-29c9-4679-9ca1-ccb862ff83f0)
subprojectC/build.gradle
ファイルでエラーが発生したのですが、その中身はsubprojectBA/build.gradle
ファイルの中身とほとんど同じです。
cloudformation {
...
}
ひとつだけ違う点がある。subprojectC/build.gradle
はCloudFormationテンプレートファイル
subprojectC/src/cloudformation/C-template.yml
を参照します。その中にこんな記述がある。
Resources:
NeoGofRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument: ./src/iam/assume-role-policy-document.json
RoleName: NeoGofRoleC
こう書いてあるのでCloudFormationはNameGofRoleC
という名前のIAMロールを作成しようとします。ここでエラーが発生しました。
失敗の原因
なぜ $ ./gradlew :subprojectC:awsCfnMigrateStack
が失敗したのか?メッセージ
Error Code: InsufficientCapabilitiesException
はどういう意味なのか?
実はエラーの原因はすでに知られています。開発プロジェクトのIssuesのなかにこの件が報告されています。
このIssueが作られたのが2016年7月で、4年たった2020年4月現在もまだopenなままであることに注目しましょう。
このGradleプラグインのプロジェクトが発足したのは2016年です。GitHubのHistoryを辿ればわかる。そのあと2017年にAWS CloudFormationの仕様が変わってCAPABILITY_NAMED_IAM
が導入された。その変更にプラグインが追随できていないということが明らかになりました。
jp.classmethod.awsの原作者である miyamoto-daisukeがRDS Instance Supportでこう書いています。
It is hard for me alone to implement all AWS product's feature. So I start to implement the features which I need now. I think that this plugin should have all of useful feature to call AWS API.
Everyone can contribute to add useful features to this plugin. I appreciate your pull-requests.
いわく、自分一人でAWSのproduct全部をカバーすることはできない、みなさんプルリクを寄せてください、と。
広汎なAWSサービスを手広くサポートするのが困難であるのはもちろんのこと、CloudFormationのようにターゲットとして選んだAWSサービスが変化していくのにもプラグインが追随できていない。
結論
Gradle AWS プラグイン jp.classmethod.aws を開発し公開した開発者に対しわたしは感謝し敬意を表明します。しかしこのプラグインはすでに古びています。今後メンテナンスが継続しない可能性が高いとも思う。
そのいっぽうで AWS CLI と CloudFormationがある。AWSがあるかぎりこの2つのプロダクトの開発は継続されるでしょう。Gradleの build.gradle
スクリプトがシェルとCLIを仲立ちとしてCloudFormationを間接的に実行するというやり方は今後もずっと使える方法です。そしてJavaで書かれたLambda関数を実戦配備するために必要なたくさんの作業を全部まとめてGradleからコマンド一発で実行できる。だからNeo GOFツールセットはContinuous Deliveryを実現するのに役立つ強力な道具です。
Neo GOFプロジェクトが提案するbuild.gradle
とシェルスクリプト一式はどれもシンプルです。ビルド不要なスクリプトですからカスタマイズも容易です。