1. 記事一覧 >
  2. ブログ記事
category logo

KotlessとLocalStackで疑似サーバーレス - AWS S3ダウンロード

(更新) (公開)

はじめに

Kotlin のサーバーレスフレームワーク、Kotless を使って実装したアプリケーションを LocalStack へデプロイ →LocalStack の AWS S3 互換機能のオブジェクトをダウンロード とやってみました。


基本的に、参考サイトとGitHub の READMEを見て、やっていきましたが、本物の AWS へデプロイするのではなく、LocalStack へデプロイして、LocalStack の AWS S3 オブジェクトをダウンロードになります。

【 参考サイト 】

https://blog.takehata-engineer.com/entry/deploy-kotlin-applications-to-aws-lambda-using-kotless https://qiita.com/hisayuki/items/b5381409378277320e48


図に示すと、以下のような感じです。
Kotlessを使って実装したアプリケーションをLocalStackへデプロイ→LocalStackのAWS S3互換機能のオブジェクトをダウンロード の図

図中の右向き矢印を本物のAWSに向けると、本物のAWSでも動作すると思いますが、本物では、デプロイ、動作確認していません。

【 LocalStack 】

オンプレ上のAWSのようなものです。疑似的なAWSを構築して、本物のAWSへ接続しなくてもAWSサービスを利用したプログラムの動作確認ができます。AWSサービスの一部、例えば、Cognitoなどは、有料になります。


今回、全て1台の Ubuntu 20.04 LTS で作業しています。

【検証環境】

VMware Workstation Pro 16

 Ubuntu 20.04.2 LTS

  Docker 20.10.14

  openjdk 17.0.3

  Gradle 7.3.3

  Kotlin 1.5.31

  LocalStack 0.11.2


Docker インストール

LocalStack は、docker pull を利用しますので、Docker をインストールします。

# apt update
# apt install -y apt-transport-https ca-certificates curl software-properties-common
# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
# apt update
# apt install -y docker-ce

samba インストール

Ubuntu のファイルを Windows から見えるようにします。(必須ではありません。なんとなく、Windows の共有フォルダからソースコードを見たかったからで、ほとんど意味無いです。)

# apt install samba
# mkdir /home/share
# chmod 777 /home/share
# vi /etc/samba/smb.conf
[share]
path = /home/share/
browsable = yes
writable = yes
guest ok = yes
read only = no

この時点で、Ubuntu の IP アドレスを 192.168.11.6 とすると、\192.168.11.6\share にて、/home/share が見えます。


リモートデスクトップ

Ubuntu 20.04 LTS を VMware で起動していて、使い勝手が悪かったため、リモートデスクトップ接続できるようにしました。(必須ではありません。

# apt install xrdp
# systemctl start xrdp
# systemctl enable xrdp

Authentication is required to create a color managed device と認証を聞かれるのを回避するため、設定を追加します。

Authentication is required to create a color managed device

# vi /etc/polkit-1/localauthority/50-local.d/45-allow-colord.pkla
[Allow Colord all Users]
Identity=unix-user:*
Action=org.freedesktop.color-manager.create-device;org.freedesktop.color-manager.create-profile;org.freedesktop.color-manager.delete-device;org.freedesktop.color-manager.delete-profile;org.freedesktop.color-manager.modify-device;org.freedesktop.color-manager.modify-profile
ResultAny=no
ResultInactive=no
ResultActive=yes

これにて、Ubuntu 20.04 LTS へ Windows からリモートデスクトップ接続ができます。

Ubuntu 20.04 LTSへWindowsからリモートデスクトップ接続

リモートデスクトップ接続しようとしているユーザーが既にデスクトップにログインしている場合、ログアウトが必要です。


JDK インストール

openjdk-17-jdk をインストールします。この後、Gradle が関係してきますが、今回使用する Gradle7.3.3 の適用範囲が version 8 ~ 17 だからです。

# apt install openjdk-17-jdk

IntelliJ Linux

IntelliJ IDEA Community   Linux を下記サイトからダウンロードします。
https://www.jetbrains.com/ja-jp/idea/download/#section=linux

【 IntelliJ IDEA 】

Kotlinに対応した統合開発環境です。今回、プロジェクトのひな形作成、Kotlessプログラム作成に使います。Community版は無料、Ultimate版は有料です。

IntelliJ IDEA Community Linux をダウンロード

スタンドアロンインストールを行います。今回、ダウンロードしたのは、ideaIC-2022.1.tar.gz です。

# tar -xzf ideaIC-2022.1.tar.gz -C /opt
# cd /opt/idea-IC-221.5080.210/bin
# ./idea.sh

スタンドアロンの他に、インストールするアプリを選択できるToolbox App(jetbrains-toolbox-1.23.11849.tar.gz)というのが有るのですが、なぜか、起動しようとしても反応しませんでした。

デスクトップ(リモートデスクトップではない。)にログインして、Terminalアプリで行う必要があるようです。

teratermの場合、以下のエラーになりました。

Startup Error

Unable to detect graphics environment

リモートデスクトップ接続して、Terminalアプリで起動すると、以下のエラーになりました。

java.awt.AWTError: Can't connect to X11 window server using ':10.0' as the value of the DISPLAY variable.

なお、インストールはできませんが、インストール後は、リモートデスクトップで使えました。

Windowsから、\\192.168.11.6\share を参照すればWindows版IntelliJで開発可能かと思ったのですが、プロジェクト作成後、

intellij external file changes sync may be slow

IntelliJ IDEA cannot receive filesystem event notifications for the project. Is it on a network drive?

とエラーになりました。

「IntelliJ IDEA User Agreement」画面がポップアップしてきますので、先に進めます。
「IntelliJ IDEA User Agreement」画面がポップアップ


「Create Desktop Entry...」をクリックして、デスクトップにインストールします。
「Create Desktop Entry...」をクリックして、デスクトップにインストール


一旦閉じて、起動し、

一旦閉じて、起動


「New Project」をクリックします。

「New Project」をクリック


「New Project」をクリック2

Name: のプロジェクト名は任意ですが、ここでは、
kotless-localstack-s3
とします。


Location: は、どこでも良いですが、今回は、先ほど、samba で Windows から見えるようにした場所にします。


JDK は、先ほどインストールした 17 が自動的に選択されています。


Language: Kotlin
Build system: Gradle
Gradle DSL: Kotlin
GroupId: org.example(任意)
ArtifactId: kotless-localstack-s3(任意)
とし、Create をクリックします。


プログラム作成

build.gradle.kts を開くと、最初以下のようになっていますので、https://github.com/JetBrains/kotless の README に従って書き換えます。

build.gradle.ktsを開く


まず、settings.gradle.kts を編集して、プラグインの場所を Gradle に指示する必要があります。

settings.gradle.kts
rootProject.name = "kotless-localstack-s3"

pluginManagement {
    resolutionStrategy {
        this.eachPlugin {
            if (requested.id.id == "io.kotless") {
                useModule("io.kotless:gradle:${this.requested.version}")
            }
        }
    }

    repositories {
        maven(url = uri("https://packages.jetbrains.team/maven/p/ktls/maven"))
        gradlePluginPortal()
        mavenCentral()
    }
}

build.gradle.kts を以下のように書き換えます。

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

plugins {
    //kotlin("jvm") version "1.6.20"
    kotlin("jvm") version "1.5.31" apply true
    id("io.kotless") version "0.2.0" apply true
}

group = "org.example.kotless-localstack-s3"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
    maven(url = uri("https://packages.jetbrains.team/maven/p/ktls/maven"))
}

val awsSdkKotlinVersion: String by project

dependencies {
    testImplementation(kotlin("test"))
    implementation("io.kotless", "kotless-lang", "0.2.0")
    implementation("io.kotless", "kotless-lang-aws", "0.2.0")
    implementation("aws.sdk.kotlin:s3:$awsSdkKotlinVersion")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "1.8"
}

kotless {
    config {
        aws {
            storage {
                bucket = "kotless.s3.example.com"
            }

            profile = "example"
            region = "eu-west-1"
        }
    }
    extensions {
        local {
            port = 8080
            //enables AWS emulation (disabled by default)
            useAWSEmulation = true
        }
    }
}

kotlin("jvm") version "1.5.31" apply true
にしないと、以下の NoSuchMethodError エラーになりました。
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, java.util.List, java.util.List, java.util.List, int, java.lang.Object)'


kotless {
config {
aws {
本物の AWS へのデプロイ用設定ですが、これを書かないと、Task :download_terraformのところで、エラーになりましたので、適当に設定しています。


port = 8080
kotless が待ち受けるポートです。この説明のためにあえて書きましたが、何も書かない場合も 8080 です。

【 Terraform 】

構成ファイルでは、手動で操作することなくインフラ構成を自動で管理できます。今回、gradlewによって、自動でTerraformが起動します。


$awsSdkKotlinVersion
直接値を書いても良いですが、gradle.properties に以下のように設定します。

gradle.properties
kotlin.code.style=official
awsSdkKotlinVersion=0.+

useAWSEmulation = true
LocalStack を使うときに true をセットします。(デフォルトは、falseです。)


IntelliJ の左側ペインから、
New -> Package
org.example.kotless_localstack_s3

New -> Kotlin Class/File -> File
/home/share/kotless-s3-example/src/main/kotlin/org/example/kotless_localstack_s3/Main.kt
を作成し、以下の内容とします。

org/example/kotless_localstack_s3作成


Main.kt作成


src/main/kotlin/org/example/kotless_localstack_s3/Main.kt
package org.example.kotless_localstack_s3

import aws.sdk.kotlin.runtime.endpoint.AwsEndpoint
import aws.sdk.kotlin.runtime.endpoint.AwsEndpointResolver
import aws.sdk.kotlin.services.s3.S3Client
import aws.sdk.kotlin.services.s3.model.GetObjectRequest
import aws.smithy.kotlin.runtime.content.writeToFile
import io.kotless.dsl.lang.http.Get
import kotlinx.coroutines.runBlocking
import java.io.File
import org.slf4j.LoggerFactory

private val logger = LoggerFactory.getLogger("kotless-localstack-s3")

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

const val downloadDirPath = "/tmp/media-down"

@Get("/download")
fun download(bucketName: String = ""): String = getObjects(bucketName)

fun getObjects(bucketName: String): String {
    class LocalHostS3: AwsEndpointResolver {
        override suspend fun resolve(service: String, region: String): AwsEndpoint
                = AwsEndpoint(System.getenv("AWS_ENDPOINT") ?: "http://172.17.0.3:4566")
    }

    runBlocking {
        val client = S3Client {
            region = "us-east-1"
            endpointResolver = LocalHostS3()
        }
        client.use { client ->
            client.listObjects { bucket = bucketName }.contents?.forEach { obj ->
                client.getObject(GetObjectRequest { key = obj.key; bucket = bucketName }) { response ->
                    val outputFile = File(downloadDirPath, obj.key!!)
                    response.body?.writeToFile(outputFile).also { size ->
                        logger.info("Downloaded $outputFile ($size bytes) from S3")
                    }
                }
            }
        }
    }

    return "OK"
}

動作内容は、
# curl http://localhost:8080/download?bucketName=sample-bucket
のようにバケット名を指定して起動されたら、該当の S3 バケットにあるファイルを /tmp/media-down へダウンロードします。


import aws.sdk.kotlin.services.s3.S3Client
aws.sdk.kotlin の Kotlin 用 AWS SDK を使っています。当然、本物の AWS 用ですが、LocalStack にも通用します。


@Get("/")
Kotless DSL(Kotless 独自の作法)の書き方です。他にも、Ktor、Spring Boot の書き方が使えるようですが、ここでは、触れません。


= AwsEndpoint("http://172.17.0.3:4566")
エンドポイント(AWS S3 の URL)を LocalStack のエンドポイントに差し替えています。これを行わないと、本物の方へ行ってしまいます。


runBlocking
listObjectssuspend fun listObjects(非同期関数)なのですが、fun downloadsuspend fun download とするとおかしくなるため、runBlocking により、同期処理に変更しています。


gradlew のパーミッションを調整します。

$ cd /home/share/kotless-localstack-s3
$ chmod 755 gradlew

【 gradlew 】

シェルスクリプトで、いろいろ準備して、gradleをインストールして、実行してくれます。


ビルドできるか確認します。

$ ./gradlew plan

Initializing the backend...
のところで、
Error: error configuring S3 Backend: no valid credential sources for S3 Backend found.
エラーになって止まりますが、これは、本物の AWS S3 の credential が準備されていないからで、今回、LocalStack のため、ここまで来たら、OKとします。


本物の AWS へデプロイするときは、

$ ./gradlew deploy

ですが、今回は、行いません。


LocalStack にデプロイする専用の local オプションが有りますので、それを使って、LocalStack にデプロイします。

内部でdocker pullをしていて、docker利用権限が無いと、エラーになるため、rootで実行しました。

# ./gradlew local
・・・
23:07:26.651 [main] INFO  org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
23:07:26.651 [main] INFO  org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
23:07:26.655 [main] INFO  org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
<===========--> 90% EXECUTING [6m 27s]
> :local

90% EXECUTING まで来ると、起動しています。


動作確認します。

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

ヨシ!


AWS CLI インストール

S3 バケットにあらかじめサンプルファイルを入れるために、aws cli コマンドをインストールします。

# apt install -y awscli
# aws --version
aws-cli/1.18.69 Python/3.8.10 Linux/5.13.0-39-generic botocore/1.16.19

S3 バケットダウンロード

curl http://localhost:8080/download?bucketName=sample-bucket
fun download(bucketName: String = ""): String = getObjects(bucketName) を起動して、LocalStack S3 のバケットをダウンロードします。


LocalStack S3 の
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
をあらかじめ設定しておく必要がありますので、環境変数をセットしてから、起動します。

# export AWS_ACCESS_KEY_ID=dummy-access-key-id
# export AWS_SECRET_ACCESS_KEY=dummy-secret-access-key
# ./gradlew local

AWS プロファイルを作成します。(s3dummy のところは、任意です。)

# aws configure --endpoint-url=http://172.17.0.3:4566 --profile s3dummy
AWS Access Key ID [None]: dummy-access-key-id
AWS Secret Access Key [None]: dummy-secret-access-key
Default region name [None]: us-east-1
Default output format [None]: json

us-east-1 は、 Main.kt に直接書いてありますので、それに合わせます。

エンドポイントの http://172.17.0.3:4566 は、LocalStack dokckerコンテナに割り当てられたIPアドレスです。

# docker container exec -it 2179f4793d31 ifconfig

で調べたIPアドレスです。

http://localhost:4566 でいけるかと思ったのですが、4566 部分がランダムになり、直接アクセスするようにしました。

http://172.17.0.3:4566 と異なる場合、# export AWS_ENDPOINT=http://172.17.0.5:4566と環境変数をセットして起動します。


確認します。

# cat ~/.aws/credentials
[s3dummy]
aws_access_key_id =  dummy-access-key-id
aws_secret_access_key = dummy-secret-access-key
# cat ~/.aws/config
[profile s3dummy]
region = us-east-1
output = json

S3 バケットを作成します。

# aws --endpoint-url=http://172.17.0.3:4566 --profile s3dummy s3api create-bucket --bucket sample-bucket

確認します。

# aws s3 ls --endpoint-url=http://172.17.0.3:4566 --profile s3dummy
2022-04-28 23:11:34 sample-bucket

適当にファイルを作成します。

# vi samplefile.txt
samplefile

sample-bucket にアップロードします。

# aws s3 cp samplefile.txt s3://sample-bucket/ --endpoint-url=http://172.17.0.3:4566 --acl public-read --profile=s3dummy
upload: ./samplefile.txt to s3://sample-bucket/samplefile.txt

確認します。

# aws s3 ls s3://sample-bucket/ --endpoint-url=http://172.17.0.3:4566 --profile s3dummy
2022-04-28 23:14:11          7 sample.txt

ダウンロードします。

LocalStackのAWS S3互換機能のオブジェクトをダウンロード の図

# mkdir /tmp/media-down
# curl http://localhost:8080/download?bucketName=sample-bucket
OK
# cat /tmp/media-down/samplefile.txt
samplefile

ヨシ!!


# ./gradlew local を停止したら、LocalStackコンテナは消滅し、バケットは消えます。


エラーメモ

ビルドで発生したエラーについて、まとめて記述します。

エラーは、以下のコマンドの出力の一部です。

# ./gradlew local --stacktrace


エラー内容:
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, java.util.List, java.util.List, java.util.List, int, java.lang.Object)'


対処内容:
build.gradle.kts を github の README 通りに
kotlin("jvm") version "1.5.31" apply true
に修正。



エラー内容:
2022-04-28T23:00:50.415+0900 [LIFECYCLE] [class org.gradle.internal.buildevents.TaskExecutionLogger] > Task :download_terraform FAILED
> FAILURE: Build failed with an exception


対処内容:
build.gradle.kts
kotless {
config {
aws {
追加。



エラー内容:
> Task :localstack_start FAILED
Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
UnixSocketClientProviderStrategy: failed with exception TimeoutException (Timeout waiting for result with exception). Root cause IOException (native connect() failed : Permission denied)
As no valid configuration was found, execution cannot continue


対処内容:
docker 起動権限が有るユーザー(今回は、root)で起動。



エラー内容:

23:23:37.817 [qtp1586845078-17] ERROR i.k.dsl.app.http.RoutesDispatcher - Failed on call of function download
java.lang.IllegalArgumentException: Callable expects 2 arguments, but 1 were provided.
at kotlin.reflect.jvm.internal.calls.Caller$DefaultImpls.checkArguments(Caller.kt:20)


対処内容:
suspend function download の場合、パラメータを受け取れずにエラーになった。→ suspend 削除



エラー内容:
Suspend function 'listObjects' should be called only from a coroutine or another suspend function


対処内容:
suspend function をやめると、listObjects でエラー。→runBlocking {} を使用。



エラー内容:
> Task :localstack_start FAILED Failure when attempting to lookup auth config (dockerImageName: testcontainers/ryuk:0.3.0, configFile: /root/.docker/config.json. Falling back to docker-java default behaviour. Exception message: /root/.docker/config.json (No such file or directory)


対処内容:
testcontainers/ryuk:0.3.0  を事前に pull。
最初からやり直したら、出なかったので、タイムアウトしただけで、必要無かったかも。

# docker pull testcontainers/ryuk:0.3.0


エラー内容:

Suppressed: aws.sdk.kotlin.runtime.auth.credentials.ProviderConfigurationException: Missing value for environment variable `AWS_ACCESS_KEY_ID` Suppressed: aws.sdk.kotlin.runtime.auth.credentials.ProviderConfigurationException: could not find source profile default Suppressed: aws.sdk.kotlin.runtime.auth.credentials.ProviderConfigurationException: Required field `roleArn` could not be automatically inferred for StsWebIdentityCredentialsProvider. Either explicitly pass a value, set the environment variable `AWS_ROLE_ARN`, or set the JVM system property `aws.roleArn` Suppressed: aws.sdk.kotlin.runtime.auth.credentials.ProviderConfigurationException: Container credentials URI not set Suppressed: aws.sdk.kotlin.runtime.auth.credentials.CredentialsProviderException: failed to load instance profile Caused by: io.ktor.network.sockets.ConnectTimeoutException: Connect timeout has expired [url=http://169.254.169.254:80/latest/api/token, connect_timeout=unknown ms] Caused by: java.net.SocketTimeoutException: Connect timed out


対処内容:
AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY  を事前に環境変数にセット。

# export AWS_ACCESS_KEY_ID=dummy-access-key-id
# export AWS_SECRET_ACCESS_KEY=dummy-secret-access-key
# ./gradlew local


エラー内容:
23:48:46.620 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties' 23:48:46.620 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2 23:48:46.624 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started. 23:48:56.160 [qtp1586845078-17] ERROR a.s.k.r.http.engine.ktor.KtorEngine - throwing java.net.ConnectException: Failed to connect to localhost/127.0.0.1:49186 at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.kt:297) at okhttp3.internal.connection.RealConnection.connect(RealConnection.kt:207)


対処内容:
kotless->LocalStack の S3 接続エラー。ポートがランダムに決まる。→ コンテナの IP アドレスへ直接接続。(例:http://172.17.0.3:4566



エラー内容:
23:39:44.827 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started. 23:41:17.302 [qtp1586845078-17] ERROR i.k.dsl.app.http.RoutesDispatcher - Failed on call of function download aws.sdk.kotlin.services.s3.model.NoSuchBucket: null at aws.sdk.kotlin.services.s3.model.NoSuchBucket$Builder.build(NoSuchBucket.kt:46) at aws.sdk.kotlin.services.s3.transform.NoSuchBucketDeserializer.deserialize(NoSuchBucketDeserializer.kt:20) at aws.sdk.kotlin.services.s3.transform.ListObjectsOperationDeserializerKt.throwListObjectsError(ListObjectsOperationDeserializer.kt:71)


対処内容:

バケットが無い。→ バケット名を正しくするか、該当バケットをあらかじめ作成する。



エラー内容:
23:31:10.821 [qtp1586845078-17] ERROR i.k.dsl.app.http.RoutesDispatcher - Failed on call of function download java.io.FileNotFoundException: /tmp/media-down/samplefile.txt (No such file or directory)


対処内容:

ダウンロード先ディレクトリ作成。

# mkdir /tmp/media-down
loading...