雑記
どうでもいい仕事の話
3月からずっと自宅勤務です。土日に自分の仕事をこなして、平日はダラダラとコードレビューとかに使う業務パターンが完全に定着しました。
自分にとってコードレビューはかなり精神の負荷の高い作業で、例えば↓みたいな感じで心が乱高下します。
- 同僚のきれいなコードを見て凹む
- レガシーシステムのクソコードを見て憤る
- ベンダーのクソコードに絶望
- 新入社員のコードを見て今後の教育方針に悩む
- etc...
で、大体コードレビューと打ち合わせと設計レビュー等々で結構時間を使うのですが、自宅だとなんか気持ちの切り替えができなくて自分の仕事が停滞するようになったので、もうこれは平日は自分の仕事をすっぱり諦めることにしました。土日はSlack見なくていいしオンラインミーティングもないし捗りますなあ。
もっとどうでもいい話
デレマスのボイスオーディションで「三好紗南」「綾瀬穂乃香」「ライラさん」に投票したけど実らなかったなあ。紗南ちゃんはワンチャンあると思ってたけど。来年も多分同じ人に入れます。 社長はびっくりした。
総選挙は「水本ゆかり」「北条加蓮」「依田芳乃」に投票しました。加蓮みたいにトップになることを公然と表明して、実践しているアイドルが勝利を掴むのを見るのは気持ちが良い。ゆかりさんは2期連続で圏内に入ったから、フェス限でも実装してくれないかしら
serverless-artilleryでIP制限のあるAPIの負荷テストを行う
仕事で負荷テストツールの選定をしているので、得た知見をブログに書いていこうと思います。
ちなみに、今まで負荷テストは人任せで、ツールはほとんど触ったことがありませんでした 😑
serverless-artillery
serverless-artillery は負荷テストツールのArtillery を Serverless Framework を使ってAWS LambdaにDeployしてくれるプロジェクトです。 負荷テストを実行する際にテストサーバのスケールを考慮しなくて良くなり、簡単に大量リクエストのテストを実行することができます。
日本語の記事だと、こちらに詳しい解説が載っていて、ものすごく参考になりました。
サーバレス時代の負荷テスト戦略 〜CircleCIで実現する継続的負荷テストとチューニングTips〜 - Qiita
で、公開Webサービスなら問題ないのですが、IP Addressのアクセス制限をしているWebアプリケーション (開発中のAPIとか、社内からのアクセス限定のAPIとか) をテストする場合はLambdaからのアクセスを許容してあげる必要があります。
1. VPC内にLambdaをDeployする
VPC内のPrivate subnetにLambdaを配置して、NAT-Gatewayに設定したElastic IPからのアクセスを許可します。 この場合、Subnetに割り当てたIP Address数までしかスケールできないので、せっかくLambdaを使っているのになあ感があって正直あんまりやりたくありません😐
serverless-artilleryをVPC内にデプロイする場合、serverless.ymlに設定します。
# ... 前略 functions: loadGenerator: # !!Do not edit this name!! handler: handler.handler # the serverlessArtilleryLoadTester handler() method can be found in the handler.js source file timeout: 300 # set timeout to be 5 minutes (max for Lambda) # ここに設定する vpc: # Optional VPC. But if you use VPC then both subproperties (securityGroupIds and subnetIds) are required securityGroupIds: - 'sg-123456789' subnetIds: - 'subnet-123456789' - 'subnet-987654321' # 後略 ...
2. AWSのLambdaからのアクセスを一時的に許可する
テストのときだけLambda (EC2) のIP Rangesからのアクセスを許容するようにします。 この場合、継続的なテストができない点が課題になります。また、一時的にIP制限が緩くなることを許容する必要があります。
terraformを利用する場合、簡単にSecurity Groupを作成できます。
data "aws_ip_ranges" "tokyo_ec2" { regions = ["ap-northeast-1"] services = ["ec2"] } resource "aws_security_group" "from_tokyo" { name = "from-tokyo" vpc_id = local.vpc_id ingress { # httpsなら443 from_port = "80" to_port = "80" protocol = "tcp" cidr_blocks = data.aws_ip_ranges.tokyo_ec2.cidr_blocks ipv6_cidr_blocks = data.aws_ip_ranges.tokyo_ec2.ipv6_cidr_blocks } description = "from-tokyo" tags = { CreateDate = data.aws_ip_ranges.tokyo_ec2.create_date SyncToken = data.aws_ip_ranges.tokyo_ec2.sync_token } }
あとはターゲットのLoad Balancerなりに作成したSecurity groupを設定すればOKです。 IP Rangesは不定期に更新されるので、テストのたびにsecurity groupを最新化する必要があります。
まとめ
ある程度のセキュリティリスクを許容できるなら、EC2からのアクセスを許容するのが良いと思います。
許容できないなら、制限のある環境でLambdaを使うよりも、Locustを利用してdocker-composeでslaveを立てる方が良いかなー 🤔
参考
サーバレス時代の負荷テスト戦略 〜CircleCIで実現する継続的負荷テストとチューニングTips〜 - Qiita
AWS: aws_ip_ranges - Terraform by HashiCorp
Serverless Framework - AWS Lambda Guide - Serverless.yml Reference
Terraformでターゲット追跡スケーリングポリシーのAuto Scalingを実装する
AWSメモ記事でゲス。
AWSのECSにおけるオートスケールは、EC2インスタンスのオートスケールと、インスタンス内のコンテナのオートスケールのコンテキストがありますが、
これはFARGATEでの運用を考えて書いたものなのでコンテナをターゲットにしています。参考:Service Auto Scaling
locals { cluster_name = "my-server" service_name = "my-server" // service/cluster-name/service-name resource_id = "service/my-server/my-server" } # autoscaleするターゲットを決める # https://www.terraform.io/docs/providers/aws/r/appautoscaling_target.html#ecs-service-autoscaling resource "aws_appautoscaling_target" "my_server_target" { service_namespace = "ecs" resource_id = local.resource_id scalable_dimension = "ecs:service:DesiredCount" role_arn = data.aws_iam_role.ecs_service_autoscaling.arn min_capacity = 1 max_capacity = 5 # 変更はGUIのほうがやりやすそうなので lifecycle { ignore_changes = [min_capacity, max_capacity] } } # https://www.terraform.io/docs/providers/aws/r/appautoscaling_policy.html#ecs-service-autoscaling # https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-auto-scaling.html resource "aws_appautoscaling_policy" "scale_policy" { name = "my-server-scale-policy" service_namespace = "ecs" resource_id = local.resource_id scalable_dimension = "ecs:service:DesiredCount" policy_type = "TargetTrackingScaling" target_tracking_scaling_policy_configuration { predefined_metric_specification { predefined_metric_type = "ECSServiceAverageCPUUtilization" } # CPU使用率 50 を維持する感じ target_value = 50 scale_out_cooldown = 300 scale_in_cooldown = 300 } depends_on = [aws_appautoscaling_target.my_server_target] } data "aws_iam_role" "ecs_service_autoscaling" { name = "AWSServiceRoleForApplicationAutoScaling_ECSService" }
参考
サービスの Auto Scaling - Amazon Elastic Container Service
AWS: aws_appautoscaling_target - Terraform by HashiCorp
AWS: aws_appautoscaling_policy - Terraform by HashiCorp
AWS Fargate Scaling with Target Tracking Policy - Kiran Gekkula - Medium
PHP (CodeIgniter), Apache のProductをDocker Imageにする
ぼくのかんがえたさいきょうのJib
現時点で考えたさいきょうのJib設定をメモしておきます。1週間の勉強での結論なのですぐに変わりそうですが 😑
コード内の、どうすっかなーみたいなコメントでお察しください。
Jibの記事は一旦これで終了にする予定です。
いままでの記事と同じく、multi-project構成のSpring Boot Applicationを対象とします。前回、前々回を参照ください。JibのVersionは2.0.0を対象としています。
// build.gradle.kts plugins { id("org.springframework.boot") version "2.3.0.M1" apply false id("io.spring.dependency-management") version "1.0.9.RELEASE" id("java") kotlin("jvm") version "1.3.61" kotlin("plugin.spring") version "1.3.61" // rootにはapply falseをつけて、main classがあるsubProjectに再度設定する id("com.google.cloud.tools.jib") version "2.0.0" apply false } // web/build.gradle.kts import org.springframework.boot.gradle.tasks.bundling.BootJar plugins { id("org.springframework.boot") id("io.spring.dependency-management") id("com.google.cloud.tools.jib") } dependencies { implementation(project(":docker-app-env")) implementation("org.springframework.boot:spring-boot-starter-webflux:2.3.0.M1") } tasks.getByName<BootJar>("bootJar") { mainClassName = "com.example.dockerapp.DockerAppApplicationKt" } jib { from { image = imageFrom() } to { image = imageTo() } // multi-projectの場合はFat Jarを利用する containerizingMode = "packaged" container { mainClass = "com.example.dockerapp.DockerAppApplicationKt" // どの環境でも設定する汎用的なpropertyはjvmFlagsに追加する // https://qiita.com/dmikurube/items/15899ec9de643e91497c を参考にAsia/Tokyoをセット jvmFlags = listOf("-Dfile.encoding=UTF-8", "-Duser.timezone=Asia/Tokyo") ports = listOf("8080") // userを指定しない場合rootで動くので必ず指定する。uid:gid以外のセットでも指定できる。docker run 時の設定でもOK user = "1000:1000" // OCI formatで作成する format = com.google.cloud.tools.jib.api.ImageFormat.OCI // Setting image creation time to current time; your image may not be reproducible. // がメッセージとして出るので、creationTimeを設定するかは環境次第で決める creationTime = "USE_CURRENT_TIMESTAMP" // image作成時に追加でdirectoryを用意したい場合に設定 // volumes = listOf("/var/log/dockerapp") } } // https://github.com/GoogleContainerTools/distroless/issues/214 // https://console.cloud.google.com/gcr/images/distroless/GLOBAL/java?gcrImageListsize=30 // Using base image with digest fun imageFrom(): String { val profile = if (hasProperty("profile")) getProperty("profile") else "" return if ("development" == profile) // java:11-debug "gcr.io/distroless/java@sha256:e91e23383a8843a3f0bb00bdf99b9b7bed8c7ce25ed929acbe0eedec70fa91f9" else // java:11 "gcr.io/distroless/java@sha256:0ce06c40e99e0dce26bdbcec30afe7a890a57bbd250777bd31ff2d1b798c7809" } fun imageTo() = getProperty("registry") + "/" + getProperty("artifactid") + ":" + getProperty("version") fun getProperty(value: String) = findProperty(value) as String
// debug imageを使う場合developmentを指定する $ ./gradlew jibDockerBuild -Pprofile=development // jvmFlagsが設定されている 😊 Container entrypoint set to [java, -Dfile.encoding=UTF-8, -Duser.timezone=Asia/Tokyo, -cp, /app/classpath/*:/app/libs/*, com.example.dockerapp.DockerAppApplicationKt] // 実行環境によって異なるpropertyはJAVA_TOOL_OPTIONSで設定する $ docker run -p 8080:8080 -e "JAVA_TOOL_OPTIONS=-Dspring.profiles.active=development -Xms1G -Xmx1G" --rm jib/dockerapp:0.0.1-SNAPSHOT // JAVA_TOOL_OPTIONSをひろってくれる (Logをだしてくれる。親切 😊) Picked up JAVA_TOOL_OPTIONS: -Dspring.profiles.active=development -Xms1G -Xmx1G
あとはApplicationの必要に応じてenvironmentやargsを設定する感じになります。
運用がガラッと変わったらまた記事にします。さよならバイバイ(^_^)/~
JibでSpring BootのFat Jarを利用する
Jib Version 2.0.0からFat Jarが正式にサポートされました!
Release jib-gradle-plugin v2.0.0 · GoogleContainerTools/jib · GitHub
※ 1.8.0 時点ではFat Jarでの起動をデフォルトでサポートしていなかったので、ExtraDirectoryにJarをCopyしてEntryPointを書き換える辛みがありました。
前回の記事のような、Multi-Project構成でresourcesファイルが複数のsubProjectにまたがっている場合、Fat Jarを利用するのがベストプラクティスだと思います。
buld.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.2.4.RELEASE" apply false id("io.spring.dependency-management") version "1.0.9.RELEASE" id("java") kotlin("jvm") version "1.3.61" kotlin("plugin.spring") version "1.3.61" id("com.google.cloud.tools.jib") version "2.0.0" apply false } // 以下略
docker-app-web/build.gradle.kts
import org.springframework.boot.gradle.tasks.bundling.BootJar plugins { id("org.springframework.boot") id("io.spring.dependency-management") id("com.google.cloud.tools.jib") } dependencies { implementation(project(":docker-app-env")) implementation("org.springframework.boot:spring-boot-starter-webflux") } jib { from { // test用にdebugを利用 image = "gcr.io/distroless/java:11-debug" } // Fat Jarを利用する containerizingMode = "packaged" container { mainClass = "com.example.dockerapp.DockerAppApplicationKt" creationTime = "USE_CURRENT_TIMESTAMP" } } tasks.getByName<BootJar>("bootJar") { mainClassName = "com.example.dockerapp.DockerAppApplicationKt" }
exec jib
$ ./gradlew jibDockerBuild $ docker run -it --entrypoint /busybox/sh --rm docker-app-web:0.0.1-SNAPSHOT # ls -l /app/classpath/ total 8 -rw-r--r-- 1 root root 6338 Jan 1 1970 docker-app-web-0.0.1-SNAPSHOT-original.jar // original.jarがある 😊 # exit $ docker container run -p 8080:8080 --rm docker-app-web:0.0.1-SNAPSHOT
build時のentrypointは
[java, -cp, /app/classpath/*:/app/libs/*, com.example.dockerapp.DockerAppApplicationKt]
となっています。
org.springframework.boot.loader.JarLauncher
でexecutable jarを起動しているわけではなく、executable jarを解凍して依存ファイルをapp/libsにcopyして、main-classを指定して実行する形式のようです。Localで実行するとこんなイメージ。
$ java -cp BOOT-INF/lib/\*:docker-app-web-0.0.1-SNAPSHOT-original.jar com.example.dockerapp.DockerAppApplicationKt