Neo GOF : GradleからCloudFormationを動かす良い方法

このページは https://github.com/kazurayam/NeoGOF のREAMDEドキュメントを日本語で書き直したものです。

<a name="overview" id="overview"></a>
## 概要

GradleとShellとAWS CLIとCloudFormation: この４つのソフトウェアツールをうまく組み合わせて使えばJava/Groovy/Kotlinの開発者の仕事が楽になります。この４つの組み合わせを Neo GOF (new Gang of Four) と名付けます。

![overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52995/4787a0c4-2737-9245-7f37-28fb3a72dcdf.png)



<a name="problem" id="problem"></a>
## 解決すべき問題


1. わたしは One Manプロジェクトつまり独りで全部作るプロジェクトをいくつも持っている。Java/Groovy/Kotlin言語でアプリケーションを書くだけでなく、それを動かすための環境をAWS上に構築するのも自分でやる。分業しない。
2. わたしは **ビルドツール [Gradle](https://gradle.org/)** を使って
[継続的デリバリ](https://martinfowler.com/bliki/ContinuousDelivery.html)
をやりたい。 `$ gradle deploy` とコマンド一発で、JavaやKotlinで書いた
[AWS Lambda](https://aws.amazon.com/lambda/)関数を稼働させるのに必要なすべての作業をGradleによる制御の元で実行したい。
3. Javaアプリケーションをコンパイルしてテストしてjarファイルにまとめる作業をいつものようにGradleでやりたい。
4. Lambda関数として実行するjarファイルをAWS S3バケットに配置する必要があるが、S3バケットを作る作業も自動化したい。
5. Javaコードを修正変更するたびにjarファイルをS3にアップロードし直さなければならない。アップロードの作業を自動化したい。
5. S3にかぎらず様々なAWSリソース (Lambda関数、CloudWatchイベント、 
[IAMロール](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html)、 CloudFront、Route53、AWS CertificateManager、SQS、SNSなど）をプロビジョンするのに**[Cloud Formation](https://aws.amazon.com/jp/cloudformation/)**を使いたい。ただしGradleからCloudFormationを動かしたい。

さてここで問題です。**Gradleの `build.gradle` スクリプトからAWS CloudFormationを動かしたいが、どういう方法を使うのがいいか?** 

<a name="solution" id="solution"></a>
## 解決方法

２つ、やり方を見つけた。

1. GradleとShellとAWS CLIとCloudFormationを組み合わせるやり方: Gradleの`build.gradle`スクリプトが`Exec`タスクで外部のシェルスクリプトを実行する。シェルスクリプト[`awscli-cooked.sh`](./awscli-cooked.sh)が
[AWS CLI](https://aws.amazon.com/cli/)を仲立ちとしてCloudFormationを実行する。
2. Gradleのカスタムプラグイン [jp.classmethod.aws](https://github.com/classmethod/gradle-aws-plugin)を使うというやり方:
このプラグインはAWSリソースを操作するGradleタスクをたくさん提供するが、その中にCloudFormationも含まれている。

数日勉強した末に前者の方法つまり有名な４つのツールをうまく組み合わせるやり方が後者のプラグインを使う方法よりも良いという結論に達した。

以下で、まずNeo GOFツールセットについて説明する。そのあとでプラグインに関してわかったことも説明する。

<a name="prerequisites" id="prerequisites"></a>
## 前提条件

- Java 8以上
- Bashシェル。Windows環境では 
[Git for Windows](https://gitforwindows.org/)をインストールしてそれに同梱されている"Git Bash"を使えばいい.
- 自分が使えるAWSアカウント
- 自分が使えるIAM User、十分な権限が付与されていると前提する.
- [AWS CLI](https://aws.amazon.com/cli/)がインストール済みかつ[設定済み](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)であること。
`default`プロファイルが`~/.aws/credentials`ファイルの中に作られていて自分が使えるIAMユーザと対応づけられてること
- わたしはMacで試した。WindowsやLinuxでも同じように動くはずだが未検証。

<a name="project_structure" id="project_structure"></a>
## プロジェクトの構造

[NeoGOFプロジェクト](https://github.com/kazurayam/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
```

<a name="if_you_want" id="if_you_want"></a>
## 自分で試すとき注意すべきこと

NeoGOFプロジェクトの[Release](https://github.com/kazurayam/NeoGOF/releases)ページからZIPファイルをダウンロードして自分の手元で試すことができます。

<a name="unique_bucket_name" id="unique_bucket_name"></a>
### S3バケット名はグローバルにユニークでなければならない

あなたがNeoGOFプロジェクトのコードを自分のPC上で試すとき、一つだけ修正しなければならない箇所があります。
[`gradle.properties`](./gradle.properties) ファイルにこんなふうに書いてある。

```
S3BucketNameA=bb4b24b08c-20200406-neogof-a
```

これはNeoGOFプロジェクトが作るS3バケットの名前を指定しています。

S3バケットの名前は皆さんご存知の通りグローバルに一意でなければならない。`bb4b24b08c`という文字で始まる名前のS3バケットは私のためのものです。これと同じ名前であなたがS3バケットを作ろうとするとエラーになります。だからあなたは `gradle.properties` ファイルを修正し、別の名前を与えることが必要です。どんな名前でもいいのですが、先頭の`bb4b24b08c`の部分を別の文字列に置換するのでどうでしょうか。

<a name="identity" id="identity"></a>
### あなたを他の人から識別する文字列を生成するワンライナー

自分のためのS3バケットにグローバルにユニークな名前を与えるために、あなたを他の人から識別する文字列を作り出したい。どうしましょう？　下記に紹介するワンライナーをbashシェルで実行すれば暗号化された（おそらくglobalにユニークな）10文字を生成することができます。あなたのAWSアカウントのIDを入力としてmd5ハッシュして得られたちょっと長い文字列の先頭10個を切り取っています。ただしAWS CLIがインストール済みであることを前提しています。

```
$ aws sts get-caller-identity --query Account | md5sum | cut -c 1-10
```

<a name="sh_mode" id="sh_mode"></a>
### シェルスクリプト をchmod +xで実行可能に

もう一つ注意すべきことがあります。Neo GOFプロジェクトにふたつシェルスクリプトが含まれています。
これらシェルスクリプトが実行可能でなければなりません。もしそうでなかったら下記のようにしてchmodしてください。

```
$ cd $NeoGOF
$ chmod +x ./awscli-cooked.sh
$ chmod +x ./subprojectD/awscli-cooked.sh
```

<a name="NeoGOF" id="NeoGOF"></a>
## 説明その1: Neo GOF ツールセット

NeoGOFプロジェクトをZIPから取り出してあなたのPCの適当なところへ配置してください。
例えばわたしは 
`/Users/myname/github/NeoGOF`
というディレクトリを作りました。以下の説明ではこのディレクトリを *`$NeoGOF`* という記号で短く表記します。

<a name="demo" id="demo"></a>
### ともかく動かしてみよう

コマンドラインでこうします。

```
$ 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ファイルが作成されるとともに、下記２つのAWSリソースが割り当てられます。

1. S3バケット、　名前は`bb4b24b08c-20200406-neogof-d`
2. IAMロール、　名前は `NeoGofRoleD`

----

隠れたところでたくさんの処理が行われます。順を追って説明しましょう。

コマンドラインで`gradlew deploy`とやってGradleの用語でいう`deploy`タスクを実行しています。この`deploy`タスクは 
[NeoGOF/build.gradle](./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`](./app/src/main/java/example/Hello.java)
が含まれています。このクラスは
`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でコマンド一発で実行できるようしている* わけです。


<a name="createStack" id="createStack"></a>
### :subprojectD:createStack　タスクがどう作られているか
 
[`subprojectD/build.gradle`](./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` タスクは２つのことをします。

最初に`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`](./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`](./awscli-cooked.sh) には `createStack`の他に`deleteStack`、 `describeStacks`、 `validateTemplate`などいくつかのタスクが定義されています。これらタスクはみな1行だけで、AWS CLIを仲立ちとしてCloudFormationサービスを呼び出しますが、AWS CLIに対するオプションやパラメータをシェルスクリプト のなかで適切に記述することができています。

シンプルでわかりやすいでしょう。いかが？

<a name="gradle_aws_plugin" id="gradle_aws_plugin"></a>
## 説明その2: Gradle AWS Plugin

ブラウザで[Gradle Plugins Repository](https://plugins.gradle.org/)を開き `aws` とキーワードを指定して検索すれば、AWSリソースを操作することを目的として開発されたGradleプラグインがいくつもリストアップされます。その中からわたしは [jp.classmethod.aws](https://plugins.gradle.org/plugin/jp.classmethod.aws)　を選択しました。このプラグインについてわたしが試したことを説明します。

<a name="subprojectA" id="subprojectA"></a>
### subprojectA: S3専用のタスクでS3バケットを作る

コマンドラインで下記のコマンドを投入します。

```
$ cd $NeoGOF
$ ./gradlew :subprojectA:createBucket
```

すると私のAWSアカウントにおいて新しいS3バケットが作られました。[subprojectA/build.gradle](./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](https://plugins.gradle.org/plugin/jp.classmethod.aws) が提供するクラスです。その名の通りAWS SDK for JavaのS3 APIを利用してS3バケットを作ることだけをするGradleタスクです。

<a name="subprojectB" id="subprojectB"></a>
### subprojectB: CloudFormation専用のタスクでS3バケットを作る

コマンドラインで下記のコマンドを投入します。

```
$ cd $NeoGOF
$ ./gradlew :subprojectB:awsCfnMigrateStack
```

S3バケットがひとつ新しく作られます。

[subprojectA/build.gradle](./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](https://plugins.gradle.org/plugin/jp.classmethod.aws)によって `awsCfnMigrateStack` タスクが提供され実行可能になります。`awsCfnMigrateStack`タスクを実行するとCloudFormationスタックが生成され、Templateファイルの記述にしたがって様々なAWSリソースのプロビジョニングが行われます。

[`src/cloudformation/B-template.yml`](./subprojectB/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バケットを作る時の標準的なコードです。

<a name="subprojectC" id="subprojectC"></a>
### 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`](./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ロールを作成しようとします。ここでエラーが発生しました。

<a name="InsufficientCapabilitiesException" id="InsufficientCapabilitiesException"></a>
#### 失敗の原因

なぜ `$ ./gradlew :subprojectC:awsCfnMigrateStack` が失敗したのか？メッセージ 
`Error Code: InsufficientCapabilitiesException`はどういう意味なのか？

実はエラーの原因はすでに知られています。開発プロジェクトのIssuesのなかにこの件が報告されています。

- [Support for CAPABILITY_NAMED_IAM](https://github.com/classmethod/gradle-aws-plugin/issues/50)

このIssueが作られたのが2016年7月で、4年たった2020年4月現在もまだopenなままであることに注目しましょう。

このGradleプラグインのプロジェクトが発足したのは2016年です。GitHubのHistoryを辿ればわかる。そのあと2017年にAWS CloudFormationの仕様が変わって`CAPABILITY_NAMED_IAM`が導入された。その変更にプラグインが追随できていないということが明らかになりました。

[jp.classmethod.aws](https://plugins.gradle.org/plugin/jp.classmethod.aws)の原作者である [miyamoto-daisuke](https://github.com/classmethod/gradle-aws-plugin/issues/188)が[RDS Instance Support](https://github.com/classmethod/gradle-aws-plugin/issues/2)でこう書いています。


>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サービスが変化していくのにもプラグインが追随できていない。

<a name="conclusion" id="conclusion"></a>
## 結論

Gradle AWS プラグイン [jp.classmethod.aws](https://github.com/classmethod/gradle-aws-plugin) を開発し公開した開発者に対しわたしは感謝し敬意を表明します。しかしこのプラグインはすでに古びています。今後[メンテナンスが継続しない可能性が高い](https://github.com/classmethod/gradle-aws-plugin/issues/188)とも思う。

そのいっぽうで AWS CLI と CloudFormationがある。AWSがあるかぎりこの２つのプロダクトの開発は継続されるでしょう。Gradleの `build.gradle` スクリプトがシェルとCLIを仲立ちとしてCloudFormationを間接的に実行するというやり方は今後もずっと使える方法です。そしてJavaで書かれたLambda関数を実戦配備するために必要なたくさんの作業を全部まとめてGradleからコマンド一発で実行できる。だからNeo GOFツールセットはContinuous Deliveryを実現するのに役立つ強力な道具です。

[Neo GOF](https://github.com/kazurayam/NeoGOF)プロジェクトが提案する`build.gradle`とシェルスクリプト一式はどれもシンプルです。ビルド不要なスクリプトですからカスタマイズも容易です。