- 記事一覧 >
- ブログ記事
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
図中の右向き矢印を本物の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
と認証を聞かれるのを回避するため、設定を追加します。
# 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 からリモートデスクトップ接続ができます。
リモートデスクトップ接続しようとしているユーザーが既にデスクトップにログインしている場合、ログアウトが必要です。
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版は有料です。
スタンドアロンインストールを行います。今回、ダウンロードしたのは、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」画面がポップアップしてきますので、先に進めます。
「Create Desktop Entry...」をクリックして、デスクトップにインストールします。
一旦閉じて、起動し、
「New Project」をクリックします。
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 に従って書き換えます。
まず、settings.gradle.kts
を編集して、プラグインの場所を Gradle に指示する必要があります。
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
を以下のように書き換えます。
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
に以下のように設定します。
kotlin.code.style=official
awsSdkKotlinVersion=0.+
useAWSEmulation = true
LocalStack を使うときに true
をセットします。(デフォルトは、false
です。)
IntelliJ の左側ペインから、
New -> Packageorg.example.kotless_localstack_s3
↓
New -> Kotlin Class/File -> File/home/share/kotless-s3-example/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
listObjects
が suspend fun listObjects
(非同期関数)なのですが、fun download
を suspend 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
ダウンロードします。
# 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
その他、宣伝、誹謗中傷等、当方が不適切と判断した書き込みは、理由の如何を問わず、投稿者に断りなく削除します。
書き込み内容について、一切の責任を負いません。
このコメント機能は、予告無く廃止する可能性があります。ご了承ください。
コメントの削除をご依頼の場合はTwitterのDM等でご連絡ください。