こまぶろ

技術のこととか仕事のこととか。

Scrum Developers Night! を初めてオンサイトで開催しました

12月14日に、運営を務めている勉強会「Scrum Developers Night!」を初めてオンサイトで開催しました。

smn.connpass.com

会場

会場は、(運営メンバーの長岡さんの勤務先である)ヤフー株式会社内のスペース「LODGE」です。新型コロナ以前は勉強会やイベントの打ち合わせなどで度々利用させてもらっていた会場で、僕自身とても懐かしく感じました。会場提供ありがとうございます。

※2022年12月現在は一般利用の受付はしていないとのことです。

lodge.yahoo.co.jp

Scrum Developers Night! について

Scrum Developers Night! は、2020年の夏ごろから始めた勉強会です。この勉強会については以前サブのブログの方に書いたことがあります。

komad.hatenablog.com

なんでオンサイトで開催したのか

新型コロナの流行以降に始まった勉強会だったので、当初から一貫してオンライン(Discord)での開催を続けてきました。僕自身、フルリモートで働いているので、夕食後の時間に家から参加できるのが便利でしたし、Discordにも慣れているのであまり不便さを感じず、オンサイトへのこだわりはありませんでした。

ただ、年の瀬ということもあり、普段一緒に運営をしているメンバーと「忘年会くらいしたいですよね、せっかくだからオンサイトでイベント開催しますか?」という話をして、オンサイトでの開催をしてみようということになりました。

オンサイトで開催してみてどうだったか

上述の通り、オンライン開催に不便を感じていなかったのですが、オンサイト開催はやっぱり全然違いました。OST形式の勉強会なのですが、オンラインだとどうしても同時に喋るのは1人か2人になってしまいがちなところが、オンサイトだと横から入っていきやすいんでしょうね。聴いているだけの参加者がほぼいませんでした(聴いているだけでも気楽なのが逆にオンラインの良さでもあるのでしょうけれど)。わざわざ足を運ぶだけのモチベーションを持って参加していただいているのでということもあるのでしょうけれど、やっぱり話しやすいのかなとは感じたところです。ホワイトボートもかなり積極的に活用されていました。

当日の様子は以下のScrapboxにまとめています。

scrapbox.io

今後について・・・は考え中です

今回のオンサイト開催の体験が良かったので、オンサイトもやっていけたらなという思いはあります。一方で、オンラインの気楽さ・地理的な制約のなさも捨て難く感じます。運営メンバーの中でも相談しながら今後については考えていければと思っています。続報をお待ちください。

MySQL Advent Calendar 2022 の 13日目の記事として「MySQLのセミジョイン最適化」について書いた

MySQL Advent Calendar 2022 の 13日目の記事として、MySQLのセミジョイン最適化について調べた記事を書きました。こちらのブログに書いてもよかったのですが、今回は会社のブログに書きました。

qiita.com

tech.bm-sms.co.jp

昨年はアドベントカレンダーはどうだったんだっけ?と確認してみたところ、1本だけ記事を書いていました。もう1年になるんですね。

ky-yk-d.hatenablog.com

今年は書く予定がなかったのですが、ちょうどいいタイミングで書けることができたのでよかったです。ブログもあまり書けていないですし、だいぶコミュニティの出入りも少なくなってしまっているので、来年はまた少し変えてみられるとよいかなぁなどと思っています。

コミュニティといえば、明日(!)、久しぶりにオフラインの勉強会に運営側として参加するので、ぜひご参加ください。

smn.connpass.com

pom.xmlの特定の要素をxmllintで書き換える

CIからpom.xmlを自動で更新するような仕組みを入れようと思って、pom.xmlを書き換える方法を調べたのでメモ。

ポイントとしては、

  • 名前空間付きのもののときは *[local-name()='hoge'] を使う
  • 同名のタグが複数ある場合に特定のノードのものを選択するには and を使う

になる。

例題

以下のようなpom.xmlで、 maven-compiler-plugin のバージョンを変えたい。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>junit5-jupiter-starter-maven</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>${maven.compiler.source}</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.9.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M6</version>
            </plugin>
        </plugins>
    </build>

</project>

(コードは https://github.com/junit-team/junit5-samples/blob/f7029d7b9e5c73a8f3615a761d51e6e756e3e09d/junit5-jupiter-starter-maven/pom.xml のもの)

対応方法

たとえば以下のように記述すればよい。

VERSION=3.9.0
xmllint --shell pom.xml << EOF
cd /*[local-name()='project']/*[local-name()='build']/*[local-name()='plugins']/*[local-name()='plugin' and *[local-name()='artifactId']/text() = 'maven-compiler-plugin']/*[local-name()='version']
set $VERSION
save
EOF

名前空間付きのタグに対しては local-name() を使う

タグ名は実際には名前空間付きなので、テキストファイル上の名前を使って /project/build/... のように指定することはできない。local-name() を使って対象のタグを指定する必要がある。

同名のタグが複数ある場合に特定のノードのものを選択するには and を使う

書き換えたい要素は <plugins> 配下に複数ある <plugin> のうちの特定のものなので、条件を追加して指定する必要がある。以下の部分。

*[local-name()='plugin' and *[local-name()='artifactId']/text() = 'maven-compiler-plugin']

<plugin> で、かつ <artifactId> の内容が maven-compiler-plugin であるものを選択するためにこのような記述をしている。

local-name() を何度も書きたくないときは

何回も local-name() を書くのは見た目的にもわかりづらくなるので、ルートノードからの厳密なパスを指定せずに // を使って指定することもできる。

VERSION=3.9.0
xmllint --shell pom.xml << EOF
cd //*[local-name()='plugin' and *[local-name()='artifactId']/text() = 'maven-compiler-plugin']/*[local-name()='version']
set $VERSION
save
EOF

参考

atmarkit.itmedia.co.jp

techblog.zozo.com

MySQLの実行計画で Using intersect (...)な index_merge には要注意

実行計画での typeカラムに index_mergeが出たら

MySQLで、WHERE句で複数の条件をANDやORで繋いでいるクエリの実行計画をとると、 type カラムに index_merge というのが出ることがある。これはインデックスマージ最適化というものが使われるということを示している。

dev.mysql.com

インデックスマージ最適化とは

インデックスマージ最適化とは、本来は単一のクエリではインデックスが1つしか利用できないところ、複数のインデックスを使ってそれらをマージすることで最終的な結果を得るというアルゴリズムを使う最適化だ。

これに対し、MySQL 5.0 以降のバージョンでは、クエリが両方のインデックスを使用でき、それらを同時にスキャンして結果をマージできる。OR条件の和集合、AND条件の積集合、2つの組み合わせの積集合の和集合という3種類のアルゴリズムがある。 (『実践ハイパフォーマンスMySQL 第3版』168ページ)

3種類のアルゴリズムのうちのどれが使われるかは、実行計画のExtra カラムで知ることができる。

  • Using union(...): 和集合アクセスアルゴリズム
  • Using intersect(...): 共通集合アクセスアルゴリズム
  • Using sort_union(...) : ソート和集合アクセスアルゴリズム

「最適化」だから安心していい?

これらは「最適化」なので、一見すると「ありがとうMySQL」となりそうなのだが、実際にはその名に反して、クエリに問題があることを示唆しているケースがある。

インデックスマージ戦略が非常にうまくいくこともあるが、実際には、テーブルがうまくインデックス付けされていないことの裏返しであることのほうが多い。

(『実践ハイパフォーマンスMySQL 第3版』169ページ)

たとえば、 Using intersect(...) であれば、これはWHERE句で指定しているすべてのカラムを含んだ複合インデックスが作られているべき場面に個々のカラムへのインデックスしかない場合に出ていることがある。具体例として、以下のようなクエリを考える。

SELECT *
FROM products
WHERE category_id = 1
AND publication_year = '202209';

このクエリの実行計画で、 Using intersect(...) となっているとしたら、それは category_idpublication_year のそれぞれのカラムにインデックスが付いている場合だろう。その場合、個々のインデックスをスキャンし、category_id のインデックスについては category_id = 1 だが publication_year が 202209 ではない部分、publication_year のインデックスについては publication_year = '202209' だが category_id が 1 ではない部分も、それぞれ一旦(インデックススキャンの結果として)取得され、その後で共通部分をとって(マージして )、テーブル本体にアクセスすることになる。

それぞれのインデックスの選択性が高ければ大きな問題にはならないが、選択性の低いインデックスの場合は、途中のインデックススキャンの段階で無駄なリソースを消費し、パフォーマンスも悪くなってしまう危険性がある。

このクエリの場合、category_id, publication_year のような複合インデックスがあれば、最初からインデックス上の category_id = 1 かつ publication_year = '202209' の部分のみを使ってテーブルにアクセスでき、パフォーマンスの改善につながるケースがある。

Using intersect(...) を見過ごしてしまいやすい理由

ここまで書いたことは、実行計画の typeカラムを見て index_merge になっていることに「おや?」と思えれば調べられるだろうが、あまり詳しくない人は見逃してしまう危険性がある。

実行計画を見て、 typeALL になっていたり、 rows に巨大な数値が出ていたりすれば、「このクエリはまずそうだ」と思いやすいが、 index_mergeUsing intersect(...) となる場合、 rows カラムに出る数値はあくまで「最終的に読み込まれる行数の見積り」なので、先述したインデックスマージの途中で読み取られるインデックスの範囲のうちマージ時に(共通部分ではなかったために)除外される部分については rows カラムの数値には含まれない。 rows カラムに小さな値が出ているからといって、パフォーマンスが良いとは限らないというわけだ。

www.percona.com

実行計画を見るというのは、ある程度DBを触っていると基本的な仕草になってくるが、どこまで細かく見られるかというのはかなり違いがある(自分もとても詳しいわけではない)と思われ、「あれ?このクエリ?」と思うことがあったら都度勉強しなおしていきたい。

S3へのソースコードの配置をトリガーにしてCodePipelineを動かす設定をTerraformで書く

Amazon S3 へのソースコードの配置をトリガーにして AWS CodePipeline を動かす方法はいくつかあります。

  • S3 を CodePipeline からポーリングして変更を検知する
  • S3 への操作を CloudTrail で検知してイベントを発行して EventBridge 経由で CodePipeline を動作させる
  • S3 からイベントを EventBridge に送って CodePipeline を動作させる

それぞれについて Terraform で書く場合の要点をまとめておきます。

なお、Terraform AWS Provider はv3を前提にした記述になっています(3番目の方法についてはv3.74.0以上が必要です)。v4などを使う場合は適宜読み替えてください。

また、CodePipeline を動かす部分に焦点を置いているため、

  • S3 にどうソースコードを配置するか
  • CodePipeline をどう設定するか(トリガーされたあとの挙動)

については省略するか簡易にしか記述しません。

S3 を CodePipeline からポーリングして変更を検知する

1番目の方法で、以前から利用できる方法です。勝手にポーリングしてくれるので簡単。

PollForSourceChanges を true にすることで有効化できます。

たとえば以下のように記述します。

resource "aws_codepipeline" "my_pipeline" {
  name     = "my-pipeline"
  role_arn = aws_iam_role.my_pipeline_role.arn

  artifact_store {
    location = aws_s3_bucket.my_source.bucket
    type     = "S3"
  }

  stage {
    name = "Source"
    action {
      category = "Source"
      configuration = {
        PollForSourceChanges = "true" // ここが肝
        S3Bucket             = aws_s3_bucket.my_source_s3.bucket
        S3ObjectKey          = "my_artifact.zip"
      }
      name             = "Source"
      output_artifacts = ["SourceArtifact"]
      owner            = "AWS"
      provider         = "S3"
      run_order        = "1"
      version          = "1"
    }
  }
  stage {
    name = "Deploy"
    action {
      category = "Deploy"
      configuration = {
        ApplicationName     = aws_codedeploy_app.my_deployment.name
        DeploymentGroupName = aws_codedeploy_deployment_group.my_deployment.deployment_group_name
      }
      input_artifacts = ["SourceArtifact"]
      name            = "Deploy"
      owner           = "AWS"
      provider        = "CodeDeploy"
      run_order       = "1"
      version         = "1"
    }
  }
}

S3 への操作を CloudTrail で検知してイベントを発行して EventBridge 経由で CodePipeline を動作させる

2番目のやり方です。前掲の公式ドキュメントで、1番目の方法よりも推奨されている方法です。イベントドリブンなので、ポーリングによる方法に比べて迅速に CodePipeline を起動することができます。

PollForSourceChanges

Required: No

PollForSourceChanges controls whether CodePipeline polls the Amazon S3 source bucket for source changes. We recommend that you use CloudWatch Events and CloudTrail to detect source changes instead. For more information about configuring CloudWatch Events, see Update pipelines for push events (Amazon S3 source) (CLI) or Update pipelines for push events (Amazon S3 source) (AWS CloudFormation template).

難点は設定が面倒なことで、AWS CloudTrail という典型的には監査などの目的で利用されるサービスを間に噛ませる必要があります。以下のような流れになります。

  • S3 上のオブジェクトへの操作
  • CloudTrail が検知
  • CloudTrail がイベントを発行
  • EventBridge で定義しておいたルールに基づいて CodePipeline を起動

こちらの方法を採用する場合は、二重でパイプラインが起動するのを避けるため、先述の PollForSourceChanges を false に設定しておく必要があります。

1番目の方法で用いるリソースにさらに追加で以下のリソースが必要になります。

  • CloudTrail aws_cloudtrail
  • CloudTrail のログ出力先となる S3 バケット aws_s3_bucket
    • バケットポリシーで CloudTrail からの読み書きを許可する必要があります
    • 説明は省略します
  • EventBridge のルール定義 aws_cloudwatch_event_rule
  • EventBridge のイベントターゲット定義 aws_cloudwatch_event_target
  • EventBridge が CodePipeline を起動するためのIAMロール aws_iam_role
    • 説明は省略します

CloudTrail はたとえば以下のように記述します。

resource "aws_cloudtrail" "start_my_pipeline" {
  name           = "start-my-pipeline"
  s3_bucket_name = aws_s3_bucket.start_codepipeline.id // CloudTrailがログを出力する先のS3バケットを指定

  event_selector {
    read_write_type = "WriteOnly"

    data_resource {
      type   = "AWS::S3::Object"
      values = ["arn:aws:s3:::my_source_s3"] // ソースコードを配置する S3 を指定する
    }
  }
}

EventBridge のルールとターゲットはそれぞれ以下のように記述します。

resource "aws_cloudwatch_event_rule" "start_my_pipeline" {
  name = "start-pipeline-from-s3-rule"
  event_pattern = <<EOF
{
  "source": ["aws.s3"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventSource": ["s3.amazonaws.com"],
    "eventName": ["PutObject", "CompleteMultipartUpload", "CopyObject"],
    "requestParameters": {
      "bucketName": ["${aws_s3_bucket.my_source_s3.bucket}"],
      "key": ["my_artifact.zip"]
    }
  }
}
EOF
}
resource "aws_cloudwatch_event_target" "start_pipeline" {
  rule     = aws_cloudwatch_event_rule.start_my_pipeline.name
  arn      = aws_codepipeline.my_pipeline.arn
  role_arn = aws_iam_role.my_pipeline_role_for_event_bridge.arn
}

S3からイベントを EventBridge に送って CodePipeline を動作させる

3番目のやり方です。2021年11月のAWSのアップデートでサポートされた方法です。こちらでも PollForSourceChanges は false に設定しておく必要があります。

S3 へのソースコードの配置のイベントを CloudTrail を介することなく直接 EventBridge に送ることができるようになりました。これにより、2番目の方法で必要だった CloudTrail 関係のリソースが不要になります。 CloudTrall は細かく設定ができるのですが、リソースの数などについての制約事項があるため、個人的にはあまりパイプラインのために利用するのは嬉しくないのでは?と感じています。とはいえ、 EventBridge への S3 からのイベント発行が CloudTrail でやる場合に比べて増えるというデメリットもこちらにはあります。

1番目の方法との差分として必要なリソースは、以下になります。

  • EventBridge のルール定義 aws_cloudwatch_event_rule
    • 少し設定の書き方が変わります
  • EventBridge のイベントターゲット定義 aws_cloudwatch_event_target
    • 2番目の方法と同じ
  • EventBridge が CodePipeline を起動するためのIAMロール aws_iam_role
    • 2番目の方法と同じ
    • 説明は省略します
  • S3 から EventBridge への通知設定 aws_s3_bucket_notification

aws_cloudwatch_event_rule はたとえば以下のように記述します。

resource "aws_cloudwatch_event_rule" "start_my_pipeline" {
  name = "start-pipeline-from-s3-rule"
  event_pattern = <<EOF
{
  "source": ["aws.s3"],
  "detail-type": ["Object Created"],
  "detail": {
    "eventSource": ["s3.amazonaws.com"],
    "eventName": ["PutObject", "CompleteMultipartUpload", "CopyObject"],
    "bucket": {
      "name": ["${aws_s3_bucket.my_source_s3.bucket}"]
    },
    "object": {
      "key": ["my_artifact.zip"]
    },
    "reason": ["PutObject", "CompleteMultipartUpload", "CopyObject"]
  }
}
EOF
}

また、 S3 から EventBridge へのイベント発行はデフォルトでは無効になっているので、 aws_s3_bucket_notification を追加する必要があります。

resource "aws_s3_bucket_notification" "bucket_notification" {
  bucket      = aws_s3_bucket.my_source_s3.id
  eventbridge = true
}

なお、 aws_s3_bucket_notification リソースの eventbridgeプロパティは、Terraform AWS provider の v3.74.0 以降でサポートされています。

Release v3.74.0 · hashicorp/terraform-provider-aws · GitHub

まとめ

この記事を執筆している2022年8月現在では、公式ドキュメント上は2番目の方法が推奨されているのですが、仕組みのシンプルさを欠いている印象が強く、ゆくゆくは3番目の方法や、それを更に発展させた方法が標準になっていくのではないかなと感じました。

参考資料