Backlog Gitリポジトリの更新をトリガーに、Amazon S3にコンテンツを自動でアップロードする

# はじめに

## この記事の概要

この記事では、タスク管理ツールBacklogのGitリポジトリとAmazon S3（以下、S3）との連携方法についてまとめています。
流れとしては、Backlog GitリポジトリのWebフックを使用して、リポジトリの更新をトリガーにLambda関数が実行されるようにします。そして、Lambda関数がS３にファイルアップロード処理を行います。
この一連の流れの具体的な構築方法や注意点をまとめています。

## BacklogのGit機能について

[Backlog](https://backlog.com/)はタスク管理ツールです。そして、その機能の一つにGit機能があります。
ソースコード管理といえば、IT業界全体では、おそらくGitHubを使用しているチーム（人）が多いと思いますが、タスク管理にBacklogを使用しているケースでは、タスクとソースコードの紐付けが行いやすかったり、タスク管理とソースコード管理（他にもWiki機能もあるのでドキュメント管理なども）を一元化できるなどのメリットがあります。

このGit機能は基本的には特に不自由なく使用できるのですが、強いてデメリットを挙げるとすると、CI/CDツールとの連携が行いにくいという点が挙げられると思います。
例えば、AWSのCodeBuildのソースプロバイダーにBacklogのGitリポジトリは指定できませんし、GitHub Actionsのような機能もありません（これらが可能であれば、この記事は不要です！）。

このように、BacklogのGit機能はCI/CDの構築という点でみると少し不便です。
他のサービスを組み合わせてある程度自分で構築する必要があります。

（もちろん、このような点はありますが、Backlogはとても素晴らしいサービスで、私の大好きなサービスの一つです！）

# 本題

今回は、CI/CDとは少し異なりますが、BacklogのGitリポジトリで管理する静的コンテンツをS3に自動でアップロードする仕組みの構築についてみていきます。
私の実際の課題は、S3+CloudFrontで配信している静的コンテンツをBacklogのGitリポジトリで管理しており、そのコンテンツの更新時に、自動的にS3にアップロードしたいというものでした。
この対応を行うまでは、コンテンツ更新ごとに、手作業でアップロードを行なっていました。

## 全体像

全体像は以下のようになります。

冒頭で記載した通り、Backlog GitリポジトリのWebフックを使用して、リポジトリの更新をトリガーにLambda関数が実行されるようにし、そして、Lambda関数がS３にファイルアップロード処理を行います。

また、私のケースでは、S3+CloudFrontで配信しているコンテンツが対象でしたので、最後にCloudFrontのキャッシュ削除の手順がありますが、単にS3へのアップロードのみを行う場合は、この手順は不要です。

なお、画像内の連番は構築した仕組みにおける処理の実行順序を示しています。
以降で説明する構築手順の説明順序とは異なる点にご注意ください。

![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3687813/4f0be7d9-4517-06fc-2716-6b836e7af8c3.png)

## 1. Backlog　Gitリポジトリ、アップロード先のS3バケットを作成 （存在しない場合）

Backlog GitリポジトリとS3バケットを作成してください。

## 2. Secrets ManagerにBacklog認証情報を登録

今回は、BacklogのGitリポジトリをhttpsでクローンします。
そのための認証情報（Backlogユーザ名とパスワード）をSecrets Managerに登録します。

シークレットキーは自由に設定してください。

![スクリーンショット_2024-07-26_21_40_50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3687813/8ed99895-f937-9dc4-d70e-b414c23784fb.png)

## 3. Lambda関数を作成する

Lambda関数を作成します。全体像にある通り、今回Lambda関数を2つ作成しています（ランタイムはNode.js20）。
一つは、BacklogのWebフックURL先となる関数（以下、関数A）、もう一つはその関数から呼び出されてコンテンツのアップロード処理等を行う関数（以下、関数B）です。

このように2つの関数に分けているのは、WebフックによるLambda関数の呼び出し形態とLambda関数の処理時間が関係しています。
BacklogのWebフックによるLambda関数実行は同期的な実行となるため、呼び出されたLambda関数の処理が全て完了しレスポンスを返すまで、Backlog側は待機することになります。
そのため、Lambda関数の処理時間が長く、接続がタイムアウトになれば、Backlog側はリクエストを再送信します。これはLambda関数の処理が複数回実行されることを意味します。

実際に、関数Bの処理時間は数十秒〜一分程度掛かります。そのため、Backlogからのリクエストは関数Aで処理するようにします。関数Aは非同期で関数Bを呼び出した後、すぐにレスポンスを返します。これにより、タイムアウトによるリクエストの再送信を防ぎます。

実際のソースコードは以下のようになります。

関数Aは、関数Bを非同期で呼び出すだけです。
この時、Backlogから受け取ったリクエストデータ（`event.body`）をペイロードに設定します。
Backlogから送信されるリクエストデータについては、[公式のユーザガイド](https://backlog.com/ja/enterprise-help/userguide/userguide1581/)で説明されています。


```javascript: 関数A - index.js
const { InvokeCommand, LambdaClient } = require('@aws-sdk/client-lambda');

// 関数Bの名前
const LAMBDA_FUNCTION_NAME = process.env.INVOKED_FUNCTION_NAME;

async function handler(event, context) {
  try {
    console.log('event: ', JSON.stringify(event));

    // 関数Bを非同期で呼び出す
    const client = new LambdaClient({region: 'ap-northeast-1'});
    const command = new InvokeCommand({
      FunctionName: LAMBDA_FUNCTION_NAME,
      InvocationType: 'Event',
      Payload: JSON.stringify({ body: event.body }),
    });
    const res = await client.send(command);

    if (res.$metadata.httpStatusCode === 202) {
      console.log('success');
      return { statusCode: 200, body: 'success' };
    }
  } catch (error) {
    console.error(error);
  }

  return { statusCode: 500, body: 'error' };
}

module.exports = { handler };
```

関数Bは、ここでは主要な部分を抜粋するかたちにしていますが、関数Aからデータを受け取り、`main` ブランチの場合にのみ、各種処理（インポートしている `modules/` にそれぞれ実際に処理を行うクラスを作成してます。今回は全てのソースコードを提示することはしませんが、それぞれSDKのドキュメント等を調べれば簡単に実装できると思います）を実行します。

前の手順で登録したSecrets Managerのシークレット情報を取得し、それを使用してGitリポジトリをクローンします。
Gitリポジトリをクローンした後は、アップロード対象のコンテンツをAWS CLIコマンドでアップロードします（今回は、`s3 sync` を使用します。`s3 sync` はSDKでは対応されていませんので、CLIコマンドを使用しています）。

繰り返しになりますが、単にS3へのアップロードのみを行う場合は、最後のCloudFrontのキャッシュ削除処理は不要です。

```javascript: 関数B - index.js
const Helper = require('./modules/helper');
const CommandExecutor = require('./modules/commandExecutor');
const SecretsManager = require('./modules/awsSecretsManager');
const CloudFront = require('./modules/awsCloudFront');

const S3_BUCKET = process.env.S3_BUCKET;
const CF_ID = process.env.CF_ID;
const REPO_NAME = 'git-sample-repo';
const REPO_URL = `https://[user]:[password]@sample.backlog.jp/git/SAMPLE/${REPO_NAME}.git`;
const WORK_DIR = '/tmp/';
const TARGET_BRANCH = 'main';

async function handler(event, context) {
  try {
    const bodyData = Helper.bodyParser(event.body);

    // リクエストボディから必要な情報を取得
    const repoName = bodyData.repository.name;
    const branch = bodyData.ref.replace('refs/heads/', '');
    const deploy = repoName === REPO_NAME && branch === TARGET_BRANCH;

    if (deploy) {
      console.log('Deploying...');
      await CommandExecutor.runCommand(`rm -rf ${WORK_DIR + REPO_NAME}`);

      // Secrets ManagerからGitリポジトリの認証情報を取得
      const { GIT_USER: user, GIT_PASSWORD: password} = await new SecretsManager('backlog_git_secrets').getSecretValue();
      console.log('Get secrets successfully!');

      // Gitリポジトリをクローン
      // 認証情報を用いて、URLを作成します
      // Lambda関数内のプログラムは、/tmp以下にのみ書き込みが許可されています
      // そのため、/tmp以下にクローンします
      const repoUrl = REPO_URL.replace('[user]', user).replace('[password]', password);
      await CommandExecutor.runCommand(`cd ${WORK_DIR} && git clone -b ${TARGET_BRANCH} --depth 1 ${repoUrl}`);
      console.log('Cloned successfully!');

      // S3バケットにファイルを同期
      await CommandExecutor.runCommand(`aws s3 sync --delete ${WORK_DIR + REPO_NAME}/contents s3://${S3_BUCKET}`);
      console.log('Synced successfully!');

      // CloudFrontキャッシュ削除
      const cf = new CloudFront(CF_ID);
      await cf.createInvalidation(['/*']);
      console.log('Invalidated successfully!');
    }
  } catch (error) {
    console.error(error);
  }
}

module.exports = { handler };
```

なお、それぞれの関数のロールには、処理実行のために適切なポリシーを設定する必要があります。
Secretes Managerからのデータ取得やS3へのアップロード、Lambda関数の呼び出しなど実行する処理に合わせてポリシーを追加してください。

また、関数Bについては、依存パッケージ（`git`, `aws cli`）を、Lambda Layerとして登録しています。
`git` については、[こちら](https://github.com/lambci/git-lambda-layer)を使用しました。
Lambda関数で `aws cli` を使用する方法については、[こちら](https://qiita.com/DaichiAndoh/items/c738a0f12c51e30f9b93)にまとめました。
そして、S3へのアップロード対象コンテンツ名に日本語が含まれている場合は、エラーが出るかもしれません。これについても対応方法を[こちら](https://qiita.com/DaichiAndoh/items/00928fd07075c635852c)にまとめています。

## 4. Lambda関数のURLを有効にし、GitリポジトリのWebフックURLを設定する

関数Aの関数URLを設定します。
今回は、Backlogからのアクセス情報（IP等）が特定できないため、パブリックアクセスを許可しています。
そのため、同時実行数を制限したり、Lambda関数のプログラム内に通知処理（Slack通知、メール通知など）を入れるなどして、実行を制限・検知できるようにすると良いでしょう。

![スクリーンショット_2024-07-26_21_58_29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3687813/3b5a4a8d-6024-125c-f82d-8ab4b523722c.png)

関数URLが設定できれば、そのURLにアクセスすると関数Aを実行できます。
つまり、このURLをBacklog GitリポジトリのWebフックに追加すれば、リポジトリの更新毎に関数Aが実行されます。

WebフックはGitリポジトリの設定画面から設定することができます。

![スクリーンショット_2024-07-26_22_07_50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3687813/8758648a-5cd1-5f05-fa70-74565b9856bd.png)

## まとめ

これで完了です。改めて流れを確認します。

1. Backlog Gitリポジトリが更新されるとWebフックURLへリクエストが送信される
2. リクエスト先となる関数Aが実行される
3. 関数Aは非同期で関数Bを呼び出し、レスポンスを返す
4. 関数Bはリポジトリ更新情報から `main` ブランチの更新の場合にS3へのアップロード処理を行う

![aarchitecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3687813/9fba8608-0281-3d4c-1281-d6ca8b584947.png)

# 最後に

今回は、S3へのアップロードでしたが、Lambda関数でBacklog Gitリポジトリを扱えることができれば、Lambda関数とその他AWSサービスとの連携は比較的容易に行えますので、例えばCodeシリーズと連携させればCI/CDの構築もできそうです（実際にそのような仕組みの構築を解説している記事もいくつか目にしました）。

最後まで読んでいただきありがとうございます。
読んでいただいた方にとって何らかのプラスになっていれば嬉しいです。

# 参考ページ

- [Backlog Git Webフックについて](https://backlog.com/ja/enterprise-help/userguide/userguide1581/)