コネヒト開発者ブログ

コネヒト開発者ブログ

Lambda も Chatbot も SNS も使わない!EventBridge だけで CloudWatch Alarm のメッセージを加工し Slack へ post する方法

こんにちは。開発部プラットフォームグループでインフラエンジニアをしている @sasashuuu です。先日、Custom notifications are now available for AWS Chatbot で AWS Chatbot でのカスタム通知が行えるアップデートが発表されました。 実はこの方法以外にも、AWS EventBridge だけで CloudWatch Alarm のメッセージをさらに柔軟に加工して Slack へ post する方法があることをご存知でしょうか。Chatbot のアップデートが発表される前に当社で導入していたのですが、SNS や Chatbot を組み合わせたアーキテクチャを組まなくても良い他、 Lambda のランタイムの管理や実装の煩雑さのリスクなども減るので意外と活用できる方法なのではないかと思っております(実は当時 Chatbot を利用した上でメッセージ加工ができないかを模索した結果、できないことを知りこの方法に辿り着きました。そしてその方法を紹介しようとこのブログの8割ほどを執筆し終えた時に Chatbot のアップデートが発表され、なんとも言えない気持ちになったことは内緒です...)。この記事ではその方法についてご紹介したいと思います!

Before/After

まずメッセージの Before/After からお見せします。

ECS のタスク数が一定時間に必要な数から基準を下回った際に発生するアラートを例にご紹介します。

Before

Before の状態は Chatbot を通じて post されたデフォルトのメッセージとなっています。内容は充実しているのですが、Slack 通知をする上では情報過多な印象で、見慣れていなければどういった内容のアラートなのかを瞬時に把握し、対応するのはハードルが高いような印象でした。

After

After の状態は EventBridge を通じて加工されたデフォルトのメッセージとなっています。

メッセージの変更内容ですが、以下のような点を工夫しました。

  • 端的に何を示すアラートなのかを日本語で表現
  • 弊社の現場において Slack で通知する上で不要と判断した情報を除去
  • アラートに対するアクションを促せるような文言を追加

アーキテクチャ概要

タイトルや冒頭でも触れていましたが、AWS EventBridge を使用します。

重要となる構成要素は以下のラインナップです。

  • イベントパターン
    • AWS で発生したイベントを検知するためのパターン
  • ルール
    • AWS サービスのイベントを検知しターゲットへ送信するためのリソース
  • 入力トランスフォーマー
    • ターゲットへ渡すためのイベント(テキスト)をカスタマイズできる機能
  • ターゲット
    • 対象のイベントおよびパターンにマッチした際に送信先となるエンドポイント
  • API Destination
    • ターゲットに HTTP エンドポイントを指定できる機能

上記を組み合わせて作成した処理の流れをざっくりと説明すると以下のようになります。

実装手順

ここからは具体的な実装手順について解説します。構築時はコンソールでの作業を中心に進めていましたが、ここでは最終的に IaC へ落とし込んだ Terraform のコードをメインに解説させていただきますのでご了承ください。

Slack App を作成

Slack App が必要になるので、なければ作成をしてください。

https://api.slack.com/apps へアクセス後、下記の動線から作成が行えます。

Incomming Webhooks の作成

Incomming Webhooks を使用しますので、作成し取得しておきます。

下記の動線から作成&取得可能です。

書き込み権限の追加

Slack App へ chat:write の権限を追加します。

下記の動線から設定可能です。

Terraform の実装

下記はルールに関する定義です。

resource "aws_cloudwatch_event_rule" "demo_alert_alarm" {
  name = "demo-alert-alarm"
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
        }
      ],
      detail = {
        state = {
          value = [
            "ALARM"
          ]
        }
      }
    }
  )
}

マッチさせたい条件を HCL に記述し、event_pattern の値に定義しています。

下記はターゲットに関する定義です。

resource "aws_cloudwatch_event_target" "demo_alert_alarm" {
  arn      = aws_cloudwatch_event_api_destination.demo_alert.arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.demo_alert_alarm.id

  input_transformer {
    input_paths = {
      account     = "$.account"
      alarmName   = "$.detail.alarmName"
      description = "$.detail.configuration.description"
      reason      = "$.state.reason"
      region      = "$.region"
      time        = "$.time"
    }
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "#E01D5A",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "*<alarmName> - ECSの必要なタスク数が足りていません*"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "アラートの内容に注意してください。\\n必要であれば *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ>* から「 <alarmName> 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針>* を参考に今回の対応記録も残しましょう。)"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

input_transformer では、local.input_path_settings で local values の EventBridge の変数用の定義を指定し、input_template では post する Slack のメッセージのヒアドキュメントを定義しています。このヒアドキュメントの中で、EventBridge の変数を のように使用し、メッセージで展開が行えます。

下記はターゲットに付随するリソースに関する定義です。

resource "aws_cloudwatch_event_api_destination" "demo_alert" {
  connection_arn                   = aws_cloudwatch_event_connection.demo_alert.arn
  http_method                      = "POST"
  invocation_endpoint              = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" # webhook URL はここでは管理しない
  invocation_rate_limit_per_second = 300
  name                             = "demo-alert"
  lifecycle {
    ignore_changes = [
      invocation_endpoint
    ]
  }
}

resource "aws_cloudwatch_event_connection" "demo_alert" {
  authorization_type = "API_KEY"
  name               = "demo-alert"
  auth_parameters {
    api_key {
      key   = "Authorization"
      value = "dummy"
    }
  }
}

aws_cloudwatch_event_api_destination, aws_cloudwatch_event_connection では イベントの送信先と接続設定に関する設定を行なっています。aws_cloudwatch_event_api_destination の invocation_endpoint には Webhook URL の値が設定されます。aws_cloudwatch_event_connection の auth_parameters.api_key の設定は使用しないため、適当な文字列の設定で大丈夫です。また、注意点としてここでは説明をわかりやすくするために、invocation_endpoint をマスクした値でハードコードしているような実装になっていますが、セキュアな情報となりますのでこの辺りの管理や参照などは適宜安全な方法で行ってください。

最後は IAM に関する定義です。

EventBridge が ApiDestination へイベントを送信するための権限などを設定しています。

resource "aws_iam_role" "demo_alert" {
  assume_role_policy = jsonencode({
    "Statement" : [
      {
        "Action" : "sts:AssumeRole",
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "events.amazonaws.com"
        }
      }
    ],
    "Version" : "2012-10-17"
  })
  managed_policy_arns = [
    aws_iam_policy.demo_alert.arn,
  ]
  max_session_duration = 3600
  name                 = "demo-alert"
}

resource "aws_iam_policy" "demo_alert" {
  name = "demo-alert"
  policy = jsonencode({
    "Statement" : [
      {
        "Action" : [
          "events:InvokeApiDestination"
        ],
        "Effect" : "Allow",
        "Resource" : [
          "arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:api-destination/demo-alert/*"
        ]
      }
    ],
    "Version" : "2012-10-17"
  })
}

ここまで解説した内容の実装は、CloudWatch Alarm の state だと ALARM に関連する内容のものとなっています。ですが、実際にはアラートが復旧した場合の OK 通知も必要となる場合がほとんどでしょう。そのため下記のリソースは state ごとに 1セットで作成するので必要に応じて追加してください。(その他は共用で利用)

  • aws_cloudwatch_event_rule
  • aws_cloudwatch_event_target

最終的に出来上がった全体のコードはこちらになります。

resource "aws_cloudwatch_event_rule" "demo_alert_alarm" {
  name = "demo-alert-alarm"
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
        }
      ],
      detail = {
        state = {
          value = [
            "ALARM"
          ]
        }
      }
    }
  )
}

resource "aws_cloudwatch_event_rule" "demo_alert_ok" {
  name = "demo-alert-ok"
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
        }
      ],
      detail = {
        state = {
          value = [
            "OK"
          ]
        }
      }
    }
  )
}

resource "aws_cloudwatch_event_target" "demo_alert_alarm" {
  arn      = aws_cloudwatch_event_api_destination.demo_alert.arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.demo_alert_alarm.id

  input_transformer {
    input_paths = {
      account     = "$.account"
      alarmName   = "$.detail.alarmName"
      description = "$.detail.configuration.description"
      reason      = "$.state.reason"
      region      = "$.region"
      time        = "$.time"
    }
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "#E01D5A",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "*<alarmName> - ECSの必要なタスク数が足りていません*"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "アラートの内容に注意してください。\\n必要であれば *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ>* から「 <alarmName> 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針>* を参考に今回の対応記録も残しましょう。)"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

resource "aws_cloudwatch_event_target" "demo_alert_ok" {
  arn      = aws_cloudwatch_event_api_destination.demo_alert.arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.demo_alert_ok.id

  input_transformer {
    input_paths = {
      account     = "$.account"
      alarmName   = "$.detail.alarmName"
      description = "$.detail.configuration.description"
      reason      = "$.state.reason"
      region      = "$.region"
      time        = "$.time"
    }
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "#2DB57C",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "*<alarmName> - ECSの必要なタスク数が足りていません*"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "引き続きアラートに注意してください"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

resource "aws_cloudwatch_event_api_destination" "demo_alert" {
  connection_arn                   = aws_cloudwatch_event_connection.demo_alert.arn
  http_method                      = "POST"
  invocation_endpoint              = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" # webhook URL はここでは管理しない
  invocation_rate_limit_per_second = 300
  name                             = "demo-alert"
  lifecycle {
    ignore_changes = [
      invocation_endpoint
    ]
  }
}

resource "aws_cloudwatch_event_connection" "demo_alert" {
  authorization_type = "API_KEY"
  name               = "demo-alert"
  auth_parameters {
    api_key {
      key   = "Authorization"
      value = "dummy"
    }
  }
}

resource "aws_iam_role" "demo_alert" {
  assume_role_policy = jsonencode({
    "Statement" : [
      {
        "Action" : "sts:AssumeRole",
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "events.amazonaws.com"
        }
      }
    ],
    "Version" : "2012-10-17"
  })
  managed_policy_arns = [
    aws_iam_policy.demo_alert.arn,
  ]
  max_session_duration = 3600
  name                 = "demo-alert"
}

resource "aws_iam_policy" "demo_alert" {
  name = "demo-alert"
  policy = jsonencode({
    "Statement" : [
      {
        "Action" : [
          "events:InvokeApiDestination"
        ],
        "Effect" : "Allow",
        "Resource" : [
          "arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:api-destination/demo-alert/*"
        ]
      }
    ],
    "Version" : "2012-10-17"
  })
}

additional

ここまでは基本的な実装方法をお伝えしましたが、対象の CloudWatch Alarm リソースが増える度に定義が増えてしまう冗長な設計となっていました。応用として下記のように local values に CloudWatch Alarm ごとの固有の設定に関する定義を切り出し、loop させて DRY に実装する方法もおすすめです。ここでは詳しくは解説しませんが、一部のサンプルコードをご紹介しておきます。

locals {
  input_paths = {
    account     = "$.account"
    alarmName   = "$.detail.alarmName"
    description = "$.detail.configuration.description"
    reason      = "$.state.reason"
    region      = "$.region"
    time        = "$.time"
  }

  config = [
    {
      target_arn     = aws_cloudwatch_event_api_destination.demo_alert.arn
      name_prefix    = "demo-alert"
      prefix_pattern = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
      alert = {
        alarm = {
          main_message = "*<alarmName> - ECSの必要なタスク数が足りていません*"
          sub_message  = "アラートの内容に注意してください。\\n必要であれば *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ>* から「 <alarmName> 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針>* を参考に今回の対応記録も残しましょう。)"
          color        = "#E01D5A"
          state_type   = "ALARM"
        }
        ok = {
          main_message = "*<alarmName> - ECSの必要なタスク数が足りていません*"
          sub_message  = "引き続きアラートに注意してください"
          color        = "#2DB57C"
          state_type   = "OK"
        }
      }
    }
  ]
}

resource "aws_cloudwatch_event_rule" "demo_alert" {
  for_each = {
    for config in local.config : config.name_prefix => {
      name_prefix    = config.name_prefix
      prefix_pattern = config.prefix_pattern
      alert          = config.alert
    }
  }

  name = format("%s-%s", each.value.name_prefix, "alarm")
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = each.value.prefix_pattern
        }
      ],
      detail = {
        state = {
          value = [each.value.alert.alarm.state_type]
        }
      }
    }
  )
}

resource "aws_cloudwatch_event_target" "demo_alert" {

  for_each = {
    for config in local.config : config.name_prefix => {
      name_prefix    = config.name_prefix
      prefix_pattern = config.prefix_pattern
      alert          = config.alert
      target_arn     = config.target_arn
    }
  }

  arn      = each.value.target_arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.alarm[each.value.name_prefix].id

  input_transformer {
    input_paths    = local.input_paths
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "${each.value.alert.alarm.color}",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "${each.value.alert.alarm.main_message}"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "${each.value.alert.alarm.sub_message}"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

おわりに

今回は、EventBridge で CloudWatch Alarm のメッセージを加工し、Slack へ post する方法をご紹介しました。Lambda などを使わずとも柔軟にメッセージを加工できるのは個人的に発見でした。皆さんも機会があればぜひ試してみてください!

Go言語による並行処理の輪読会で行った、輪読会でワイワイするための工夫

こんにちは!@TOC です! 最近は「ゾン100〜ゾンビになるまでにしたい100のこと〜」というアニメにハマっており、見るたびに「やりたいことをやるんだ!」という気持ちになります🙌 キャラも魅力的なのでこれからが楽しみですね!

さて、今回は社内で行った「Go言語による並行処理」本の輪読会が最近完走したので、その様子をご紹介しながら、輪読会をより良いものにするために行った工夫点についてご紹介します。


目次


輪読会で読む本の選定

今回はオライリーから出版されている「Go言語による並行処理」という本を読む本として選定しました。

www.oreilly.co.jp

Goに関する本は数多く出版されていますが、この本はGo言語における並行処理に特化した本で、Goの並行処理を学ぼうと思った人は一度目にしたことがあるのではないでしょうか。

数あるGoの本の中からこの本を選定した理由は2つあります。

  1. 本の内容として難易度が高く、一人で読むと心が折れそうだから
  2. Goの強みでもある並行処理を業務で活かせるレベルに引き上げたいから

一つ目は少し弱気な理由にはなりますが、今回輪読会をやった感想としてはかなり大事なポイントかなと思っております。「Go言語による並行処理」本はかなり丁寧に解説されてるので、読んだらなんとなくわかった気になりますが、理解を深める・自分の中で昇華するまで一人で行うにはかなり骨が折れます。

以前、GoのLT会でこの本が紹介された時も「一人で読んで心が折れた」と言った意見が多く見られました。

その点、輪読会で一緒に読む仲間がいると進めようという気持ちも高まりますし、議論することで理解が深まりやすいです。なので、この本に限らず、興味あるけどなかなか読み進められない本は一緒に読む仲間を見つけるのは一つ良い方法かなと思います。

二つ目に関しては、現在コネヒトではテックビジョンとしてLet's Goというものを掲げております。

tech-vision.connehito.com

新たな武器としてGoを選定したわけですが、せっかくならGoの強みである並行処理についてちゃんと理解したい、業務で使えるようになりたい、という思いもあり、この本を選定しました。

どういう形式でやったのか

頻度としては隔週くらいで行いました。事前に読む章を決めて読んでおき、学びになったこと、疑問に思ったことを付箋に書いて各付箋について議論していきます。

輪読会の様子
わからない箇所について議論をしたり、後述するようにコードを実際に動かしてみて理解を深めたりしました。

こうやって議論をすることで一人だけでやるよりも理解を深められるのが輪読会のいいところですね!

輪読会を進める上での工夫

今回輪読した本は丁寧に書いてありつつも、理解に至るには難易度も高く進めるのも一苦労でした。そのため輪読会を進める上で工夫した点を紹介できればと思います。

1. 写経当番を決めて、実際に手を動かす

「Go言語による並行処理」は3章くらいからコード表記が多くなり、実践的な内容になってきます。並行処理は実際に動かしてみたり、コードを変えてみたりして挙動を確かめないと全然動きが想像できなかったりするので、3章以降では各パートで写経当番を決めて、各自写経してくることにしました。

写経したコードをコミットできるリポジトリを用意し、自分の担当した分のコードをコミットしていきます。

これは実際にコードを動かしながら学べるし、負担を分担できるため今回のケースとしてはかなり有用だったかなと思います。みんなやってるんだから自分もやらなきゃって気持ちになりますしね!

やはりある程度イメージしづらい内容だとコードで語る方が理解しやすくなる側面があるかと思うので、難易度が高かったりイメージしづらい内容の輪読会の場合、この方法はおすすめです。

2. 学んだことを実践的にアウトプットしてみる勉強会でワイワイ

輪読会を完走し、実際に写経しながら進めていたとはいえ、やはり 知っている使える には大きなギャップがあります。なので、テーマはなんでもいいから、実際に並行処理を書いてみてみんなでワイワイする勉強会を行おうという話になりました。

せっかくならガッツリやって、最後みんなで飲みにでも行こうという話になったので、有志で日曜日に会社に集まって、各々がテーマを決めて並行処理を書いてみる会を行いました。

テーマは以下のようなものがありました

  • PHPのEnumからGoのEnumに変換するConverterを並行処理を使って作成してみる
  • 並行処理を使って、複数のAPIレスポンスをまとめる処理を書いてみる
  • 並列処理のアルゴリズムを自分で作ってみる
  • プッシュ通知処理をゴルーチンを使って実装してみる

それぞれテーマが興味深く、かつ具体性もあったので取り掛かりやすかったのかなとも思います!

まずはみんなでワイワイランチ!

ランチの様子
お腹を満たしたら、それぞれもくもく作業!

2時間やったら中間進捗発表して、さらに2時間やって最終発表といった感じでメリハリをつけてもくもくすることができました。

ちょっとした発表時間があると、進めようという程よい緊張感もあり、集中して取り組めたと思います。 発表は実際にデモをしてみたり、ホワイトボードの前で解説したりとみんな進捗でてて最高でした!(写真撮り忘れた…)

勉強会も終わってパシャリ
ちょっとしたプチ開発合宿みたいな感じで楽しかったです!

最近はオフラインの場も増えてきたので、こういった機会をたまに設けると学びが加速しそうですね!

最後に

今回は「Go言語による並行処理」本の輪読会を有意義にするために行ったことを紹介させていただきました。

この輪読会は途中で日程が合わずに継続が危ぶまれる時もありましたが、なんとか完走することができました(実際開始から終了までのリードタイムとしては10ヶ月ほどかかっています…笑)。

難しい部分もありつつ、工夫もしながら完走できたのはすごい自信にも繋がったかなと思うので、また10月からも新しい本の輪読会に取り組む予定です!

みなさんも一人で読むと心が折れそうになる本は仲間を見つけてワイワイしながら取り組んでみるのはどうでしょうか?🙌

みんなで打ち上げ!
お疲れ様でした!!

DroidKaigi 2023 Day3 参加レポート

こんにちは!Androidエンジニアの関根です。

2023/09/14から3日間、DroidKaigi 2023が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、僭越ながらレポートをします。 少しでもAndroid開発の盛り上がりに貢献できたら嬉しく思います。

3日目は、これまでと趣向を変えてコミュニケーションを中心にしたコンテンツが行われました。 一覧にすると以下の通りです。

  • Codelabs / コードラボ -
  • Career Panel Discussion / キャリア・パネルディスカッション -
  • Career Advice Sessions / キャリア相談会 -
  • Meetups on different topics / 特定のトピックについてのミートアップ -

わたしは、コードラボとキャリア・パネルディスカッションに参加しましたので、2つのコンテンツを中心に感想を書かせていただきます。 補足ですが、バリスタの方が作るカフェラテの提供があり、リラックスして参加できました。

※Day1、Day2の様子と、バリスタの方のお店のWebサイトをAppendixに載せているので、そちらもお読みください。

キャリアパネルディスカッション

さまざまなバックボーンを持つパネリスト4名の、パネルディスカッションです。

2023.droidkaigi.jp

  • キャリアプランを立てるかどうか
  • キャリア形成をする上での行動や心構え
  • プライベートとキャリア

など、日常的には聞けないテーマが多く取り上げられました。slidoを使った質疑応答もあり、技術とは異なる観点で、コミュニティを支えるコンテンツだったと感じています。内容に共感しながら、業界を支えている方々の姿勢に触れ、背筋が伸びる思いがしました。

Codelab

Codelabsの中から選定された、3つのコースが用意されており、完了するとプレゼントがもらえるというコンテンツです*1

2023.droidkaigi.jp

わたしは「Jetpack Composeの基本」というコースを選んで取り組みました。

developer.android.com

じっくりとJetpackComposeに触れる機会を作れていなかったので、純粋に良い機会になりましたし、ランチタイムから同席になった方々と、お互いの作業内容の共有をしながら取り組めました。JetpackComposeやマルチモジュールの導入状況など、業務上での裏話を情報交換をできたので、貴重な時間になりました。

参加したコンテンツ以外にも、魅力的なコンテンツがありましたので、紹介しておきます。

Meetup

いくつかのテーブルに分かれ、特定のテーマを元に交流するコンテンツです。

2023.droidkaigi.jp

わたしはCodelabsに集中していたため、時間切れとなり、参加できませんでしたが、終始人が集まり交流が行われていました。オンライン主流の日常では、得難い機会であり、オフラインならではのコンテンツだと感じました。

キャリア相談会

パネルディスカッションのパネリストの方々に、少人数で、キャリア相談ができるコンテンツです。

2023.droidkaigi.jp

キャリアの悩みは、所属会社だけでは解決できないケースもあると思うので、コミュニティで相談できることは心強いと感じました。満席のアナウンスもあり、盛況だったようです。

オフィスツアー

スカラーシッププログラムの参加者向けのコンテンツで、DroidKaigiに協賛している企業のオフィスを見学するツアーです。

medium.com

コミュニティの一員として、学生を支援していることに、強く意義を感じます。オフィスツアーから戻った学生の方々を、拍手で迎えることもAndroidコミュニティの温かさを実感しました。

まとめ

スピーカーの皆様、スタッフの皆様、そして参加された皆様、3日間お疲れ様でした!

1日目、2日目では、技術的な見識を深く得られましたし、企業ブースではさまざまな業種の方々から、貴重な開発事例をお聞きできました。3日目には、Androidコミュニティの方々と、情報交換やもくもくコーディングできたので、有意義に過ごさせていただきました。

スポンサー一覧を見ると、モバイルアプリ以外への広がりを感じますし、キャリアについて相談したり、スカラーシップでの学生支援など、Androidコミュニティを支えるイベントに、より一層進化していることを実感しております。

DroidKaigiを支えてくれている皆様に、心から感謝です!また来年お会いしましょう!

Appendix

tech.connehito.com

tech.connehito.com

alphabetticafe.com

*1:完了しましたが、プレゼントをもらい忘れてしまいました・・・

DroidKaigi 2023 Day2 参加レポート

こんにちは!Androidエンジニアの関根です。

2023/09/14から3日間、DroidKaigi 2023が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、僭越ながらレポートをします。

本日は、2日目にわたしが聴講したセッションを紹介します。後日アーカイブ動画公開後に更新していく予定です。 少しでもAndroid開発の盛り上がりに貢献できたら嬉しく思います。

ビジネス向けアプリを開発するときに知っておくべきAndroid Enterpriseの世界

最初は、Yusaku Tanakaさんによるモバイルデバイス管理(MDM)を利用した開発についてのセッションです。

MDMに関して、システム構成や配布方法、実装方法など始まりから終わりまでを理解できる内容になっていました。 相対的に事例を多く聞ける事例ではないので、初めての知見ばかりで、新鮮に聞かせていただきました。 すぐにでもMDMを利用して社内ツールの提供をすることもできそうに感じています。

speakerdeck.com

ビジネス向けアプリの開発を予定している方、社内用のアプリを作りたい方におすすめしたいです。

Flutterにおけるアプリ内課金実装 -Android/iOS 完全なる統一-

弊社の中島さんのFlutterを用いたアプリ内課金の定期購入についてのセッションです。 Androidの関数と対比しながら解説して頂き、Flutter未経験の方にも理解しやすい内容でした。 終始落ち着いて発表されていて、改めて一緒に働けることを心強く思いました。中島さん登壇お疲れ様でした!

speakerdeck.com

Flutterで定期購入機能の導入を検討中、あるいはアプリ内課金機能があるアプリのFlutterへのリプレイス を検討中の方におすすめしたいです。

Androidアプリの良いユニットテストを考える

Nozomi Takumaさんによる、Androidアプリ開発でのユニットテストに関するセッションです。

良いユニットテストの定義を提示して頂いた上で、テストの種類ごとにおける実行速度の対比や、 テストダブル利用時の考慮すべきトレードオフ、テスト実装時の手法など、網羅的でわかりやすい内容でした。テストコードはプロダクトコードとは違う知識や思考が必要だと思うので、解説いただいた内容をもとにチームでの議論もしやすくなると感じました。

speakerdeck.com

ユニットテストを書いていない、もしくは書いているがもう一歩踏み込んで取り組みたい方におすすめです。

レイヤードアーキテクチャーでの例外との向き合い方

Yukihiro MoriさんによるMVVMをベースにした例外の扱いに関する発表です

レイヤーごとの例外の対処方法や、ユーザーへの例外の伝え方のパターンと実装方法など、開発上の関心を総合的に解説した内容です。 実装方法とアーキテクチャ図を合わせて紹介していただき、理解しやすい内容で、実装内容を具体度高く聞けたので、弊社にも取り入れていきたいと思います。

speakerdeck.com

なんとなく例外を扱っている、例外の種類が多くハンドリングに困っているという方にお勧めしたいです

できる!アクセシビリティ向上

Ogura Yuriさんによるアプリのアクセシビリティに関するセッションです。

そもそものアクセシビリティの定義や、TalkBackとスイッチアクセスの機能を、実際のデバイスで操作するデモから始まり、知識がない状態でも安心して聞けました。 デモの後は施策立案〜リリースまでのプロセスと、明日にでもできる容易な改善手法を解説いただいたので、導入時に参考にできることを多く知れたと思います。

speakerdeck.com

アクセシビリティに対して見識がない方にお勧めしたいです。

まとめ

スピーカーの皆様、スタッフの皆様、2日目もお疲れ様でした!

今日は企業ブースもいくつか回らせていただき、久しぶりにオフラインでの情報交換ができ、楽しませてもらいました。 今回は、NISSANさんの企業ブースでモビリティアプリを触らせていただくなど、スマートフォン以外を扱う企業参加も増えている印象で、Androidコミュニティのさまざまな広がりを感じました。 明日もよろしくお願いします!

最後にコネヒトでは一緒に働く仲間を募集しています! 興味持っていただけた方は気軽にご連絡ください!

www.wantedly.com

CakePHP4.3から非推奨になったFixtureのテーブル定義の問題を解決しました

こんにちは。

今回はCakePHPのバージョン4.3から非推奨機能に追加されたTestFixture の対応をしたのでバックエンドエンジニアの共同制作でブログに書いてみました。

冒頭〜導入手順5までは高橋で、手順6以降は西中が書いております。

以前PHP8.1にアップデートした際にCakePHPのバージョンも4.2から4.3にアップデートしました。

その時のブログはこちら↓

https://tech.connehito.com/entry/2023/03/31/195819

この時からテストを実行する度に以下のような警告が出るようになりました。

Deprecated Error: You are using the listener based PHPUnit integration. This fixture system is deprecated, and we recommend you upgrade to the extension based PHPUnit integration. See https://book.cakephp.org/4/en/appendices/fixture-upgrade.html
/var/www/html/server/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php, line: 79

対象のリポジトリは元々CakePHP3系で作られていたので、テストに使うテーブル定義はFixtureクラスの中に定義されていました。 ですが、CakePHPのマイグレーションを利用しておらず、いわゆるマスタとなるようなテーブル定義は別のリポジトリで管理されています。

そのため、テーブル定義が二つの場所にある二重管理状態になっていました。

CakePHPの公式ドキュメントではCakePHPのマイグレーションを使った方法については詳細に記述されているのですが、DDLファイルを使った方法は簡潔に記述しかありません。ネットで探してもあまり見つからなかったので、同じように躓いた人の役に立てれたら嬉しいなと思っています。


目次


前提

対応方法をご紹介する前に前提条件をお伝えします。

  • PHP: 8.1
    • CakePHP: 4.3
    • PHPUnit: 9.6.5
  • AWS
    • ECS
    • Amazon Aurora MySQL v2(MySQL5.7)
  • DBのスキーマ管理はアプリケーションコードとは別リポジトリ
    • マイグレーションツールは Ridgepole
  • GitHub Actions

導入手順

mysqldumpによって取得したDDLファイルからテストのテーブル定義を使うように変更しました。

1. アプリケーションリポジトリにDDLファイルを/config/schema配下に置く

DDLファイルは以下のSQLコマンドで生成しました。

mysqldump -u[ユーザー] -h[ホスト] -P[ポート] -p[パスワード] --no-data --skip-column-statistics --no-create-db [データベース名] > base_test.sql

FixtureやFactoryでレコードを追加する際に、AUTO_INCREMENTの値が1ではない場合主キーが重複してしまうため、awkコマンドを使って初期値を変換しました。

awk '{ gsub(/AUTO_INCREMENT=[0-9]+/, "AUTO_INCREMENT=1"); print }' "base_test.sql" > "test.sql"

2. phpunit.xmlを変更する

phpunit.xml から <listeners> ブロックを削除し、以下の内容を phpunit.xml に追加しました。

ref: https://book.cakephp.org/4/en/appendices/fixture-upgrade.html

<extensions>
    <extension class="\Cake**\T**estSuite\Fixture\PHPUnitExtension" />
</extensions>

3. tests/boostrap.phpに追記する

1で置いたDDLファイルを使用してテーブル定義を取得します。

ref: https://book.cakephp.org/4/ja/development/testing.html#creating-test-database-schema

$testSqlFile = dirname(__DIR__) . '/config/schema/test.sql';
(new SchemaLoader())->loadSqlFiles($testSqlFile, 'test');

4. Fixtureクラスからテーブル定義を削除する

実施したリポジトリは元々CakePHP3系で作られていたので、テストに使うテーブル定義はFixtureの中に定義されていました。 段階的なアップグレードを行っていたため、Fixtuer内にまだテーブル定義は残っている状態です。 そのため、まずは各Fixtureクラスからごっそりテーブル定義ブロックを削除していきます。

<?php
namespace App\Test\Fixture;

use Cake\TestSuite\Fixture\TestFixture;

/**
 * ArticlesFixture
 *
 */
class ArticlesFixture extends TestFixture
{
    /**
     * Table name
     *
     * @var string
     */
    public $table = 'articles';
    public $connection = 'test_hoge';

-    /**
-     * Fields
-     *
-     * @var array
-     */
-    // @codingStandardsIgnoreStart
-    public $fields = [
-        'id' => ['type' => 'integer', 'length' => 11, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null],
-        'title' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => '', 'collate' => 'utf8mb4_general_ci', 'comment' => '', 'precision' => null, 'fixed' => null],
-        'description' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => '', 'collate' => 'utf8mb4_general_ci', 'comment' => '', 'precision' => null, 'fixed' => null],
-        'status' => ['type' => 'integer', 'length' => 3, 'unsigned' => false, 'null' => false, 'default' => '0', 'comment' => '', 'precision' => null, 'autoIncrement' => null],
-        'created' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
-        'modified' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
-        '_indexes' => [
-            'index_status' => ['type' => 'index', 'columns' => ['status'], 'length' => []],
-        ],
-        '_constraints' => [
-            'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
-        ],
-        '_options' => [
-            'engine' => 'InnoDB',
-            'collation' => 'utf8_general_ci',
-        ],
-    ];

    /**
     * Records
     *
     * @var array
     */
    public $records = [
        ︙
    ];
}

また、$recordsの中に配列がある場合、今まで$fieldsのtype指定していたのでこのように独自でjson形式にしなければなりません。

$records = [
    [
        'id' => 1,
        'urls' => [
            0 => [
          'type' => 1,
        'url' => 'https://www.example1.co.jp',
      ],
      1 => [
          'type' => 2,
        'url' => 'https://www.example2.co.jp',
      ],
        ],
    ],
    [
        'id' => 2,
        'urls' => [
            0 => [
          'type' => 1,
        'url' => 'https://www.example1.co.jp',
      ],
      1 => [
          'type' => 2,
        'url' => 'https://www.example2.co.jp',
      ],
        ],
    ],
];
// テストで使用するのにjson形式に変換する
foreach ($records as $seq => $record) {
    if (is_array($record['urls'])) {
       $records[$seq]['urls'] = json_encode($record['urls']);
    }
}
$this->records = $records;

以下のように直接json形式にするでも大丈夫です。

$records = [
    [
        'id' => 1,
        'urls' => '[{"type": 1,"url": "https://www.example1.co.jp"},{"type": 2,"url": "https://www.example2.co.jp"}',
    ]
    [
        'id' => 2,
        'urls' => '[{"type": 1,"url": "https://www.example1.co.jp"},{"type": 2,"url": "https://www.example2.co.jp"}',
    ]
];

5. ユニットテストを回してみる

今回実施したリポジトリではDockerコンテナを利用しているので、コンテナの中に入ってPHPUnitを回すようにしていました。

元々、composer.json 内でPHPUnitを定義しているため、composer から呼び出せるようになっています。

    "scripts": {
        "post-install-cmd": [
            "App\\Console\\Installer::postInstall"
        ],
        "post-create-project-cmd": "App\\Console\\Installer::postInstall",
        ︙
        "test": "phpunit --colors=always"
    },

テストコードの件数が少ない場合は気にしなくて良いのですが、テスト件数が多くテスト実行後に結果を見ようとするとターミナル上で見切れてしまうことが多々あったので実行結果はテキストファイルに書き出すようにしていました。 その時、composerのタイムアウトが発生してしまうことがあるので予め環境変数を上書きしてタイムアウトが起きないように設定した上でテストを実行し、出力したファイルを見比べながらテストコードを実情にあった形で直していきます。

$ export COMPOSER_PROCESS_TIMEOUT=0
$ composer test > text.log

CakePHPのFixtureはどうやら主キーやユニークキーが重複してしまうFixtureのレコードも許してしまい、レコード重複エラーが発生してしまうという事象が頻発してしまいました。

このリポジトリではFixture Factoriesを利用しているので、重複が出ないようにテストケース内でFactoryクラスを使ってレコードを作成するように書き換えることで既存のテストに影響が出ないように修正を行いました。

ref: https://tech.connehito.com/entry/2022/07/22/100000

これで一応Fixtureの警告は消えましたが、これだとテーブル定義を変更した場合にアプリケーションリポジトリのDDLファイルを手動で更新しなくてはなりません。

テーブル定義を管理しているリポジトリに変更があった場合に自動でAWSのS3にDDLファイルをアップロードし、アプリケーションリポジトリでそのDDLファイルをダウンロードしてくるという仕組みを作ることになりました。

6. 自動化

6.1. DDLファイルをS3のバケットにアップロードする

テーブル定義変更の度にリポジトリ内のDDLファイルの変更を行わないといけないだけでなく、弊社の場合、複数のリポジトリで同じDBを参照しているということが多々あるため、スキーマ定義の変更の度に各リポジトリ内のDDLファイルを書き換えるのは手間だという問題もありました。

今回の本題であるスキーマ定義のDDLファイルをダウンロードする仕組みは、正にこの問題を解決する手段として考えたものでした。

弊社では前述の通り、 Ridgepole を使ってスキーマ定義を管理しているのでスキーマ定義の変更が発生したタイミングでDDLファイルを作成し、S3のバケットにアップロードする仕組みを作成しました。

簡単に流れを説明するとこのようになっています。

  1. 開発者がテーブル定義を変更し、スキーマ定義管理用のリポジトリに push します。
  2. スキーマ定義管理用のリポジトリに設定されているGitHub ActionsがDDL反映処理の実行のためのECSを起動します。 この一連の処理については過去にブログで投稿しているので、そちらを参考にしてください。 refs: https://tech.connehito.com/entry/2019/10/08/165500
  3. 前述のDDL反映を行います。
  4. ECS内でmysqldumpコマンドを実行します。
  5. 各スキーマのDDLファイルを取得し、ローカルに保存します。 この記事の冒頭で紹介させていただいたawkコマンドを使ってDDLファイルの一部を書き換える処理も行っています。
  6. S3にファイルをアップロードします。

S3へのアップロードフロー

6.2. S3のバケットからDDLファイルをダウンロードする

「S3のバケットからDDLファイルをダウンロードする」ということは決まったのですが、「じゃあどのタイミングでDDLファイルをダウンロードするのが適切なんだろう?」という課題が出てきました。

そこで考えたのがこの3つのタイミングでした。

  • テスト実行時にDDLファイルをダウンロードする
  • DockerビルドのタイミングでDDLファイルをダウンロードする
  • Dockerコンテナを立ち上げたタイミングでDDLファイルをダウンロードする

テスト実行時にDDLファイルをダウンロードする

最初に思いついたのはこの方法です。 毎回テストの度に最新のDDLファイルをダウンロードすれば最新のスキーマ定義でテストが実行できるというメリットはあります。

ですが、

  • 通信エラーでS3のバケットからDDLファイルをダウンロードできなかった場合にテストが動かなくなってしまう
  • スキーマ定義の変更は頻繁に行われるわけではないので、毎回DDLファイルをダウンロードするのは無駄

という問題があったため、却下となりました。

DockerビルドのタイミングでDDLファイルをダウンロードする

Dockerコンテナをビルドするのはそんなに頻繁ではないので一見良さそうに思えます。

ですが、

  • テストコードは本番環境には不要なため、不要なファイルがDockerイメージに含まれてしまう
  • DDLファイルを含める分、Dockerイメージが大きくなってしまう
  • 本番環境には不要なファイルをダウンロードするために、DockerイメージをビルドするためのCIの設定にAWSのキーを設定しないといけない

という問題があったため、こちらも却下となりました。

Dockerコンテナを立ち上げたタイミングでDDLファイルをダウンロードする

ローカルでDockerコンテナを立ち上げる際に必ず実行するシェルファイルがあるため、そちらにDDLファイルをダウンロードする処理を追加しました。

ローカルのDockerコンテナは何かライブラリの更新・変更のような大きな変更があったらビルドし直すということもあり、いまの開発スタイルではこのタイミングが適切だろうということになり、このタイミングでDDLファイルをダウンロードすることになりました。

また、「なんかスキーマ定義合ってないかも?」となった時でも「まず最初にDockerコンテナを立ち上げ直してみよう」という手段が取れるので、問題解決の第一歩の負担が重くないことも良さそうだねという話になりました。

実際のシェルの処理とは異なりますが、以下のように aws-cli の aws s3 cp コマンドでS3のバケットからダウンロードし、 tests/bootstrap.php で読み込めるようにしました。

# copy table schema files from s3
aws s3 cp s3://[DDLファイルのあるバケット]/メインで使うDB.sql /tmp/ && \
aws s3 cp s3://[DDLファイルのあるバケット]/連携して使うDB1.sql /tmp/ && \
aws s3 cp s3://[DDLファイルのあるバケット]/連携して使うDB2.sql /tmp/ && \
aws s3 cp s3://[DDLファイルのあるバケット]/連携して使うDB3.sql /tmp/

cp /tmp/*.sql /var/www/html/config/schema/

弊社のリポジトリはローカルのディレクトリを /var/www/html ディレクトリにマウントする形にしているので、マウントの対象外の /tmp ディレクトリにDDLファイルをダウンロードし、実際に読み込むファイルはマウント後のディレクトリにコピーしたものを利用する形にしています。

// Load one or more SQL files.
$ddlFiles = [
    'test' => 'メインで使うDB.sql',
    'test_sub1' => '連携して使うDB1.sql',
    'test_sub2' => '連携して使うDB2.sql',
    'test_sub3' => '連携して使うDB3.sql',
];

$schemaLoader = new \Cake\TestSuite\Fixture\SchemaLoader();
foreach ($ddlFiles as $schema => $file) {
$fullPath = dirname(__DIR__) . '/config/schema/' . $file;
    if (!file_exists($fullPath)) {
        // ファイルが存在しない場合は /tmp ディレクトリからコピーしてくる
        copy('/tmp/' . $file, $fullPath);
    }
    $schemaLoader->loadSqlFiles($fullPath, $schema);
}

まとめ

スキーマ定義をダウンロードできる仕組みを入れ、単体テストで利用できるようにしました。

これにより、最新のスキーマ定義でテストを実行できるようになり、テストコードとデータベース構造の整合性を維持できるようになりました。また、スキーマ定義の変更に柔軟に対応できるため、開発のスピードアップにもつながっていくことが期待できます。

他のリポジトリでも同じDBを参照しているため、横展開していくことで開発効率・開発者体験の向上が見込めそうです!

コネヒトでは一緒に働く仲間を募集しています!

そして興味持っていただけた方は気軽にご連絡ください!

https://www.wantedly.com/companies/connehito/projects

DroidKaigi 2023 Day1 参加レポート

こんにちは!Androidエンジニアの関根です。

2023/09/14から3日間、DroidKaigi 2023が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、末筆ながらレポートをします。

1日目に、わたしが聴講したセッションを紹介し、後日アーカイブ動画が公開されたら、リンクを貼りたいと考えています。 少しでも、Android開発の盛り上がりに貢献できたら嬉しく思います。

これで安心! Compose時代のDon't keep activities対応

最初はuponさんによるComposeの実装と絡めたActivity破棄に対処方法をまとめた発表です。

Activity破棄が起こる要因や対処方法が、わかりやすくまとめられおり、サービスの特性によってどこまで対応するかなど、判断軸も話されていて、あまり社外の事例は知れないため、参考になりました。

2023.droidkaigi.jp

Compose導入にあたりActivity破棄の対処方法を見直したい方に、おすすめしたいと思います。

Unleashing the Power of Android Studio

mhidakaさんによる、直近のAndroid Studioの追加機能をまとめた発表です。

直近数回のAndroid StudioのアップデートをUI Tool、Build System、Inspectの区切りで解説いただきました。 アップデートをまとめていただいたことで、現状と今後の方向性がよくわかる内容になっています。 特に、Network Inspectorで通信内容をインタセプトし改変できるようになったのは革命だと思いました。

speakerdeck.com

最近Android Studioのアップデートを追えてなかったなという方におすすめです。

iOSとAndroidで定期購入の意図しない解約を防ぐ

Ryo YamazakiさんとYuta Satoさんによる定期購読の解約に関する発表です。 定期購読中のクレジットカードの決済エラーなどによる、非自発的解約をAndroidとiOSそれぞれの機能と実装を詳細に解説した内容です。 弊社のママリアプリでも定期購読の機能を提供しているため、改めて見直したいと感じました。

2023.droidkaigi.jp

定期購読を導入している方、これから導入を検討している方におすすめしたいと思います。

YouTubeのLive配信をリリースするまで

yurihondaさんによるYouTubeのLive配信についての発表です。

事前準備からリリースまでを網羅的に解説されいて、サンプルアプリのコードを元にした解説が、わかりやすかったです。 躓きやすいところも共有していただき、実装にこの発表を見ておくと安心して取り組めそうです。

speakerdeck.com

Live配信は、昨今新しいコンテンツやビジネスとして盛り上がっているため、 実装機会も増えてくると思うので、現時点で予定がない方に、ぜひおすすめしたいです。

Master of Nested Scroll

すいみーさんによるネストスクロールに関する発表です。

JetpackComposeとAndroidViewでのそれぞれの解説に加え、併用する際の実装方法も解説いただきました。 ネストスクロール問題は、AndroidViewから悩みの種でCompose時代の解決策を知れたので、これから導入する場合の考慮すべきことが明確になりました。

speakerdeck.com

Composeへの移行検討中で、ネストスクロール問題を整理したい方お勧めしたいと思います。

まとめ

スピーカーの皆様、スタッフの皆様、1日目お疲れ様でした!

今回参加してJetpackComposeを当たり前に利用している事例が増えていることを実感しました。 弊社でも、そろそろJetpackCompose導入に向けて、計画を立て始めたところなので、よりモチベーションが高まりました。 1日目は、発表を聞く機会が多かったので、2日目は企業ブースにもお邪魔したいと思います。

明日は7月に弊社に参画した中島さんも発表されるので、応援も兼ねて2倍楽しみたいと思います。

2023.droidkaigi.jp

最後にコネヒトでは一緒に働く仲間を募集しています! 興味持っていただけた方は気軽にご連絡ください!

www.wantedly.com

第2回 リーン開発の現場輪読会 プロセス改善や WIP についてワイワイ編

こんにちは!コネヒトのプラットフォームグループでインフラエンジニアをしている @sasashuuu です。最近は VIVANT というドラマにハマっており、クラマックスに向けて目が離せず、日曜が待ち遠しい今日この頃です。

本日は、社内で実施中のリーン開発の現場という本の輪読会の様子についての記事の第2段です!(前回記事: 第1回 リーン開発の現場輪読会 技術課題についてワイワイ編

中盤から終盤へ差し掛かり、輪読会もヒートアップしてきましたので得られたことを本記事でお伝えしていければと思っております。

ちなみに前回の記事でも説明しましたが、現在リーン開発の現場という本を輪読しています。アジャイルソフトウェア開発手法のひとつであるリーンソフトウェア開発手法を、スウェーデンの警察機関のシステム開発で事例をもとに解説した本で、カンバンシステムを中心に書かれています。

リーン開発の現場 カンバンによる大規模プロジェクトの運営 | Henrik Kniberg, 角谷 信太郎, 市谷 聡啓, 藤原 大 |本 | 通販 | Amazon

今回の輪読会では、9 ~ 16章までが対象でした。進め方は前回同様に参加者は事前に各章の内容を読み、「思ったこと」「あるある」「やってみたい」の3つの観点で付箋を miro に書いてくるようにし、当日はそれらをもとにワイワイ話し合うというワークを実施しました。

輪読会でワイワイと話したこと

今回も書き出された付箋から、いくつか「特に話したいこと」をピックアップし、ワイワイと話しました。

今回は以下のようなラインナップのテーマがフォーカスされました。

外部ファシリテータの話

これは「第10章 継続的プロセス改善」で触れられている内容の、チームのでふりかえりの際(スプリント等)に、外部からファシリテーターを連れてきてファシリテートをしてもらうというやり方に着目した際の議論です。 チームにとっても外部ファシリテーターにとっても新たな気づきがあるという点や、チームリーダーがファシリテートを行わないことでじっくりと参加することができるメリットなどが本書では解説されていました。

以下、議論をした際のコメントを抜粋しておきます。

  • チーム外のファシリをすることでお互いのチームに良さそう
  • 自分のチームはファシリテーターは毎回違うが同じようなやり方になっているので、他のチームメンバーがファリシテーターをすることで他の課題の解決の仕方の視点が取り込めそう
  • チームのフェーズも関係しそう。 成熟しているチームは、やり方が固まっているところを見つめ直せるかもしれない。外部ファシリテーターでコンサル的な効果が見込めそうに思う
  • 純粋に他のチームのやり方を知りたい
  • 他のチームの雰囲気を知るのは良さそうだ

WIPとバッファを区別する話

これは「第11章 WIP をマネジメントする」で触れられている内容の、WIP(仕掛かり作業)とバッファを区別するというものに着目した際の議論です。本書ではカンバンボード上のバッファは待ち状態を意味するため、無駄だと解説されており(ただし、開発とテストの間のバッファなど、必要である小さなバッファは例外)、それらを区別しておくことが重要だとされていました。

以下、議論をした際のコメントを抜粋しておきます。

  • WIPとバッファなど時間の使い方や仕事の積み方を意識して使えると今後にとって良くなりそう
  • 職種が分かれていることで、WIPが増えるという状況はあると思う。(ネイティブとバックエンドの開発)
  • 一旦スタブを作って、ネイティブアプリとの連携部分を先に進めるということはやっている(ネイティブとバックエンドの開発)

サイクルタイム計測の話

これは「第12章 プロセスメトリクス」で触れられている内容の、ある作業が完了するまでにかかった時間を計測するサイクルタイムに着目した際の議論です。本書では、ほとんどの人は機能の開発にどれくらいの時間がかかって気がついておらず、実際に開発に費やした時間を知ると恐怖に近い感覚を覚えると書かれていました。また、サイクルタイムを可視化することで WIP 制限のテクニックを行うことで、サイクルタイムの短縮が見込めると解説されていました。

以下、議論をした際のコメントを抜粋しておきます。

  • 定めたポイントをもとに開発タスクの消化量を計測することはやっている
  • ベロシティの計算はやっている
  • サイクルタイムはフロー効率を可視化する意図がありそうに思った 。優先順位が高いものをリリースできているかどうかということの指標になりそう
  • WIPの制限とセットでこの指標を取ると良さそう
  • 作業日数/経過日数で出せると良さそう。実際サイクルタイムはどう取ると良いのだろう?
  • 本書では着手日と終了日を付箋に記入していたようだ、アナログでやっても良いかも
  • Notionだとプロパティで計算できそうだが、みんな使っていない?
  • Zenhubにはreport機能でそういうものがあったかもしれない

テスターからのFBの話

これは「第9章 バグをさばく」で触れられている内容の、バグの対応の際に機能開発チームに存在するテスターと開発者が毎日一緒に働いている様子に着目した際の議論です。

以下、議論をした際のコメントを抜粋しておきます。

  • QAエンジニアの人がいると良さそうに思っている。仕様にFBをもらえたりQAツールに 詳しかったり、テストの行程からFBをもらえると良さそうだ
  • QAエンジニアの採用は計画していないが、最近はMagicPodの導入は検討している

記入された付箋の数々

議論にはあがらなかったものの、それぞれの章で挙がった付箋も紹介しておきます。

第9章 バグをさばく

第10章 継続的プロセス改善

第11章 WIP をマネジメントする

第12章 プロセスメトリクス

第13章 スプリントとリリースの計画

第14章 バージョン管理の方法

第15章 アナログなカンバンボードを使う理由

第16章 僕たちが学んだこと

輪読会実施後の組織の変化

先述した外部ファシリテーターの件は、社内の MTG でも取り入れようというムーブが...!?

最後に

今回は、第2部 9~ 16章の内容を輪読会の様子を発信しました!次回はラストパートの第3部 17~ 21章です!お楽しみに!

本輪読会の本が気になった方はぜひ手に取ってみてください。

リーン開発の現場 カンバンによる大規模プロジェクトの運営 | Henrik Kniberg, 角谷 信太郎, 市谷 聡啓, 藤原 大 |本 | 通販 | Amazon