🤔

[Amplify] Gen1からGen2へのバックエンドリソース移行

に公開

最初に

今回は、Gen1で構築したバックエンドリソースをGen2の管理下として完全移行できるかどうかを試した結果の備忘録となります。

結論、完全に移行することは正直難しいのが現状です。
ではなぜ難しかったのか。どのような対応を行なったのかを書いていきます。

では始めます。

基礎知識

Gen1とGen2では大きく異なる点がいくつか存在しますが、バックエンドリソースを作成する観点で考えると、Gen1では基本的にCLIでamplify add authamplify pushといったようにコマンドラインから対話形式で回答した内容に沿ったリソースが勝手に作成される形式でしたが、Gen2からは、amplify/backend.ts内で、typescriptによるコードベースでのバックエンドリソースを作成及び管理する方針へと転換したのが大きな違いとなります。

https://docs.amplify.aws/react/build-a-backend/

さらに言うと、npx ampx sandboxを実行すると自身の環境(PR単位)で定義したリソースが作成されるので、開発効率も大幅に向上している点も異なります。

なぜ難しかったのか?

結論から言うと、現状は「互換性がないほど設計思想と構造が異なる」ためです。

主な設計思想の違いは以下となります。

比較対象 Gen1 Gen2
設定ファイル CLIベースでの対話式セットアップ(例: amplify add auth) TypeScriptでインフラを記述(CDKライクな構成でCFnが新たに作成される)
自動生成・抽象化 多くの設定が抽象化・自動化されている ある程度明示的に記述する設計
カスタマイズ性 カスタマイズに制限あり(プリセット機能を追加していくが基本のため内部がブラックボックス) 高度なカスタマイズが可能(IAMやリソース名など定義しなくても勝手に作成されるものもあるが基本的には自由にカスタマイズが効く)

上記以外にも異なる部分はありますが、ここで重要なのが、、

「CFnが新たに作成されるため、Gen2でGen1の時に作成したリソース名やIDを定義したとしても、CFnが異なるので再定義扱いとなり、既存のリソースとしては認識できないと言うこと」

です。

そもそも、CFnは一度作られたリソースIDやARNを他のスタックで参照する以外は再利用できないため、そりゃそうだよなといった制御になります。

もちろん、Gen1で定義したリソースを一度削除して、Gen2で同じ内容で定義すれば、名目上再定義ができたと行っても良いケースはありそうですが、storage関連のリソースを定義している場合、、一度削除するなど決して考えられないことなので、無理にGen2で再定義し直すことは避けた方が良いでしょう。

ではどうする

現状の選択肢は2つかと思います。

  • 移行ツールがリリースされるまでおとなしくGen1を使用する
  • authcustom resourceなどで再定義しなおしても問題なさそうだと判断できるリソースのみGen2で定義し直し、再定義できなさそうなリソースは「参照」するで割り切る

移行ツールについて

We are actively developing migration tooling to aid in transitioning your project from Gen 1 to Gen 2. Until then, we recommend you continue working with your Gen 1 Amplify project. We remain committed to supporting both Gen 1 and Gen 2 for the foreseeable future. For new projects, we recommend adopting Gen 2 to take advantage of its enhanced capabilities. Meanwhile, customers on Gen 1 will continue to receive support for high-priority bugs and essential security updates.

上記の通り、現在開発中のようですが、完成の目処が明確ではないのが実情です。
そのため、公式からは「新規で開発するならGen2だけど、Gen1が既存の場合はそのまま開発に専念することを薦める」といった内容にしたがっておいた方が無難かもしれません。

https://docs.amplify.aws/react/start/migrate-to-gen2/

再定義及び参照

前述した通り、検討の上再定義し直しても問題ないリソースに関しては、Gen2の機能を使用して新たにリソースを作成するで良いと思いますが、再定義不可のリソースに関しては参照するが現状できる対応かと思います。

その場合の対処法は以下です。

  • 既存プロジェクトであるGen1はそのまま維持
    • バックエンドリソースの定義を残しておくため
  • Gen2でGen1のリソースを参照する
    • 以下の例ではs3を参照するとしておりますが、基本的に、cdkのfrom~~を使用すればリソースの参照は可能かと思います。
backend.ts
const backend = defineBackend({
  auth,
});

const existBucketStack = backend.createStack("ExistBucketStack");

const bucketName = StringParameter.valueForTypedStringParameterV2(
  existBucketStack,
  "example/bucket",
);

const existBucket = Bucket.fromBucketAttributes(
  existBucketStack,
  "ExistBucket",
  {
    bucketArn: `arn:aws:s3:::${bucketName}`,
    region: process.env.AWS_REGION,
  },
);

// output
backend.addOutput({
  storage: {
    aws_region: existBucket.env.region,
    bucket_name: existBucket.bucketName,
    buckets: [
      {
        aws_region: existBucket.env.region,
        bucket_name: existBucket.bucketName,
        name: existBucket.bucketName,
        paths: {
          "*": {
            guest: ["get", "list"],
            authenticated: ["get", "list", "write", "delete"],
          },
        },
      },
    ],
  },
});

// AmplifyのAuthにs3へのアクセス権限を付与
const unauthPolicy = new Policy(backend.stack, "ExistBucketUnauthPolicy", {
  statements: [
    new PolicyStatement({
      effect: Effect.ALLOW,
      actions: ["s3:GetObject"],
      resources: [`${existBucket.bucketArn}/*`],
    }),
    new PolicyStatement({
      effect: Effect.ALLOW,
      actions: ["s3:ListBucket"],
      resources: [`${existBucket.bucketArn}`, `${existBucket.bucketArn}/*`],
      conditions: {
        StringLike: {
          "s3:prefix": ["*"],
        },
      },
    }),
  ],
});

const authPolicy = new Policy(backend.stack, "ExistBucketAuthPolicy", {
  statements: [
    new PolicyStatement({
      effect: Effect.ALLOW,
      actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      resources: [`${existBucket.bucketArn}/*`],
    }),
    new PolicyStatement({
      effect: Effect.ALLOW,
      actions: ["s3:ListBucket"],
      resources: [`${existBucket.bucketArn}`, `${existBucket.bucketArn}/*`],
      conditions: {
        StringLike: {
          "s3:prefix": ["*"],
        },
      },
    }),
  ],
});

backend.auth.resources.unauthenticatedUserIamRole.attachInlinePolicy(
  unauthPolicy,
);
backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(authPolicy);

// cdk outputs
new cdk.CfnOutput(backend.stack, "AuthUserPoolId", {
  value: backend.auth.resources.userPool.userPoolId,
  exportName: `${projectName}-auth-user-pool-id-${stage}`,
  description: "UserPoolId of Amplify Gen2 build",
});

new cdk.CfnOutput(backend.stack, "ReferenceS3BucketName", {
  value: existBucket.bucketName,
  exportName: `${projectName}-reference-s3-bucket-name-${stage}`,
  description: "Name of S3 bucket built with Amplify Gen1",
});

この例では、バケット名はパラメータストアからバケット名を参照し、そのバケット名から既存のバケットを参照するといった方法をとっています。

こうすることで既存のバケットを参照することができ、暫定対応としては完了となります。

これで出力されるamplify.output.jsonで諸々処理をしてあげればよさそうです。

まとめ

Amplify Gen2は、TypeScriptを用いた柔軟なインフラ定義が可能になる一方、Gen1からの互換性が現状ないため「移行」ではなく**「再構築」**に近い作業になります。

そのため、どうしてもGen2に移行する必要があれば、できないものは割り切って移行ツールのリリースを見守るに徹するがよさそうかなと思いました。

今回の記事が誰かのお役に立てれば幸いです。

NCDCエンジニアブログ

Discussion