タケハタのブログ

プログラマの生き方、働き方、技術について雑多に書いていくブログです。

KotlessでKotlinのアプリケーションをAWS Lambdaにデプロイする

ちょっと個人で作ろうとしてるアプリケーションでLambdaを使おうとしていて、せっかくなのでKotlessで作ってみようと思っています。
そこで一旦導入してみたので、まとめておきます。

Kotlessとは

JetBrains社純正のKotlinのサーバーレスフレームワークです。

site.kotless.io

最新のリリースバージョンが0.1.6(0.1.7はbeta)なのでまだまだ開発中のものですが、KotlinConfやオンラインイベントでも紹介されていて、バックエンドで注目の技術スタックの一つです。
ざっくり言うと、Kotlinで書いたプログラムをGradleタスクでAWS Lambdaにデプロイしたりできるものです。

LambdaはNode.jsやPythonで書くことが多いと思いますが、「Kotlinで書きたい!」という願いを叶えるものになりますね。
見てもらった方がわかりやすいかと思うので、実際にGradleとKotlinのコードを見ながら解説していきます。

ちなみに今回のサンプルコードは以下に公開しているので、さっと動かして見たい方はこちらもご利用ください。

https://github.com/n-takehata/kotless-examples

検証した環境

調べてる感じ、バージョンによっての問題とかちょこちょこありそうだったので、検証した環境も一応載せておきます。

  • macOS Big sur 11.4(Intel CPU)
  • Docker Desktop for Mac 3.5.2
  • Kotless 0.1.7-beta-5
  • Kotlin 1.4.32

プロジェクトの作成と実装

コードの実装方法を説明していきます。

IntelliJ IDEAでKotlinのプロジェクト作成

まずはIntelliJ IDEAでプロジェクト作成。
詳細は省きますが、以下の画像のような構成で、通常のKotlin Project(GradleはKotlin DSL)を作成してください。

f:id:take7010:20210724134309p:plain

プラグインと依存関係の追加

build.gradle.ktsを以下のような内容に書き換えます。
(groupの名前とかはご自身の環境に合わせて変えてください)

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import io.kotless.plugin.gradle.dsl.kotless

plugins {
    kotlin("jvm") version "1.4.32" apply true // ①バージョンを1.4系に変更
    id("io.kotless") version "0.1.7-beta-5" apply true // ②Kotlessのプラグインを追加
}

group = "com.example.kotless.takehata"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    implementation("io.kotless", "ktor-lang", "0.1.7-beta-5") // ③KotlessのDSLを使用する依存関係を追加(ここではKtor DSL)
    testImplementation(kotlin("test"))
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "11"
}

①バージョンを1.4系に変更

現状使用できる最新であるKotlessの0.1.7-beta-5を使おうとした時、Kotlinを1.5.0以上のバージョンにすると以下のエラーが発生しました。
(ここの関数呼び出しでエラー出てそう)

org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':generate'.
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$3(ExecuteActionsTaskExecuter.java:186)
    at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:268)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:184)

// 省略・・・

Caused by: java.lang.NoSuchMethodError: 'org.jetbrains.kotlin.analyzer.AnalysisResult org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(org.jetbrains.kotlin.com.intellij.openapi.project.Project, java.util.Collection, org.jetbrains.kotlin.resolve.BindingTrace, org.jetbrains.kotlin.config.CompilerConfiguration, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function2, org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope, int, java.lang.Object)'
    at io.kotless.parser.utils.psi.analysis.ResolveUtil.analyze(ResolveUtil.kt:26)
    at io.kotless.parser.utils.psi.analysis.ResolveUtil.analyze$default(ResolveUtil.kt:25)
    at io.kotless.parser.utils.psi.analysis.ResolveUtil.analyze(ResolveUtil.kt:21)
    at io.kotless.parser.utils.psi.analysis.ResolveUtil.analyze(ResolveUtil.kt:17)

なので1.4系の最終である1.4.32に変更しています。

kotlin("jvm") version "1.4.32" apply true

②Kotlessのプラグインを追加

GradleのKotlessプラグインを追加します。
ローカル実行やデプロイなど、後述するKotless関連のタスクを実行するために必要になります。

id("io.kotless") version "0.1.7-beta-5" apply true

③KotlessのDSLを使用する依存関係を追加

Kotlessのコードの実装で必要なDSLの依存関係を追加します。

implementation("io.kotless", "ktor-lang", "0.1.7-beta-5")

Kotlessにはアプリケーションを実装するためのDSLとして

  • Kotless独自の構文(kotless-lang)
  • Ktorの構文(ktor-lang)
  • Spring Bootの構文(spring-boot-lang)

を選択できるようになっていて、ここではKtor構文のDSLであるktor-langを追加しています。
(他の2つについては後述します)

ちなみに②のKotlessプラグインを追加した時、dependenciesでこの3つのどれかの依存関係を追加していないとbuildでエラーになります。
また、DSLは1種類しか使用できないようになっており、2つ以上の依存関係を追加した場合もエラーになります。

アプリケーションを実装

実行するアプリケーションを実装します。
src/main/kotlin配下に適当なパッケージを切り、以下のようなコードを作成してください。

import io.kotless.dsl.ktor.Kotless
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing

class Server : Kotless() {
    override fun prepare(app: Application) {
        app.routing {
            get("/") {
                call.respondText { "Hello World!" }
            }
        }
    }
}

Kotlessクラスを継承し、prepare関数をオーバーライドすることで実装できます。
実装内容としては通常のKtorのAPIと同様で、routingの中にgetなどでパスと処理を定義するだけです。
ここではシンプルにrootパスでGETのリクエストを受け付けて、「Hello World!」の文字列が返ってくるだけのAPIを作成しています。

これが、Lambda上で実行されるアプリケーションになります。

ローカルで起動する

ここまでで一旦動かすプログラムは作れました。
これをLambdaにデプロイする前に、一回ローカル環境で動かしてみます。

実行前にもいくつか事前準備が必要です(この情報があまり書かれていない)。

Dockerのインストール

AWS上での実行をローカルでエミュレートする際、GradleのタスクでDockerを使用します。
ローカル環境にDockerが入っていない場合は、インストールして起動してください。

www.docker.com

ryukのimageをpull

ryukというDockerイメージをpullします。

$ docker pull testcontainersofficial/ryuk:0.3.0

READMEには特に書かれていないのですが、これを自分でpullしておかないと起動時にエラーが発生します。

Caused by: com.github.dockerjava.api.exception.NotFoundException: {"message":"No such image: testcontainersofficial/ryuk:0.3.0"}

LocalStackのインストール

LocalStackのインストールも必要です。

$ brew install localstack

詳しくは公式サイトの方を見ていただきたいですが、AWSの機能をローカル上で実行してテストしたりするための機能(test/mocking framework)を提供するツールですね。
こちらもインストールされていないと、起動時に以下のようなエラーがでます。

org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':localstack_start'.

一応READMEにも以下のような一文がありますが、明示的にはインストールの指示は書いていないので注意です。

During local run, LocalStack will be started and all clients will be pointed to its endpoint automatically.

localタスクを実行して起動

ここまで事前準備ができたら、あとはGradleのlocalタスクを実行するだけです。
IntelliJ IDEAのGradleビューから[Tasks]->[kotless]->[local]を実行、もしくはターミナルから以下のGradleコマンドを実行してください。

$ ./gradlew local

そして次のようなログが出力されていれば、起動成功です。

INFO  application:39 - Responding at http://0.0.0.0:8080

8080ポートで先ほど作成したアプリケーションが起動されました。
ブラウザやcurlコマンドでhttp://localhost:8080にアクセスすると、「Hello World!」の文字列が返ってきます。

$ curl http://localhost:8080
Hello World!

AWS上での起動

それでは作ったアプリケーションをAWS上にデプロイしてみます。
もしAWSのアカウントを持っていない場合は、作成してください。

デプロイの流れ

Kotlessでのデプロイの流れを簡単に説明すると、AWSの構成を定義するTerraformのコードを生成し、そこから環境を作成しアプリケーションがデプロイされます。 その際に、Terraformのtfstateファイル(後述)とアプリケーションのjarファイルがS3にアップロードされます。

そのため、事前にAWS上にS3のバケットを作成しておく必要があります。

S3のバケットを作成

以下のURLにアクセスし、右側の「バケットを作成」ボタンを押します。

https://s3.console.aws.amazon.com/s3/home

f:id:take7010:20210724134747p:plain

そして以下のように適当なバケット名と、使用したいリージョンを選択し、画面の下の方にある「バケットを作成」ボタンを押します。

f:id:take7010:20210724134810p:plain f:id:take7010:20210724134825p:plain

S3のホームに戻り、リストに作成したバケットの名前が追加されていれば成功です。

f:id:take7010:20210724134847p:plain

AWS CLIのクレデンシャルを取得

すでにAWS CLIをローカルで使用したことがある方は、$HOME/.aws/credentialsで使用したいprofileの名前とregionを確認してください。
もし使用したことがない方は、以下のページを参考にIAMユーザーやprofileの作成などをしてください。

docs.aws.amazon.com

credentailsには以下のような情報が記述されています。

[default]
aws_access_key_id=XXXXXXX
aws_secret_access_key=YYYYYYY
region=us-west-2

[profile user1]
aws_access_key_id=XXXXXXX
aws_secret_access_key=YYYYYYY
region=us-east-1

今回必要なのは[]内にあるprofile名(ここではdefalt、もしくはuser1)、そしてその下にあるregionの情報です。
次の「build.gradle.ktsにAWSの構成を追加」の内容で必要になります。

build.gradle.ktsにAWSの構成を追加

build.gradle.ktsに以下のkotlessブロックを追加します。

kotless {
    config {
        // ①作成したS3のバケットの名前を指定
        bucket = "kotless-example-takehata"

        // ②$HOME/.aws/credentialsの情報を指定
        terraform {
            profile = "default"
            region = "us-west-2"
        }
    }

    webapp {
        lambda {
            memoryMb = 1024
            timeoutSec = 120
        }
    }

    // ③destroyタスクを有効化
    extensions {
        terraform {
            allowDestroy = true
        }
    }
}

AWSにデプロイするための情報などを設定しています。

①作成したS3のバケットの名前を指定

まずconfigブロックの中のbucketで、先ほど作成したS3のバケットの名前を指定します。

bucket = "kotless-example-takehata"

前述の通り、このバケットにtfstateファイルがアップロードされることになります。

②$HOME/.aws/credentialsの情報を指定

次に、こちらも先程確認した$HOME/.aws/credentialsの情報を、terraformブロックの中で指定します。

terraform {
    profile = "default"
    region = "us-west-2"
}

本当はこれ用のprofileを作成して使用した方が良いですが、今回は面倒だったので私の環境では一旦defaultを使用しました。

③destroyタスクを有効化

これは構成とは直接関係ありませんが、作成したAWSのリソースを削除するための、destroyタスク(Terafformのdestroyコマンドが実行されます)を有効にしています。

extensions {
    terraform {
        allowDestroy = true
    }
}

この記述を入れないと、destroyタスクは使用できません。

planを実行

ここからGradleタスクを実行して、Lambdaへのデプロイを進めていきます。
まずはplanタスクを実行します。

これはTerraformのplanコマンドを実行していて、現在の構成を適用した時にどのような動作が行われるのかを確認できます。

IntelliJ IDEAのGradleビューから[Tasks]->[kotless]->[plan]を実行、もしくはターミナルから以下のGradleコマンドを実行してください。

$ ./gradlew plan

長いので省いて載せますが、以下のような出力になります。

  # aws_api_gateway_deployment.root will be created
  + resource "aws_api_gateway_deployment" "root" {
      // ・・・
    }

  # aws_api_gateway_integration.get will be created
  + resource "aws_api_gateway_integration" "get" {
      // ・・・
    }

  # aws_api_gateway_method.get will be created
  + resource "aws_api_gateway_method" "get" {
      // ・・・
    }

  # aws_api_gateway_rest_api.kotless_example_ktor will be created
  + resource "aws_api_gateway_rest_api" "kotless_example_ktor" {
      // ・・・
    }

  # aws_cloudwatch_event_rule.autowarm_get will be created
  + resource "aws_cloudwatch_event_rule" "autowarm_get" {
      // ・・・
    }

  # aws_cloudwatch_event_target.autowarm_get will be created
  + resource "aws_cloudwatch_event_target" "autowarm_get" {
      // ・・・
    }

  # aws_iam_role.get will be created
  + resource "aws_iam_role" "get" {
      // ・・・
    }

  # aws_iam_role.kotless_static_role will be created
  + resource "aws_iam_role" "kotless_static_role" {
      // ・・・
    }

  # aws_iam_role_policy.get will be created
  + resource "aws_iam_role_policy" "get" {
      // ・・・
    }

  # aws_iam_role_policy.kotless_static_policy will be created
  + resource "aws_iam_role_policy" "kotless_static_policy" {
      // ・・・
    }

  # aws_lambda_function.get will be created
  + resource "aws_lambda_function" "get" {
      // ・・・
    }

  # aws_lambda_permission.autowarm_get will be created
  + resource "aws_lambda_permission" "autowarm_get" {
      // ・・・
    }

  # aws_lambda_permission.get will be created
  + resource "aws_lambda_permission" "get" {
      // ・・・
    }

  # aws_s3_bucket_object.get will be created
  + resource "aws_s3_bucket_object" "get" {
      // ・・・
    }

Plan: 14 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

BUILD SUCCESSFUL in 53s

新規作成なので、

14 to add, 0 to change, 0 to destroy.

と追加だけ行われているのが確認できますね。
planの内容を確認して問題なければ、いよいよAWS上へのデプロイを実行します。

deployを実行

デプロイにはdeployタスクを使用します。
これはTerraformのapplyコマンドを実行していて、Terraformで記述したコードを元に、リソースを作成していきます。

IntelliJ IDEAのGradleビューから[Tasks]->[kotless]->[deploy]を実行、もしくはターミナルから以下のGradleコマンドを実行してください。

$ ./gradlew deploy

デプロイが実行され、以下のようなログが出力されます。

Initializing the backend...

Initializing provider plugins...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
data.aws_s3_bucket.kotless_bucket: Refreshing state...
data.aws_caller_identity.current: Refreshing state...
data.aws_region.current: Refreshing state...
data.aws_iam_policy_document.kotless_static_assume: Refreshing state...
data.aws_iam_policy_document.get_assume: Refreshing state...
data.aws_iam_policy_document.get: Refreshing state...
data.aws_iam_policy_document.kotless_static_policy: Refreshing state...
aws_iam_role.get: Creating... [40s]
aws_iam_role.kotless_static_role: Creating...
aws_cloudwatch_event_rule.autowarm_get: Creating...
aws_api_gateway_rest_api.kotless_example_ktor: Creating...
aws_s3_bucket_object.get: Creating...
aws_cloudwatch_event_rule.autowarm_get: Creation complete after 2s [id=autowarm-get]
aws_iam_role.get: Creation complete after 2s [id=get]
aws_iam_role.kotless_static_role: Creation complete after 2s [id=kotless-static-role]
aws_iam_role_policy.get: Creating...
aws_iam_role_policy.kotless_static_policy: Creating...
aws_api_gateway_rest_api.kotless_example_ktor: Creation complete after 3s [id=8443r74qk8]
aws_api_gateway_method.get: Creating...
aws_api_gateway_method.get: Creation complete after 0s [id=agm-8443r74qk8-ad3mxxsox2-GET]
aws_iam_role_policy.get: Creation complete after 2s [id=get:terraform-20210724002903793400000001]
aws_iam_role_policy.kotless_static_policy: Creation complete after 2s [id=kotless-static-role:terraform-20210724002903793800000002]
aws_s3_bucket_object.get: Still creating... [10s elapsed]
aws_s3_bucket_object.get: Creation complete after 19s [id=kotless-lambdas/get.jar]
aws_lambda_function.get: Creating...
aws_lambda_function.get: Creation complete after 8s [id=get]
aws_lambda_permission.get: Creating...
aws_lambda_permission.autowarm_get: Creating...
aws_api_gateway_integration.get: Creating...
aws_cloudwatch_event_target.autowarm_get: Creating...
aws_cloudwatch_event_target.autowarm_get: Creation complete after 1s [id=autowarm-get-terraform-20210724002928940500000003]
aws_lambda_permission.autowarm_get: Creation complete after 1s [id=autowarm-get]
aws_api_gateway_integration.get: Creation complete after 1s [id=agi-8443r74qk8-ad3mxxsox2-GET]
aws_api_gateway_deployment.root: Creating...
aws_api_gateway_deployment.root: Creation complete after 2s [id=j30w3f]
aws_lambda_permission.get: Creation complete after 7s [id=get]

Apply complete! Resources: 14 added, 0 changed, 0 destroyed.

Outputs:

application_url = https://8443r74qk8.execute-api.us-west-2.amazonaws.com/1

BUILD SUCCESSFUL in 1m 16s

最後にapplication_url = https://8443r74qk8.execute-api.us-west-2.amazonaws.com/1というログが出力されていますが、これがデプロイしたアプリケーションのエンドポイントになります。
ここにブラウザやcurlコマンドでアクセスすると、「Hello World!」が返ってきます。

$ curl https://8443r74qk8.execute-api.us-west-2.amazonaws.com/1
Hello World!

これでKotlessで作成したアプリケーションをAWS Lambdaで動かすことができました。

AWS上のリソースを確認してみる

ここまで実行したタスクで、AWS上にどのようなリソースが作られているのか確認します。

まずはS3です。
S3のバケットの一覧から、今回作成しbuild.gradle.ktsで指定していたバケット(サンプルではkotless-example-takehata)を選択すると、kotless-lambdaskotless-stateというディレクトリが作成されています。

f:id:take7010:20210724134946p:plain

kotless-stateを選択すると、中にstate.tfstateというファイルが作成されています。
前述の通り、tfstateファイルがS3にアップロードされているのが分かります。

f:id:take7010:20210724135046p:plain

現状の構成の状態を保持したファイルです。
Terraformはこのファイルの情報を元に、planやdeployで差分を検出し、リソースの変更を行います。

www.terraform.io

kotless-lambdasを選択すると、get.jarというファイルが作られています。
これはデプロイされるアプリケーションのjarファイルです。

f:id:take7010:20210724135059p:plain

そしてLambdaのホームから左側のメニューで「関数」を選択すると、以下のようにgetという関数が作成されているのがわかります。

f:id:take7010:20210724135147p:plain

そして選択して中身を確認すると、作成したアプリケーション(HelloWorld!を返すだけのもの)が関数として作成され、そのトリガーとなるAPI Gatewayも作成されています。

f:id:take7010:20210724135216p:plain

「API Gateway」を選択し、トリガーの下にある「詳細」を押すと、以下のような内容が表示されます。

f:id:take7010:20210724135527p:plain

「APIエンドポイント」のところを見ると、前述のdeployタスク実行時に出力されていたapplication_urlの値で作られていることが分かります。

リソースを削除する

リソースを削除する時は、build.gradle.ktsで有効にしたdestroyタスクを使用します。
これはTerraformのdestroyコマンドを実行していて、Terraformで作成した当該のリソースを削除します。

IntelliJ IDEAのGradleビューから[Tasks]->[kotless]->[destroy]を実行、もしくはターミナルから以下のGradleコマンドを実行してください。

$ ./gradlew destroy

削除が実行され、以下のようなログが出力されます。

Initializing the backend...

Initializing provider plugins...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
aws_cloudwatch_event_rule.autowarm_get: Refreshing state... [id=autowarm-get]
data.aws_region.current: Refreshing state...
aws_s3_bucket_object.get: Refreshing state... [id=kotless-lambdas/get.jar]
data.aws_iam_policy_document.kotless_static_assume: Refreshing state...
data.aws_caller_identity.current: Refreshing state...
data.aws_s3_bucket.kotless_bucket: Refreshing state...
data.aws_iam_policy_document.get_assume: Refreshing state...
aws_api_gateway_rest_api.kotless_example_ktor: Refreshing state... [id=8443r74qk8]
aws_iam_role.kotless_static_role: Refreshing state... [id=kotless-static-role]
aws_iam_role.get: Refreshing state... [id=get]
data.aws_iam_policy_document.get: Refreshing state...
aws_iam_role_policy.get: Refreshing state... [id=get:terraform-20210724002903793400000001]
aws_lambda_function.get: Refreshing state... [id=get]
aws_api_gateway_method.get: Refreshing state... [id=agm-8443r74qk8-ad3mxxsox2-GET]
data.aws_iam_policy_document.kotless_static_policy: Refreshing state...
aws_iam_role_policy.kotless_static_policy: Refreshing state... [id=kotless-static-role:terraform-20210724002903793800000002]
aws_lambda_permission.get: Refreshing state... [id=get]
aws_cloudwatch_event_target.autowarm_get: Refreshing state... [id=autowarm-get-terraform-20210724002928940500000003]
aws_api_gateway_integration.get: Refreshing state... [id=agi-8443r74qk8-ad3mxxsox2-GET]
aws_lambda_permission.autowarm_get: Refreshing state... [id=autowarm-get]
aws_api_gateway_deployment.root: Refreshing state... [id=j30w3f]
aws_iam_role_policy.kotless_static_policy: Destroying... [id=kotless-static-role:terraform-20210724002903793800000002]
aws_api_gateway_method.get: Destroying... [id=agm-8443r74qk8-ad3mxxsox2-GET]
aws_lambda_permission.get: Destroying... [id=get]
aws_lambda_permission.autowarm_get: Destroying... [id=autowarm-get]
aws_iam_role_policy.get: Destroying... [id=get:terraform-20210724002903793400000001]
aws_cloudwatch_event_target.autowarm_get: Destroying... [id=autowarm-get-terraform-20210724002928940500000003]
aws_api_gateway_deployment.root: Destroying... [id=j30w3f]
aws_api_gateway_method.get: Destruction complete after 0s
aws_cloudwatch_event_target.autowarm_get: Destruction complete after 0s
aws_iam_role_policy.get: Destruction complete after 0s
aws_iam_role_policy.kotless_static_policy: Destruction complete after 0s
aws_iam_role.kotless_static_role: Destroying... [id=kotless-static-role]
aws_lambda_permission.autowarm_get: Destruction complete after 1s
aws_cloudwatch_event_rule.autowarm_get: Destroying... [id=autowarm-get]
aws_api_gateway_deployment.root: Destruction complete after 1s
aws_api_gateway_integration.get: Destroying... [id=agi-8443r74qk8-ad3mxxsox2-GET]
aws_cloudwatch_event_rule.autowarm_get: Destruction complete after 0s
aws_api_gateway_integration.get: Destruction complete after 0s
aws_lambda_permission.get: Destruction complete after 2s
aws_api_gateway_rest_api.kotless_example_ktor: Destroying... [id=8443r74qk8]
aws_lambda_function.get: Destroying... [id=get]
aws_iam_role.kotless_static_role: Destruction complete after 2s
aws_lambda_function.get: Destruction complete after 0s
aws_iam_role.get: Destroying... [id=get]
aws_s3_bucket_object.get: Destroying... [id=kotless-lambdas/get.jar]
aws_api_gateway_rest_api.kotless_example_ktor: Destruction complete after 1s
aws_s3_bucket_object.get: Destruction complete after 1s
aws_iam_role.get: Destruction complete after 2s

Destroy complete! Resources: 14 destroyed.

BUILD SUCCESSFUL in 42s

再びLambdaのページで確認すると、先ほど確認した関数の情報がなくなっていると思います。
これで一通りの操作の説明も終わりです。

なお、各タスクについては以下のページも参考に見ていただけると良いと思います。

site.kotless.io

その他のDSLを使ってみる

最後に他のKotless DSL(kotless-lang)とSpring Boot DSL(spring-boot-lang)の書き方も簡単に紹介します。
local、plan、deployといったタスクの実行方法は同じなので、build.gradle.ktsとアプリケーションのコードだけ変更します。

Kotless DSL

Kotlessの独自DSLはbuild.gradle.ktsで以下のように定義します。

import io.kotless.plugin.gradle.dsl.kotless
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.4.32" apply true
    id("io.kotless") version "0.1.7-beta-5" apply true
}

group = "com.example.kotless.takehata"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    implementation("io.kotless", "kotless-lang", "0.1.7-beta-5")
    testImplementation(kotlin("test"))
}

kotless {
    config {
        bucket = "kotless-example-takehata"

        terraform {
            profile = "default"
            region = "us-west-2"
        }
    }

    webapp {
        lambda {
            kotless {
                packages = setOf("com.example.kotless")
            }
            memoryMb = 1024
            timeoutSec = 120
        }
    }

    extensions {
        terraform {
            allowDestroy = true
        }
    }
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "11"
}

dependenciesでkotless-langを追加しています。
そしてKotless DSLではwebapp.lambdaに以下の記述が必要です。

kotless {
    packages = setOf("com.example.kotless")
}

これはアプリケーションのコードが配置されているパッケージを設定しています(各自の環境に合わせて変更してください)。
これを書いておかないと、配置したアプリケーションの関数として認識してもらえません。

そしてアプリケーションは以下のように、@GetなどのKotlessのアノテーションを付けた関数を定義するだけです。

import io.kotless.dsl.lang.http.Get

@Get("/")
fun main() = "Hello world!"

アノテーションはKtorのものと似たような感じで、パスを引数に渡して指定します。

Spring Boot DSL

Spring Boot DSLはbuild.gradle.ktsで以下のように定義します。

import io.kotless.plugin.gradle.dsl.kotless
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.4.32" apply true
    id("io.kotless") version "0.1.7-beta-5" apply true
}

group = "com.example.kotless.takehata"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    implementation("io.kotless", "spring-boot-lang", "0.1.7-beta-5")
    testImplementation(kotlin("test"))
}

kotless {
    config {
        bucket = "kotless-example-takehata"

        terraform {
            profile = "default"
            region = "us-west-2"
        }
    }

    webapp {
        lambda {
            memoryMb = 1024
            timeoutSec = 120
        }
    }

    extensions {
        terraform {
            allowDestroy = true
        }
    }
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "11"
}

dependenciesでspring-boot-langを追加しています。
Ktor DSLとの差分はここだけですね。

そしてアプリケーションのコードは以下のように実装します。

import io.kotless.dsl.spring.Kotless
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import kotlin.reflect.KClass

@SpringBootApplication
open class Application : Kotless() {
    override val bootKlass: KClass<*> = this::class
}

@RestController
object Main {
    @GetMapping("/")
    fun main() = "Hello World!"
}

Kotlessクラスを継承したクラスに、Spring Bootの@SpringBootApplicationを付与します。
そしてControllerのobjectと関数は、通常のSpring Bootのアプリケーションと同じように記述します。

IDE上からKotlinで書いてデプロイまでできるのは楽

まだまだ開発中のフレームワークですが、アプリケーションもTerraformのコードもKotlinで書ける(GradleもKotlin DSL)のはいいですね。
Kotlinエンジニアによくいる「全部Kotlinで書きたい!」という人にもぴったりです笑
ちょっと作るだけならIntelliJ IDEAで全部書いて、タスク実行するだけで完結するので、とても楽でした。

【宣伝】サーバーサイドKotlinの書籍を発売しています!

私の執筆した書籍「Kotlin サーバーサイドプログラミング実践開発」が発売中です!

gihyo.jp

タイトル通り実践での開発にも持っていける内容になっているので、サーバーサイドKotlinを始めてみたい方はぜひお手にとっていただければと思います。