CDKで管理しているリソースを別スタックに移行する
システムを運用する中で、リファクタリング等で CDK で管理しているリソースを別のスタックへ移動したくなるケースがありました。
ほとんどのリソースは再作成すれば済む話なのですが、今回 IAM ロールが外部の Principal に指定されており、再作成を許容できないケースです。
IAM ロールを Principal に設定している状態で削除すると、以下のように自動で文字列が変更されてしまい動作しなくなります。これを発生させずに移行するのが目的です。
こういったケースの時の備忘録として、CDK で管理しているリソースを再作成せずに別スタックへ移動するまでの手順を残しておきます。
移行のイメージは以下です。
移行前

移行後

やってみる
前準備
元のスタックとして Lambda と別スタックに移行したい IAM ロールがデプロイされているとします。
CDK 側の移行前(SourceStack)は以下のような定義です。
export class SourceStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    //IAM Role
    const lambdaRole = new iam.Role(this, 'Role', {
      roleName: 'LambdaRole',
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    });
    //Lambda Function
    const lambdaFunction = new lambda.Function(this, 'Lambda', {
      runtime: lambda.Runtime.PYTHON_3_11,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('lambda'),
      role: lambdaRole,
    });
  }
}
このLambdaRoleを他の CDK からデプロイされるスタックに移行していきます。
RETAINをつけて元のスタック管理から外す
まずは元のスタックからLambdaRoleの管理を外す必要があります。
先ほどのコードに DeletionPolicy の RETAIN を追加してスタックが削除されてもリソースが残るように更新してください。
    //IAM Role
    const lambdaRole = new iam.Role(this, 'Role', {
      roleName: 'LambdaRole',
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    });
    lambdaRole.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
移行先のスタックでIAMロールを参照する
移行先のスタックでは移行前のスタックで定義されていた Lambda を再作成して定義しています。
ここで本来であれば移行前の Lambda 用のロールも同じように再作成したいのですが、移行前と全く同じ状態で利用したいため再作成できません。
export class DestinationStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    // //IAM Role
    // const lambdaRole = new iam.Role(this, 'Role', {
    //   roleName: 'LambdaRole',
    //   assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    // });
    //Lambda Function
    const lambdaFunction = new lambda.Function(this, 'Lambda', {
      runtime: lambda.Runtime.PYTHON_3_11,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('lambda'),
      role: lambdaRole,
    });
  }
}
そのため、一度移行前のスタックで定義した IAM ロールを fromRoleName を使って直接 Construct 化します。
export class DestinationStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    // //IAM Role
    // const lambdaRole = new iam.Role(this, 'Role', {
    //   roleName: 'LambdaRole',
    //   assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    // });
    const lambdaRole = iam.Role.fromRoleName(this, "LambdaRole", "LambdaRole");
    //Lambda Function
    const lambdaFunction = new lambda.Function(this, 'Lambda', {
      runtime: lambda.Runtime.PYTHON_3_11,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('lambda'),
      role: lambdaRole,
    });
  }
}
こうすることで、一度既存リソースである IAM ロールを設定した Lambda を再作成できます。
移行前スタックの削除
IAM ロールは CDK 管理外のままですが、他のリソースは再作成できたため、移行前のスタックを削除します。

IAM ロールは RETAIN になっているため、スタックを削除してもそのまま残ります。
現状は以下のような形になっています。

移行先のスタックでIAMロールをインポートする
このままでも良いのですが、LambdaRoleを修正したい場合はコンソールから行う必要があるのはイマイチですよね。できれば CDK で管理したいので、移行先のスタックにインポートして管理できるようにします。
fromRoleNameをコメントアウトして、移行前から利用していた IAM ロールのコードを記述してみます。
export class DestinationStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    //IAM Role
    const lambdaRole = new iam.Role(this, 'Role', {
      roleName: 'LambdaRole',
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    });
    // const lambdaRole = iam.Role.fromRoleName(this, "LambdaRole", "LambdaRole");
    //Lambda Function
    const lambdaFunction = new lambda.Function(this, 'Lambda', {
      runtime: lambda.Runtime.PYTHON_3_11,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('lambda'),
      role: lambdaRole,
    });
  }
}
この状態でnpx cdk synthを実行します。すると cdk.out で出力されている CloudFormation テンプレート(DestinationStack.template.json)を取得できます。
このテンプレートを使ってコンソールからスタックのインポートを実行してみます。

インポートするリソースの識別子のプロパティは IAM ロール名を入力します。

最後まで進めると、インポート前の変更セット作成時に既存リソースへの変更が検出されたため、エラーとなってしまいました。

これはfromRoleNameで参照している IAM ロールとインポート用に書いたロールの定義が別のリソースとして扱われているため、Lambda に対し更新がかかることを意味しています。
このままではインポートできないため、一手間手動で変更を加えていきます。
差分を検出しているのは cdk.out で出力されている CloudFormation テンプレート(DestinationStack.template.json)です。そのため、このテンプレートを修正し、Lambda に設定される IAM ロールがインポート予定の IAM ロールと書き直すことで、変更なしと判定させてみます。
fromRoleNameで参照している状態のテンプレートを出力して控えておきます。
    const lambdaRole = iam.Role.fromRoleName(this, "LambdaRole", "LambdaRole");
次にインポート時の記述に変更してテンプレートを出力します。
    //IAM Role
    const lambdaRole = new iam.Role(this, 'Role', {
      roleName: 'LambdaRole',
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    });
この2つのテンプレートの diff をとると、IAM ロールの参照部分に差分が出ていることがわかります。

差分が出ないようにするためには、インポート時の記述の時 IAM ロールの参照方法をfromRoleName時のものにする必要があります。具体的にはFn::GetAttの部分をFn::Joinの参照に置き換えます。
DependsOnの差分も合わせて削除して、IAM ロールのリソース部分以外に差分が出てない状態にします。もし CDKMetadata に差分があればそれも修正してください。

ここまでできたら修正後のテンプレートを使って、再度コンソールからインポートを実施してみます。
うまくいけば、インポートされるリソースが表示されます。問題なければインポートしましょう。

CDKで再デプロイ
手動で変更した IAM ロールの参照を元に戻す必要があるため、CDK から再デプロイしましょう。
念の為デプロイ前にcdk diffで確認をとっておくと、先ほど手動で変更した部分の差分が表示されるはずです。
 ❯ npx cdk diff
Stack DestinationStack
Resources
[~] AWS::Lambda::Function Lambda LambdaD247545B 
 ├─ [~] Role
 │   ├─ [+] Added: .Fn::GetAtt
 │   └─ [-] Removed: .Fn::Join
 └─ [+] DependsOn
     └─ ["Role1ABCC5F0"]
✨  Number of stacks with differences: 1
この変更自体では IAM ロールの再作成がされないため、デプロイしてみましょう。成功すれば CDK への取り込みは完了です。
試しに IAM ロールへポリシーを追加してみます。
    const lambdaRole = new iam.Role(this, 'Role', {
      roleName: 'LambdaRole',
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
+      managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')]
    });
    lambdaRole.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
デプロイしてみると、IAM ロールにポリシーが追加されました。

これでようやく移行完了です、お疲れ様でした。
cdk importを使わなかった理由
実は CDK の CLI にもリソースをインポートするためのコマンドが用意されています。
コンソールを使わずに済むのであればいいなと一度試してみましたが、cdk import 実行時には差分取得の際に cdk.out のテンプレートが自動更新されてしまいます。手動で変更したものが上書きされてしまい、IAM ロールのみのインポートができずエラーとなったため断念しました。
もしオプション等で既存テンプレートを更新しない方法があれば教えてください。
まとめ
CDK で管理しているリソースの移行を試してみました。ポイントとしては以下のあたりを気をつける必要があります。
- リソースの置き換わるタイミングを理解する
- スタックへのインポートは愚直に手動でテンプレートを修正する
- 都度 diff をとる
単純なリソースのインポートであればそこまで難しくないのですが、再作成が許容できない場合やリソースの参照などが絡むと非常に難しくなります。行う際はしっかりと手順を用意しておきましょう。
できれば CDK ではスタックは分けずに Construct 化して管理していきたいですね。











