ecspresso で CloudFormation のリソース読み込みに対応 + その他機能追加した v1.3.0 をリリースしました

あけましておめでとうございます。Amazon ECS デプロイツール ecspresso の v1.3.0 をリリースしたのでお知らせです。

[2021-01-16 追記] v1.3.0 は init コマンドでクラッシュする問題があります。v1.3.1 をご利用ください。

今回は機能追加が盛り盛りなので、まとめて紹介します。

Release v1.3.0 · kayac/ecspresso · GitHub

【新機能】 cloudformation プラグインを追加

これまで ecspresso は Terraform tfstate を読み込んでそのリソース名から属性値を解決できる機能を持っていましたが、CloudFormation でも同様の処理ができるようになりました。

README に例がありますが、次のように Output と Export を定義したテンプレートで構築された、Stack 名が ECS-ecspresso という CloudFormation stack がある場合に

Outputs:
  SubnetAz1:
    Value: !Ref PublicSubnetAz1
  SubnetAz2:
    Value: !Ref PublicSubnetAz2
  EcsSecurityGroupId:
    Value: !Ref EcsSecurityGroup
    Export:
      Name: !Sub ${AWS::StackName}-EcsSecurityGroupId

ecspresso の設定ファイルで cloudformation plugin を指定すると

plugins:
  - name: cloudformation

定義ファイルで cfn_outputcfn_export という関数が使用可能になります。cfn_output スタック名 OutputKey で Outputs の値を名前で解決できます。cfn_export エクスポート名 で、エクスポートされた値を名前で解決できます。

サービス定義での使用例は次のようになります。

{
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "subnets": [
        "{{ cfn_output `ECS-ecspresso` `SubnetAz1` }}",
        "{{ cfn_output `ECS-ecspresso` `SubnetAz2` }}"
      ],
      "securityGroups": [
        "{{ cfn_export `ECS-ecspresso-EcsSecurityGroupId` }}"
      ]
    }
  }
}

ご意見をくださった @torics さん、@hamako9999 さん、@_sisisin さん、ありがとうございました!

【新機能】scale コマンドを追加

これまでサービスのタスク数を変更するためには deploy --no-update-service --skip-task-definition --tasks タスク数 を指定していましたが、これをショートカットにした scale コマンドを追加しました。

$ ecspresso --config config.yaml scale --tasks 10

これで ecspresso --config config.yaml deploy --no-update-service --skip-task-definition --tasks 10 とおなじ意味になります。

タスク数だけを変えたい場合でも指定するオプションが多いため、うっかりサービスやタスクを更新してしまう事例がありました。scale コマンドを使用すると必要なのは --tasks のみ(必須) になるため、事故を防げます。

【新機能】tfstate plugin が URL を直接読み込めるようになりました

これまで tfstate plugin はローカルファイルの読み込みのみが可能でしたが、直接 s3, http(s) URL からも読み込めるようになりました。デプロイする環境に terraform.tfstate が存在していない場合、これまではわざわざ aws s3 cp などでローカルにコピーする必要がありました。

plugins:
  - name: tfstate
    config:
      url: s3://my-bucket/terraform.tfstate

tfstate-lookup コマンドも v0.1.0 で URL からの tfstate 読み込みをサポートしています。 https://github.com/fujiwara/tfstate-lookup/releases/tag/v0.1.0

【新機能】設定ファイルで required_version が使用可能に

特定のバージョン以降で追加された機能を要求する場合など、ecspresso のバージョンに制約を持たせたい場合に設定ファイルで使用できる機能です。

required_version: ">= v1.3.0, < v2"

このように semver で指定することで、条件を満たしていないバージョンの ecspresso で実行しようとするとエラーになります。hashicorp/go-version を使用しています。

この機能をサポートしていない v1.3.0 未満のバージョンでは無視されるため、v1.3.0 がリリースされた時点では指定する意味はありません。将来新しいバージョンがリリースされ、その時点で入った機能や修正に依存した処理があるためそれ未満のバージョンでは実行させたくないような場合に役に立つ予定です。

PR を送ってくださった mashiike さん (同僚) ありがとうございました!

【新機能】run コマンドでタスクにタグが付けられるように

ecspresso run --tags Foo=Bar で、実行するタスクに Foo という名前のタグが Bar という値で設定されます (複数指定可能)。

ecspresso run --propagate-tags=(SERVICE|TASK_DEFINITION) で、実行するタスクに付けるタグをサービス定義もしくはタスク定義から引き継ぐことができます。

実は現状では、ecspresso でタスク定義を登録する際にはタグを附与することができません。これは定義ファイルを SDKecs.TaskDefinitionマッピングしていて、この構造体にはタグの定義がないためです。(サポート方法を悩みつつ、具体的な需要が今のところないので何もしていません)

その他修正いろいろ

  • [Fix] Unable to update platformVersion and networkConfiguration for CodeDeploy services. #209
    • CodeDeploy でデプロイする際に、--update-service を付けていても platformVersion と networkConfiguration が変更できなかった問題を修正しています。
  • [Fix] diff command becomes to compare local task definition and remote task definition. #218
    • diff コマンドが比較するタスク定義のリモート(ECS)側が、これまで latest (一番リビジョンが大きい) のものになっていましたが、サービスが現在使用しているリビジョンのものと比較するようになりました。
  • [Fix] Support to Docker Registry V2 API. Enables to read any public image repository using V2 API. (public.ecr.aws, gcr.io, ghcr.io and etc.) #214 #213
    • ECR Public repo など、一般の公開 image を verify できなかった問題を修正しています。Docker Registry API V2 を利用している任意の公開リポジトリの image が存在検証できます。
    • この修正に伴って、verify に 必要な IAM 権限から ecr:ListImages が不要になりました。

ecspresso handbook も改訂予定

昨年末に公開した ecspresso handbook ですが、こちらも v1.3.0 の新機能へ追従して近日改訂予定です。ご購入の方はそのままお読みいただけます。まだの方は是非この機会にお求めください。

zenn.dev

ecspresso handbookをZennで公開しました

激動の2020年もいよいよ押し詰まってきましたが、皆様いかがお過ごしでしょうか。

今年は ecspresso advent calendar 2020 というひとりアドベントカレンダーを唐突に思いついて始めてしまったのですが、なんとか完走することができました。

拙作の OSS である Amazon ECS 用のデプロイツール ecspresso の基本的な使い方から応用編、コマンドリファレンス、設計思想までを一通り書ききることができたので、せっかくなので Zenn で本としてまとめることにしました。

zenn.dev

9万字弱とそこそこのボリュームになり、書くのも大変だったしなあということで、500円の有料本としています。

内容自体はアドベントカレンダーに書いたものを再編集(章立ての変更と校正など)したものなので、特に新しい情報が入っているわけではないですが、もしよろしかったらお買い求め頂けると大変ありがたいです。売れると開発にもやる気が出ます(重要)。冬休みのお供にいかがでしょうか!

ecspresso の今後ですが、CloudFormation との連携 (OutputとExportの論理名を指定して物理名を解決できる) の実装の目処が付いたので、年明けぐらいにリリースできるといいなと思っております。

それでは、皆様良いお年を (よろしければ ecspresso handbook と一緒に) お迎えください。

Amazon Lightsail Container を Mackerel で監視する

Mackerel Advent Calendar 2020 8日目の参加記事です。

2020年11月に、Amazon Lightsail Container というサービスがリリースされました。

dev.classmethod.jp

最低月額 $7 で、コンテナを気軽に(ロードバランサー込みで!) Amazon Lightsail 上にデプロイできるというものです。普通に AWS で Fargate のコンテナとロードバランサー(ALB)を動かすと最低でも$25/月程度はかかってしまうので、これはお手軽でよさそうですよね。

ということで、この Lightsail Container を Mackerel で監視してみましょう。

Lightsail Container の実体は?

なにはともあれ、Ligtsail Container に環境変数を出力する Web アプリケーションをデプロイして素性を探ってみます。環境変数JSON で出力する HTTP サーバを起動してくれる便利な phamviet/printenv という Public Image があるので、これを適当に Lightsail Container にデプロイしてアクセスしてみると、以下のようなレスポンスが得られました。

{
  "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/v2/credentials/03aed4fb-11f7-4c84-967a-34389ff48412",
  "AWS_DEFAULT_REGION": "ap-northeast-1",
  "AWS_EXECUTION_ENV": "AWS_ECS_FARGATE",
  "AWS_REGION": "ap-northeast-1",
  "ECS_CONTAINER_METADATA_URI": "http://169.254.170.2/v3/3715f73c-a351-4df9-a923-02163d4df095",
  "ECS_CONTAINER_METADATA_URI_V4": "http://169.254.170.2/v4/3715f73c-a351-4df9-a923-02163d4df095",
  "HOME": "/root",
  "HOSTNAME": "ip-172-26-26-31.ap-northeast-1.compute.internal",
  "NODE_VERSION": "10.16.3",
  "PATH": "/usr/src/app/node_modules/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  "PWD": "/usr/src/app",
  "SHLVL": "1",
  "YARN_VERSION": "1.17.3"
}

"AWS_EXECUTION_ENV": "AWS_ECS_FARGATE" なるほどですね。

Lightsail Container = Fargate を簡単に使えるようにいい感じにしてくれているなにか、のようです(あくまで推測ですよ?)

ということは普通に mackerel-container-agent で ECS / Fargate を監視するのと同じ方法でいけそうですよね。サイドカーを追加してみましょう。

mackerel-container-agent のサイドカーを追加する

公式のドキュメントをもとに、以下のような設定でコンテナを追加しました Amazon ECSにmackerel-container-agentをセットアップする

f:id:sfujiwara:20201205022844p:plain

  • image: mackerel/mackerel-container-agent:v0.5.0
  • environment
    • MACKEREL_APIKEY: *************
    • MACKEREL_CONTAINER_PLATFORM: ecs

デプロイしてしばらく待ってみると…

f:id:sfujiwara:20201205022918p:plain

普通に Fargate task として認識され、同様のメトリックが得られました。よかったですね。

[PR] デプロイツール sailtrim 開発中

現在、fujiwara/sailtrim という、Lightsail Container 用のデプロイツールを開発しています。

まだまだ荒削りですが、ECSのデプロイツール ecspresso や Lambda のデプロイツール lambroll と同じような使い勝手で Lightsail Container をコード管理してデプロイできるツールです。よろしくどうぞ。

ECS task definition を Jsonnet で生成する

Amazon ECS でタスクの構成定義に使う task definition をリポジトリで管理する場合、JSON 形式で取り扱うことが多いと思います。

ECS デプロイツールである ecspresso でも、task definition は環境変数等を展開できる記法があるものの、基本的には JSON ファイルとして扱っています。

これは AWS コンソールで作成した task definition を aws describe-task-definition や ecspresso init コマンドでファイルとして取り出して利用することを前提としているためで、これを YAML や別の形式で扱うことは考えていません。

ところで、実際に ECS でそれなりのサービスを運用すると、task definition JSON の管理で悩むことが増えてきました。

  • JSON は人間が編集するのに便利ではない
    • コメントが書けない、配列末尾の , の有無で余分な diff が発生、key をいちいち " " で括るのが面倒…
  • サイドカーなどがだいたい同じで、一部だけ異なる task definition を複数運用する場面がある
    • 複雑な構造の一部が異なるような場合、環境変数展開ではカバーできないので、コピペせざるを得ない
  • タスク内の各コンテナで共通で埋め込みたい要素がそこそこあるが、揃えて変更するコストが高い
    • environment 要素などが典型

例として、nginx にサイドカーとして mackerel-container-agent を追加した task definition が以下のようにあるとします。

{
  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx:latest",
      "environment": [
        {
          "name": "AWS_REGION",
          "value": "ap-northeast-1"
        },
        {
          "name": "TZ",
          "value": "Asia/Tokyo"
        }
      ],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp"
        }
      ]
    },
    {
      "name": "mackerel-container-agent",
      "image": "mackerel/mackerel-container-agent:v0.4.0",
      "environment": [
        {
          "name": "AWS_REGION",
          "value": "ap-northeast-1"
        },
        {
          "name": "TZ",
          "value": "Asia/Tokyo"
        },
        {
          "name": "MACKEREL_CONTAINER_PLATFORM",
          "value": "ecs"
        }
      ],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "secrets": [
        {
          "name": "MACKEREL_APIKEY",
          "valueFrom": "/MACKEREL_APIKEY"
        }
      ]
    }
  ],
  "cpu": "256",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "family": "ecspresso-test",
  "memory": "512",
  "networkMode": "awsvpc",
  "requiresCompatibilities": [
    "EC2",
    "FARGATE"
  ],
  "taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskRole"
}

environment には AWS_REGION と TZ が共通で設定されています (mackerel-container-agent には追加で MACKEREL_CONTAINER_PLATFORM)。

共通で設定したい環境変数に変更がある場合、nginx と mackerel-container-agent の両方のコンテナ定義を変更する必要があります。この例ではタスク内に 2コンテナしかないですが、実際に稼働するタスクでは多数のサイドカーがあったりして、人手で漏れなく変更するのは大変ダルいことになります。

Jsonnet で JSON を生成する

JSON をテンプレートから生成する処理系として、Jsonnet というものがあります。これを使って task definition JSON を生成して、辛さが軽減できないか試してみました。

jsonnet.org

qiita.com

共通要素の括りだし

まず environment 要素の共通部分を抜き出してファイルにします。

[
    {
        "name": "AWS_REGION",
        "value": "ap-northeast-1"
    },
    {
        "name": "TZ",
        "value": "Asia/Tokyo"
    }
]

このファイルを envs.libsonnet として保存して、taskdef.json をコピーして taskdef.jsonnet を以下のように用意します。もとの taskdef.json をちょっといじるだけです。

  • local envs = import 'envs.libsonnet'; を追加
  • "environment": envs, として import した値を展開
# taskdef.jsonnet
local envs = import 'envs.libsonnet'; # envs テンプレートの読み込み
{
  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx:latest",
      "environment": envs, # 読み込んだ envs をここに展開
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp"
        }
      ]
    },
    {
      "name": "mackerel-container-agent",
      "image": "mackerel/mackerel-container-agent:v0.4.0",
      "environment": envs + [  # envs 配列への追加
        {
          "name": "MACKEREL_CONTAINER_PLATFORM",
          "value": "ecs"
        }
      ],
      "essential": false,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "secrets": [
        {
          "name": "MACKEREL_APIKEY",
          "valueFrom": "/MACKEREL_APIKEY"
        }
      ]
    }
  ],
  "cpu": "256",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "family": "ecspresso-test",
  "memory": "512",
  "networkMode": "awsvpc",
  "requiresCompatibilities": [
    "EC2",
    "FARGATE"
  ],
  "taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskRole"
}

これを jsonnet コマンドで処理すると、taskdef.json と同一のものが生成されます。

$ jsonnet taskdef.jsonnet | head
{
   "containerDefinitions": [
      {
...

$ # 差分確認
$ diff -w <(jsonnet taskdef.jsonnet | jq --sort-keys .) <(cat taskdef.json | jq --sort-keys .)

JSON より扱いやすい記法への変換

Jsonnet は JSON の superset なので JSON 記法そのままでも扱えますが、jsonnetfmt コマンドを通すと素の JSON よりも人間が扱いやすい Jsonnet の記法に変換してくれます。

$ jsonnetfmt envs.libsonnet
[
  {
    name: 'AWS_REGION',
    value: 'ap-northeast-1',
  },
  {
    name: 'TZ',
    value: 'Asia/Tokyo',
  },
]

key のクォートが不要だったり、配列やオブジェクト末尾の , が許可されたりと、すっきり書けるようになります。

コンテナ定義の共通化

タスク内に複数あるコンテナ定義をテンプレートで共通化してみます。各コンテナの共通要素を定義したファイルを用意して container.libsonnet というファイルに保存します。

# container.libsonnet
local envs = import 'envs.libsonnet';
{
  essential: true,
  environment: envs,
  logConfiguration: {
    logDriver: 'awslogs',
    options: {
      'awslogs-group': 'ecspresso-test',
      'awslogs-region': 'ap-northeast-1',
    },
  },
}

このコンテナ定義テンプレートを使って、nginx コンテナの定義を生成する jsonnet は次のようになります。 import したコンテナ定義テンプレートに、必要な要素を上書き(追加)するだけです。

# nginx.libsonnet
local container = import 'container.libsonnet';
container {
  name: 'nginx',
  image: 'nginx:latest',
  portMappings: [
    {
      containerPort: 80,
      hostPort: 80,
      protocol: 'tcp',
    },
  ],
}

同様に mackerel-container-agent のコンテナ定義も container 定義テンプレートから生成します。environment は配列要素への追加になるため、container.environment + [ ] という形で定義しています。

# mackerel.libsonnet
local container = import 'container.libsonnet';
container {
  name: 'mackerel-container-agent',
  image: 'mackerel/mackerel-container-agent:v0.4.0',
  environment: container.environment + [
    {
      name: 'MACKEREL_CONTAINER_PLATFORM',
      value: 'ecs',
    },
  ],
  secrets: [
    {
      name: 'MACKEREL_APIKEY',
      valueFrom: '/MACKEREL_APIKEY',
    },
  ],
}

この各コンテナ定義を import して、taskdef を生成する jsonnet は以下のようになりました。すっきり!

# taskdef.jsonnet
local mackerel = import 'mackerel.libsonnet';
local nginx = import 'nginx.libsonnet';
{
  containerDefinitions: [
    nginx,
    mackerel,
  ],
  cpu: '256',
  executionRoleArn: 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole',
  family: 'ecspresso-test',
  memory: '512',
  networkMode: 'awsvpc',
  requiresCompatibilities: [
    'EC2',
    'FARGATE',
  ],
  taskRoleArn: 'arn:aws:iam::123456789012:role/ecsTaskRole',
}

この taskdef.jsonnet を jsonnet コマンドでレンダリングすると、もとの taskdef.json と同一内容の JSON が出力されます。

更に似たような task definition を増やしたい場合には taskdef の定義テンプレートを用意して共通項目を括りだし、それを基にそれぞれの JSON を生成してもいいでしょう。

まとめ

取り扱いが煩雑になりがちな JSON による ECS task definition を Jsonnet というテンプレート言語で生成してみました。

Jsonnet のサイトを見るとあまりにいろいろな機能があってびっくりしますが、まずは既存 JSON の共通部分を括り出す程度のテンプレートとして最小限で使うのをお勧めします。

Jsonnet は独自関数定義なども使えるので、やり過ぎると読めなくなる可能性はありそうです。が、生成されるものは plain な JSON なので、最悪読めなくなっても生成後の JSON からリファクタリングをやり直すことは可能です。

デプロイに ecspresso を利用する場合は、デプロイ前に ecspresso diff コマンドを使用すると、現在稼働中の task との差分があるかを検出できます。Jsonnet でリファクタリングした結果意図せず壊していないかの検知に便利だと思います。

ecspresso v1 をリリースします(v0.99.x をリリースしているのでお試しください)

おかげさまで各所でご利用頂いている(模様の) Amazon ECS デプロイツール ecspresso ですが、リリースしてからもうすぐ3年になりますし、ここで一部非互換の修正を入れた v1 を出そうとしています。

非互換な点は少ないのでほとんどの方にはそのまま使って頂けると思いますが、下記を確認した上で v0.99.x を試して頂いて、もし何か問題があればフィードバックを頂けると幸いです。

(2020-10-30 追記) v1.0.0 をリリース済です。

github.com

(2020-09-25 23:00 追記) Homebrew で 0.99.x がインストールできるようになりました。 tap ecspresso@0.99 by mashiike · Pull Request #9 · kayac/homebrew-tap · GitHub

$ brew install kayac/tap/ecspresso@0.99

既に v0.x がインストールされている場合は conflict するので、一旦 unlink して link し直してください。

$ brew unlink ecspresso
$ brew link ecspresso@0.99

v0.x に戻したい場合は逆操作です。

$ brew unlink ecspresso@0.99
$ brew link ecspresso

v1 リリース後は @0.99 は削除されるので、単に brew upgrade ecspresso で v1 へ更新されるようになる予定です。 (追記ここまで)

v1 で非互換になる点

deploy --update-service (true) がデフォルトになります

従来 、service 定義ファイルを更新した場合にそれを ECS service に反映するには、ecspresso deploy --update-service とオプションを指定する必要がありました。

v1 では --update-service はデフォルトで true になります。v0.x と同様、service 定義に変更があっても deploy 時に適用したくない場合、deploy --no-update-service として明示する必要があります。

次に説明する、service 定義の desiredCount が反映されるようになる件との組み合わせで、意図しない挙動になる場合がありますのでご注意ください。

service 定義に記述された desiredCount を解釈するようになります

Does not `desiredCount` field work? · Issue #131 · kayac/ecspresso · GitHub

これまでは service 定義に desiredCount 属性が記述されていても無視されていました。v1 では service 定義に desiredCount 属性が存在して null ではない数値が記載されている場合に限り、deploy 時にその値を使用するようになります。deploy --tasks N が指定されていればそちらが優先されます。

ecspress deploy 実行時に実際に ECS task に指定される desiredCount は、以下の優先順位で決定されます。

  1. --tasks で指定された値
  2. service 定義に desiredCount で定義されている値
  3. (現在の service の desiredCount と変更なし)

これまで通りの挙動を期待する場合は、service 定義に desiredCount が指定されていないことを確認してください。

service / task 定義ファイルの path を config ファイルからの相対で解釈するようになります

feature request: resolve definition files path relatively from config.yml · Issue #140 · kayac/ecspresso · GitHub

例えば現在 config.yaml に以下のように記述されていている場合、

service_definition: ecs-service-def.json
task_definition: ecs-task-def.json

ecs-(service|task)-def.json は ecspresso 実行時のカレントディレクトリに存在している必要があります。

v1 では config ファイルからの相対で解釈するようになるため、

$ ecspresso --config path/to/config.yml

として実行した場合は ecs-(service|task)-def.json は ecspresso 実行時のカレントディレクトリではなく、config.yml と同一ディレクトリ (path/to) に存在しているものとして実行します。


以上、よろしくお願いします!

ISUCON10予選に参加して不通過でした

Webアプリケーションパフォーマンスチューニングコンテスト ISUCON http://isucon.net/、記念すべき10回大会の予選に参加して、あと3チーム100点弱の差で不通過に終わりました。悔しい!

チームメイトは会社の同僚の @acidlemon (ISUCON 3の出題、ISUCON 4, 7 のチームメイト), @mackee_w (macopy, 実はチームを組んだのは初) です。

序盤から中盤

  • ベンチを回して MySQL が重いねー(いつものことだ) と把握
  • acidlemon
    • estate の範囲検索になっているカラムを = 条件で取れるように 0〜49999 -> 0, 50000〜100000 -> 1 のようにクラスわけ(verify がたまにコケるのを解消できず取り込めず
    • interpolateParams=true (server side prepare を無効)
    • select * をやめて id だけまず取得して where id in
  • macopy
    • featureがLIKE検索で複数条件に一致をやっていたのを別テーブルに逃して縦持ち
    • index 追加
    • geo 検索でポリゴンに座標が含まれているかを MySQL ではなく Go 側で判断 (後回しにしたら試行もできず…
  • fujiwara
    • OS は Ubuntu と予告されていたので各種ツール (alp, percona-toolkit, notify_slack, netdataなど) のセットアップ用 mitamae を流して初期設定
    • MySQL を軽くチューニング
    • nginx.conf で bot に 503 を返すように正規表現
    • Regexp::Assemble でまとめてこんなのを "(I(SUCON(bot((-Mobile)?|-Image\/)|FeedSeeker(Beta)?|Coffee)|supider((-image)?\+)?)|crawler \(https:\/\/isucon\.invalid\/(support\/faq\/|help\/jp\/)|Mediapartners-ISUCON|isubot))"

今回はとにかく検索が重く (単発ではそこまで重くないけど10000〜30000行読む必要がある数十msのクエリが大量に飛ぶ)、MySQL の負荷を index では軽減しきれず、序盤〜中盤まで全くスコアを伸ばせず苦しい展開でした。初期スコアは500程度、いろいろやっても600ぐらい。

大抵の ISUCON では序盤にいくつかの定番の手を打つと上がるみたいなジャブが効く感じがあるのですが、今回は全然ダメージを与えられている気がしないという、アプリケーションとしては比較的単純な作りなのに心を折りに来る良問でしたね…

また途中、自分が用意した deploy.sh がカレントディレクトリからでないと正常に動かないという雑な作りなため、macopy がデプロイしたつもりができてない (のでスコアが何も変わらない) というので revert を連発することになり、1時間ほど消費するというトラブルもありました。

中盤から終盤

時間的にはもう終盤に近く、まったくスコアが伸びないと心が先に折れるので、力押しでスコアを出そうと MySQLレプリケーションを組んで3台構成にしました。

  • isucon1: nginx, app, mysql (writer)
  • isucon2: app, mysql (reader)
  • isucon3: app, mysql (reader)

として、isucon2, 3 にレプリケーションを設定。アプリケーションからは 127.0.0.1 に接続する reader 用の db 接続を用意し、Search 系のクエリはそちらに切り替え。レプリ遅延回避のため多少小ネタを入れます。

  • ベンチの最初に叩かれる /initialize でデータが初期化されるため、その遅延が解消するまで initialize のレスポンスを sleep(10) して粘る
  • /initialize が終わると整合性チェックが User-Agent: isucon-verify で飛んでくるので、このアクセスはすべて 1 台目で処理するように nginx を設定
    • 1台目は reader 接続も 127.0.0.1 (=writer) を向いているので確実に遅延なしで読めます

これで1000を超えるぐらいまでなんとか伸ばして一息入れました。

更に、データの更新はほぼないので MySQL のクエリキャッシュが効きそう、ということで有効化して1300ぐらい? CPU が若干使い切れてなさそうだったので db.SetMaxOpenConns(10)db.SetMaxOpenConns(30) にしたらまた少し。slowlog はもう見ないことにして無効化。

あと残り2時間程度、ここからもう MySQL そのものをどうにかするのは無理筋と判断して、acidlemon と macopy に、それぞれ chair と estate の検索をオンメモリにする実装を並行で取りかかってもらいました。

先に取りこんだ estate の検索は fail したので切り戻し、残り30分を切ったところで chair の検索オンメモリ化の投入になんとか成功して 1800 越え、再起動試験をして残り10分で 2074、競技中の最高スコアをマークしたところで打ち止めとなりました。

終戦

最終的には、一般枠予選通過まで84点差、あと3チーム上回れば…という惜しいところで敗戦しました。

アクセスログみる限り、追試でのベンチ走行では競技中の最終ベンチよりも10%ほど多くレスポンスを返せていたので、あと何度かベンチを回していたらブレで100点程度上がっていたかも知れず、通過の可能性も僅かながらあったはずなので、粘りが足りなかったですね。ISUCON 6 の時は最後の1分を切ったところでベンチを開始して、それが fail せずに通ったことで予選を通過できていたのに…

ISUCON10 オンライン予選 全てのチームのスコア(参考値) : ISUCON公式Blog

検索のオンメモリ化ももう1,2時間早く判断して取りかかっていれば、estate, chair の両方を動かすことができた可能性が高く、これは判断の遅れです。そもそも予選通過ボーダー付近は毎年混み合うので、そこで揉み合っていた時点でベンチのスコアのブレの運要素を排除しきれず、つまりは実力不足でしたね。

おわりに

ISUCONでは予選通過できたのが7まで (8は出題、9, 10 が予選落ち) ということで、2年連続で本選の地を踏めず、悔しい思いです。今年も楽しい問題でした。運営の皆様、トラブルで開始が遅れたにもかかわらず、当日中に結果の発表まで大変お疲れ様でした!

GitHub Actions の Composite Run Steps で ecspresso / lambroll をインストールする action を作った

要約

GitHub Actions に Amazon ECS デプロイツール ecspressoAWS Lambda デプロイツール lambroll を簡単にインストール action を用意しました。

steps で以下の指定をするだけです。(Linux 環境専用です)

- uses: kayac/ecspresso@v0
- uses: fujiwara/lambroll@v0

もうちょっと詳しく

これまで GitHub Actions の action は基本的に Node で書く必要があったのですが、やりたいことはバイナリをダウンロードして展開してコピーするだけなのになんで Node を書かないといけないのか……(Linux以外の環境を考えると仕方ないとはいえ)、と面倒くささが先に立って、これまで action を作らないままでした。

が、先日 Composite Run Steps が使えるようになり、これは要するに自分の steps に shell script を書くかのように定義した action.yml を置いておくだけでよいという、顧客が本当に欲しかったものだったのでした。最高。

github.blog

ソースはこれだけで、見ての通り単に curl で落として展開して install するだけ、です。

https://github.com/kayac/ecspresso/blob/master/action.yml

inputs:
  version:
    description: "A version of ecspresso"
    default: "v0.17.3"
runs:
  using: "composite"
  steps:
    - run: |
        FILENAME=ecspresso-${{ inputs.version }}-linux-amd64
        cd /tmp
        curl -sLO https://github.com/kayac/ecspresso/releases/download/${{ inputs.version }}/${FILENAME}.zip
        unzip ${FILENAME}.zip
        sudo install ${FILENAME} /usr/local/bin/ecspresso
        rm -f ${FILENAME} ${FILENAME}.zip
      shell: bash

使う側は他の action と同様、step で uses: kayac/ecspresso@v0 などとするだけです。便利。

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: kayac/ecspresso@v0
        with:
          version: v0.17.3
      - run: |
          ecspresso deploy --config config.yaml

どうぞご利用ください。