すきま風

勉強したことのメモとか

雑記

どうでもいい仕事の話

3月からずっと自宅勤務です。土日に自分の仕事をこなして、平日はダラダラとコードレビューとかに使う業務パターンが完全に定着しました。
自分にとってコードレビューはかなり精神の負荷の高い作業で、例えば↓みたいな感じで心が乱高下します。

  • 同僚のきれいなコードを見て凹む
  • レガシーシステムのクソコードを見て憤る
  • ベンダーのクソコードに絶望
  • 新入社員のコードを見て今後の教育方針に悩む
  • etc...

で、大体コードレビューと打ち合わせと設計レビュー等々で結構時間を使うのですが、自宅だとなんか気持ちの切り替えができなくて自分の仕事が停滞するようになったので、もうこれは平日は自分の仕事をすっぱり諦めることにしました。土日はSlack見なくていいしオンラインミーティングもないし捗りますなあ。

もっとどうでもいい話

デレマスのボイスオーディションで「三好紗南」「綾瀬穂乃香」「ライラさん」に投票したけど実らなかったなあ。紗南ちゃんはワンチャンあると思ってたけど。来年も多分同じ人に入れます。 社長はびっくりした。


総選挙は「水本ゆかり」「北条加蓮」「依田芳乃」に投票しました。加蓮みたいにトップになることを公然と表明して、実践しているアイドルが勝利を掴むのを見るのは気持ちが良い。ゆかりさんは2期連続で圏内に入ったから、フェス限でも実装してくれないかしら

serverless-artilleryでIP制限のあるAPIの負荷テストを行う

仕事で負荷テストツールの選定をしているので、得た知見をブログに書いていこうと思います。
ちなみに、今まで負荷テストは人任せで、ツールはほとんど触ったことがありませんでした 😑

serverless-artillery

serverless-artillery は負荷テストツールのArtilleryServerless 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を立てる方が良いかなー 🤔

参考

GitHub - Nordstrom/serverless-artillery: Combine serverless with artillery and you get serverless-artillery for instant, cheap, and easy performance testing at scale.

サーバレス時代の負荷テスト戦略 〜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にする

業務でPHPアプリケーションを保守することになったのでとりまコンテナ化しました。 fpm × nginxで作りたかったけど業務上Apache縛りがあったのでモジュール版を使います。

2020/06/12 追記: fpm versionも書きました

ソフトウェアバージョン

software version
PHP 7.4.3
Apache 2.4
CodeIgniter 3.1.11

追加でやること

  • Redis を使えるようにする
  • LogはJsonにして標準出力にする
  • Apacheのconfファイルの設定値は環境変数で設定する
  • gzip圧縮する
  • root userでコンテナを動かさないようにする
続きを読む

Cloud Native Buildpacksのpack CLIでSpring BootのFat Jarを起動するDocker Imageを作成する。 あるいはSpring Boot 2.3.0.M1 のlayeredをエミュレートする

この記事の内容

  • pack CLIでLayer ArchitectのSpring Boot ApplicationのDocker Imageを作成する
  • 上記の目的のために、Spring Boot 2.3.0.M1のlayered()をエミュレートしたshell scriptを書く
  • 実用性皆無のネタ記事です 😑


続きを読む

ぼくのかんがえたさいきょうの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を書き換える辛みがありました。

How to package Spring Boot Executable Jar to Docker image? · Issue #954 · GoogleContainerTools/jib · GitHub


前回の記事のような、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