Amplify (Gen 1) で Lambda から Amplify の Branch を追加する 

## やりたいこと

Amplify のアプリを組織管理用と各組織用の 2 種類作成して、組織管理用のアプリから組織を追加したときに、各組織用 Amplify のブランチを作成するということをやりたい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599504/7dc37f52-06ca-c78b-5a79-5ff5e1d08ceb.png)

## 前提

- 各組織用 Amplify のソースコードは CodeCommit に push 済み
- 各組織用 Amplify も一度は push 済みで Build できることを確認済み

## Lambda でやること

1. Amplify の BackendEnviroment 作成
    - [CreateBackendEnvironmentCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/amplify/command/CreateBackendEnvironmentCommand/)
        - 定義ができるだけ。実体は CodeCommit の Branch 作成時に作成される
        - Gen 2 では要らなくなるかも
2. Amplify の Branch 作成
    - [CreateBranchCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/amplify/command/CreateBranchCommand/)
        - これも定義ができるだけ。実体は CodeCommit の Branch 作成時に作成
3. CodeCommit の Branch 作成
    - [CreateBranchCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/codecommit/command/CreateBranchCommand/)
        - デプロイが実行されて Amplify 環境ができていく
4. Amplify Job の待ち合わせ
    - [GetJobCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/amplify/command/GetJobCommand/)
        - 定期的に実行して、環境ができたか確認する

作成に時間がかかる場合は Step Functions にすれば良いです。

## コード

環境変数として以下を定義します。

- `PROJECT_REPOSITORY`: CodeCommit に作成した Amplify の Repository 名
- `AMPLIFY_APP_ID`: 作成する Amplify の AppId

```ts
import {
  AmplifyClient,
  ListJobsCommand,
  type ListJobsCommandOutput,
  CreateBranchCommand as CreateAmplifyBranchCommand,
  type CreateBranchCommandOutput as CreateAmplifyBranchCommandOutput,
  CreateBackendEnvironmentCommand,
  type CreateBackendEnvironmentCommandOutput,
  type GetJobCommandOutput,
  GetJobCommand,
} from '@aws-sdk/client-amplify';
import {
  CodeCommitClient,
  GetBranchCommand as GetCodecommitBranchCommand,
  type GetBranchCommandOutput as GetCodecommitBranchCommandOutput,
  CreateBranchCommand as CreateCodecommitBranchCommand,
  type CreateBranchCommandOutput as CreateCodecommitBranchCommandOutput,
} from '@aws-sdk/client-codecommit';
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';

import { type Handler, type Context } from 'aws-lambda';
 B
type Event = {
  typeName: string;
  fieldName: string;
  arguments: {
    srcBranchName: string;  // ベースとなる CodeCommit のBranch 名
    newBranchName: string;  // 新しく作る Branch 名
  };
};

type Output = {
  jobId?: string;      // Amplify 作成 Job の ID
  jobStatus?: string;  // Amplify 作成 Job の Status
};

const amplifyClient = new AmplifyClient();
const codecommitClient = new CodeCommitClient();

/* Amplify BackendEnviroment 作成 */
const CreateBackendEnvironment = async (
  appId: string,
  environmentName: string,
): Promise<CreateBackendEnvironmentCommandOutput> => {
  const command = new CreateBackendEnvironmentCommand({
    appId,
    environmentName,
  });

  return await amplifyClient.send(command);
};

/* Amplify Branch 作成 */
const CreateAmplifyBranch = async (
  appId: string,
  branchName: string,
  backendEnvironmentArn: string,
): Promise<CreateAmplifyBranchCommandOutput> => {
  const command = new CreateAmplifyBranchCommand({
    appId,
    backendEnvironmentArn,
    branchName,
  });

  return await amplifyClient.send(command);
};

/* Amplify Job 一覧取得 */
const ListJobs = async (
  appId: string,
  branchName: string,
): Promise<ListJobsCommandOutput> => {
  const command = new ListJobsCommand({
    appId,
    branchName,
  });

  return await amplifyClient.send(command);
};

/* Amplify Job 情報を取得 */
const getJob = async (
  appId: string,
  branchName: string,
  jobId: string,
): Promise<GetJobCommandOutput> => {
  const command = new GetJobCommand({
    appId,
    branchName,
    jobId,
  });
  
  return await amplifyClient.send(command);
};

/* CodeCommit Branch 取得 */
const GetCodecommitBranch = async (
  repositoryName: string,
  branchName: string,
): Promise<GetCodecommitBranchCommandOutput> => {
  const command = new GetCodecommitBranchCommand({
    repositoryName,
    branchName,
  });

  return await codecommitClient.send(command);
};

/* CodeCommit Branch 作成 */
const CreateCodecommitBranch = async (
  repositoryName: string,
  branchName: string,
  commitId: string,
): Promise<CreateCodecommitBranchCommandOutput> => {
  const command = new CreateCodecommitBranchCommand({
    repositoryName,
    branchName,
    commitId,
  });

  return await codecommitClient.send(command);
};

/* 待ち */
const sleep = async (ms: number) => {
  return await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
};

export const handler: Handler = async (
  event: Event,
  context: Context,
): Promise<Output | undefined> => {
  console.log(`EVENT: ${JSON.stringify(event)}`);

  const repositoryName = process.env.PROJECT_REPOSITORY;
  const appId = process.env.AMPLIFY_APP_ID;
  if (repositoryName == null) {
    throw new Error('PROJECT_REPOSITORY undefined.');
  }
  if (appId == null) {
    throw new Error('AMPLIFY_APP_ID undefined.');
  }

  /* CodeCommit 元 Branch Commit ID 取得 */
  const codecommitBranch = await GetCodecommitBranch(
    repositoryName,
    event.arguments.srcBranchName,
  );
  console.log(codecommitBranch);
  if (codecommitBranch?.branch?.commitId == null) {
    throw new Error(`Not found CodeCommit branch ${event.arguments.srcBranchName}.`);
  }

  /* Amplify Backend Environment 作成 */
  const createBackendEnvironmentResponse = await CreateBackendEnvironment(
    appId,
    event.arguments.newBranchName,
  );
  console.log(createBackendEnvironmentResponse);

  const backendEnvironmentArn =
    createBackendEnvironmentResponse?.backendEnvironment?.backendEnvironmentArn;

  if (backendEnvironmentArn == null) {
    throw new Error(`Faild to create Amplify BackendEnvironment ${event.arguments.newBranchName}.`);
  }

  /* Amplify Branch 作成 */
  const createAmplifyBranchResponse = await CreateAmplifyBranch(
    appId,
    event.arguments.newBranchName,
    backendEnvironmentArn,
  );
  console.log(createAmplifyBranchResponse);

  /* CodeCommit Branch 作成 */
  const createCodecommitBranchResponse = await CreateCodecommitBranch(
    repositoryName,
    event.arguments.newBranchName,
    codecommitBranch.branch.commitId,
  );
  console.log(createCodecommitBranchResponse);

  /* Amplify 構築 Job ができるのを待ち合わせる */
  let jobId;
  while (jobId == null) {
    await sleep(1_000);
    const listJobsResponse = await ListJobs(appId, event.arguments.newBranchName);
    console.log(listJobsResponse);

    jobId = listJobsResponse.jobSummaries?.[0].jobId;
  }

  /* Amplify 構築終了を待ち合わせる */
  let jobStatus;
  while (jobStatus == null || ['PENDING', 'PROVISIONING', 'RUNNING'].includes(jobStatus)) {
  
    /* Lambda の残り時間が 10 秒切ったら諦める */
    if (context.getRemainingTimeInMillis() < 10_000) {
      console.log('Remaining time: ', context.getRemainingTimeInMillis());

      break;
    }

    await sleep(1_000);
    const getJobResponse = await getJob(
      appId,
      event.arguments.newBranchName,
      jobId,
    );
    console.log(getJobResponse);
    
    jobStatus = getJobResponse?.job?.summary?.status;
  }

  // 作成失敗
  if (jobStatus !== 'SUCCEED') {
    throw new Error(`Faild to create Amplify Branch ${event.arguments.newBranchName}.`);
  }

  return {
    jobId,
    jobStatus,
  };
};
```

## Amplify Branch の削除

削除の時は、[DeleteBranchCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/amplify/command/DeleteBranchCommand/) と [DeleteBackendCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/amplifybackend/command/DeleteBackendCommand/) を使えばできるはず。