0
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-08

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

概要

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

overview.png

解決すべき問題

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

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

解決方法

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

  1. GradleとShellとAWS CLIとCloudFormationを組み合わせるやり方: Gradleのbuild.gradleスクリプトがExecタスクで外部のシェルスクリプトを実行する。シェルスクリプトawscli-cooked.shAWS CLIを仲立ちとしてCloudFormationを実行する。
  2. 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リソースが割り当てられます。

  1. S3バケット、 名前はbb4b24b08c-20200406-neogof-d
  2. 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プロジェクトであって珍しいところはすこしもありません。

サブプロジェクト subprojectDcreateStackタスクは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の他にdeleteStackdescribeStacksvalidateTemplateなどいくつかのタスクが定義されています。これらタスクはみな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-daisukeRDS 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とシェルスクリプト一式はどれもシンプルです。ビルド不要なスクリプトですからカスタマイズも容易です。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1