Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

Next GenerationなAndroidアプリのデプロイSlack Appを作ってみた

この記事はMoney Forward Engineering 2 Advent Calendar 2023 18日目の投稿です。
15日目はtatsuo48さんの TerraformコードのレビューをAIにさせてみた でした。

こんにちは。Androidエンジニアの宮本です。
マネーフォワード クラウド確定申告アプリの開発を担当しています。

本記事ではSlackの「次世代プラットフォーム機能 (以下、next-generation platformと呼びます)」 を使って、AndroidアプリをFirebase App Distributionにデプロイするまでの自動化について紹介します。

以下のようなイメージのSlack Appを開発しました。

現在の運用方法

確定申告Androidアプリでは現在以下のようなフローで開発したもののQA確認を実施しています。

  1. エンジニア間でコードレビューを実施、approveをもらう
  2. Asanaで管理しているチケット内でQAメンバーに連絡
  3. QAメンバーがCircleCIのWebページからデプロイPipelineをトリガー
  4. Firebase App Distributionにアップロードされたアプリを検証端末にインストールして動作確認

以前はGithub Actionsを使っていたこともありましたが、アプリのビルド時間が長くなってきたことを踏まえて、よりリソースの大きいCircleCIをメインで使っています。

Firebase App DistributionにデプロイするPipelineをトリガーする際は、ダッシュボードの上部にあるブランチフィルターを使用してブランチを選択したあと、パラメータを指定して実行します。

この運用方法について、都度PipelineをトリガーするためにCircleCIのWebページを開き、手入力でパラメータの入力が必要な点に注目しました。
一部のパラメータは決められた値を入力する必要がありますが、CircleCIのWebページ上ではプルダウンなどのリッチな入力方法が無いため、打ち間違いが起きる可能性があります。

そこで、普段から使用しているSlackからパラメータを入力してPipelineをトリガーできるSlackアプリを作成することを検討しました。

next-generation platformを使ったSlackアプリの開発

現在、Slackアプリを開発するための手法は3つあります。

next-generation platformで開発

  • メリット
    • Slackが管理するサーバーレスのインフラストラクチャにデプロイできるため、AWSなどのクラウド環境を用意する必要がない
    • ローカルでの開発中は、自動でソースコードの変更がアプリに反映される
  • デメリット
    • Slackのフリープランでは利用できない
    • 開発言語・フレームワークが1択しかない (TypeScript×Deno)
    • スラッシュコマンドなど一部の機能は対応していない

SlackのAppページからアプリを作成し、Bolt Sdkを使って開発

  • メリット
    • 対応機能が一番豊富
    • Bolt SDKが対応している言語が多い(JavaScript、Python、Java)
  • デメリット
    • Slack Appの機能をリリースできる環境(FaaSなど)を準備してデプロイする必要がある

    (これらのリソースを活用したい場合はメリットになるでしょう)

ワークフロービルダーを使ってノーコードで作成

  • メリット
    • ノーコードで作成できるため、簡単なワークフローなら非エンジニアでも作れる
  • デメリット
    • 外部のサービスと連携する場合はWebhookを使う必要があるなど、カスタマイズ性が乏しい

私がAndroidエンジニアということもあり最初はBolt for Javaを使ってKotlinで開発を行おうかと思っていました。
しかし、最近TypeScript×Denoをチーム内で導入した事例があることと、他の業務と並行してなるべく早く開発したかったため、自前でインフラを用意する必要のないnext-generation platformを採用してみました。

SlackアプリからFirebase App Distributionにデプロイする

next-generation platformでのSlackアプリ開発は非常に簡単です。 まずは公式ドキュメントのクイックスタートに沿ってSlack CLIとDenoをインストールして認証を済ませましょう。
0からプロジェクトを作らずとも、Slackが用意しているテンプレートリポジトリを使えばサンプルコードを見ながら開発をすることができます。

next-generation platformのSlackアプリを開発するには3つの要素を理解する必要があります。

Function

Functionはチャンネルの作成やメッセージの送信などのSlackの機能を呼び出したり、サードパーティーのAPIを使うなど自身でカスタマイズすることができます。

例えば特定のGithubリポジトリのブランチの一覧を取得するFunctionは以下のように実装することができます。

import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
import { GithubBranch } from "../model/github_branch.ts";

/**
 * FunctionのInputとOutputを定義する
 * InputではGithub APIにアクセスするためのToken設定を受け取り、
 * Outputではブランチ名の配列を返す
 **/
export const FetchBranchesDefinition = DefineFunction({
  callback_id: "fetch_branches",
  title: "Fetch Branches",
  description: "Fetch branches",
  source_file: "functions/fetch_branches_function.ts",
  input_parameters: {
    properties: {
      githubAccessTokenId: {
        type: Schema.slack.types.oauth2,
        oauth2_provider_key: "github",
      },
    },
    required: ["githubAccessTokenId"],
  },
  output_parameters: {
    properties: {
      branches: {
        type: Schema.types.array,
        items: {
          type: Schema.types.string,
        },
        description: "Branches",
      },
    },
    required: ["branches"],
  },
});

/**
 * Github APIを使ってブランチの一覧を取得し、
 * ブランチ名を抜き出して配列で返す
 */
export default SlackFunction(
  FetchBranchesDefinition,
  async ({ inputs, client }) => {
    const token = await client.apps.auth.external.get({
      external_token_id: inputs.githubAccessTokenId,
    });

    if (!token.ok) throw new Error("Failed to access auth token");

    const headers = {
      Accept: "application/vnd.github+json",
      Authorization: `Bearer ${token.external_token}`,
      "Content-Type": "application/json",
      "X-GitHub-Api-Version": "2022-11-28",
    };

    const endPoint =
      "https://api.github.com/repos/{organization}/{repository}/branches";

    try {
      const branches = await fetch(endPoint, {
        method: "GET",
        headers,
      }).then((res) => {
        if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);
        return res.json() as Promise<Array<GithubBranch>>;
      });

      return {
        outputs: {
          branches: branches.map((branch: GithubBranch) => branch.name),
        },
      };
    } catch (e) {
      console.error(e);
      return {
        error: `Failed to get branches: \`${e.message}\``,
      };
    }
  },
);

Workflow

Workflowはアプリに複数定義することができ、定義したFunctionはWorkflowに紐付けることで処理を実行することができます。

先程のFunctionをWorkflowに紐付けるには以下のようにaddStep()を使用します。

const branches = DeployAppDistributionStartWorkflow.addStep(
  FetchBranchesDefinition,
  {
    githubAccessTokenId: {
      credential_source: "END_USER",
    },
  },
);

また、ワークフロー間で値を受け渡しすることができます。 アプリユーザーが入力しやすいように、取得したブランチ名の一覧を使ってフォームからプルダウンでブランチを選択できるようにしました。

const openDeployAppForm = DeployAppDistributionStartWorkflow.addStep(
  Schema.slack.functions.OpenForm,
  {
    title: "branch名と動作環境を入力してください",
    interactivity: branches.outputs.interactivity,
    submit_label: "Deploy",
    fields: {
      elements: [{
        name: "env",
        title: "動作環境",
        type: Schema.types.string,
        enum: Object.values(AppEnv),
      }, {
        name: "branch",
        title: "branch",
        type: Schema.types.string,
        enum: branches.outputs.branches,
        default: "develop",
      }, {
        name: "releaseNotes",
        title: "リリースノート",
        type: Schema.types.string,
      }],
      required: ["env", "branch"],
    },
  },
);

Trigger

TriggerはWorkflowをどのように起動するかを定義するものです。
Triggerには4つの種類があります。

  • Link Triggers : Slackのパブリックチャンネルに投稿したリンクから起動
  • Scheduled triggers : 特定の時間間隔で起動
  • Event Triggers : Slackで特定のイベントが発生したときに起動
  • Webhook Triggers : 特定のURLがPOSTリクエストを受信したときに起動

今回はアプリユーザーが任意のタイミングでWorkflowを起動したいため、Link Triggersを選択しました。

$ slack trigger create --trigger-def triggers/xxx.ts のようにlocal確認用のトリガーを作成するコマンドを実行すると、URL形式のSlackショートカットリンクが生成されます。
これを任意のSlackチャンネルに投稿したり、チャンネルにブックマークしておくことでワークフローを起動することができます。

工夫したこと

Github APIを使うためのOAuth2認証

Github APIを使うためにはHeaderにTokenを指定する必要があります。
GithubのTokenにはPersonal Access TokenとGithub Appから払い出せるTokenの2種類があります。
個人開発であればPersonal Access Tokenを使っても問題ないですが、業務で利用するアプリであることを踏まえて、今回は専用のGithub Appを作成し、User Access Tokenを払い出すことにしました。

OAuth2認証を行う実装については、公式ドキュメントの他、公式サンプルにGithub AppとOAuth2認証を行うアプリが公開されているので、こちらを参考にしてみてください。

CircleCI APIを安全に使うためにGithub Actionsを経由

デプロイするアプリのビルドとFirebase App DistributionへのアップロードはCircleCI側に定義しているため、Slack AppからCircleCIのAPIを使ってPipelineをトリガーしようと考えていました。

CircleCIのV2 APIにはPersonal Access Tokenしか使えず、Project API Tokenは使えません。
私個人のアカウントでTokenを作成するのは適切ではないと思い、組織アカウントでTokenを作成してもらいました。

通常、Tokenなどの機密データをソースコードにハードコーディングすることは推奨されません。 このようなケースでは、Slackのドキュメントによると、slack env add xxx yyyのように環境変数を登録することができます。

今回組織アカウントで作ってもらったTokenを環境変数として登録するのは組織的に管理が行き届かないため、少しリスクがあると感じました。

そこで、GithubのOrganization SecretsにTokenを保存してもらい、Slack Appは最初にGithub Actions経由でCircleCIのAPIを使うようにしました。
Github Actionsのjob内であれば安全にOrganization Secrets経由でCircleCIのTokenを利用することができます。

最終的な処理フローは以下の図のようになりました。

おわりに

Slack Appをサーバーレスで簡単に開発することができるnext-generation platformを使った、Androidアプリのデプロイアプリについてまとめました。

私はこれまでAndroidアプリの開発を中心に過ごしてきたため、初めてTypeScriptやdenoを触ることになりましたが、公式ドキュメントやサンプルコードがかなり充実していたおかげで数日で実装することができました。
また、AWSやGCPなどのインフラのセットアップ無しでデプロイできることも、素早く開発を完了することができた要因の一つです。

今後はユーザーであるQAメンバーからのフィードバックを受けて、CircleCIのWebページを開く方法と比較してどれくらい業務が楽になったかをヒアリングしながら改良を進めたいと思います。

本記事で紹介した内容を活用して、Slack Appを使用して日常のタスクを自動化してみてはいかがでしょうか。