🚅

[AWS] Terraform の import と cdk import を比べてみる

2023/11/24に公開

ログラスのクラウドエンジニアの原と申します。
ログラスでは、AWS のリソースのほぼ 100% を Terraform で IaC (Infrastructure as Code)化しました。新しいリソースも Terraform で記述した上でデプロイをしていますが、開発段階ではアプリ開発者や自分が AWS マネジメントコンソールをポチポチやりながらリソースを構築する場合があり、そのようなリソースを IaC 化をするときには、既存リソースを IaC の管理下に置くインポート機能が便利です。

Terraform では、最近のバージョンアップ(v1.5〜)で import ブロックが使えるようになり、インポートがさらに手軽にできそうなのでぜひ試してみたいと思っていました。また、AWS CDK の使用経験はあるのですが、そのときに cdk import は使うのが面倒だったり怖いなぁと思ったことがあり、その点の最新事情について検証してみたくなりました。

当記事は、Terraform や AWS CDK のインポート機能を使ってみた当方の記録です。

要旨

  • Terraform の import は便利。特に import ブロックはインポート操作の敷居をさらに下げてくれた。既存のリソースの操作するリスクなく安心してインポートができる。
  • cdk import では、既存リソースと(CDK コードから生成される) CloudFormation のテンプレートとの差分をドリフトとして正しく検出するためには、CDK のコード(リソースのコンストラクタ)に設定可能な属性を網羅しておく必要がありそう。また、CDK コードに指定するリソースの属性に誤りがあったりすると、インポート操作やドリフト解消の際にリソースの変更をしてしまうことがあるので注意(私の経験上)。

検証環境

  • Terraform: v1.6.4
  • Terraform AWS provider: 5.26.0
  • AWS CDK: 2.110.0 (build c6471f2)

なお、本記事は当方の環境での検証結果に基づいており、どの環境でも同じ結果が得られることを保証するものではないことをおことわりしておきます。

題材

ここでは、SQS のキューを AWS CLI から作成して、これを「既存のリソース」とし、IaC ツールである Terraform や CDK の管理下に取り込む(インポートする)ことを考えます。

AWS CLI の aws sqs create-queue コマンドを使って、次のコマンドで SQS のキューを作成します。

$ aws sqs create-queue --queue-name import-test \
    --attribute VisibilityTimeout=10

これは次のような設定で SQS キューを作成したことになります。

  • キューの名前: import-test
  • 可視性タイムアウト: 10 秒(VisibilityTimeout を指定しない場合のデフォルトは30秒)

実際のキューの属性値を aws sqs get-queue-attributes コマンドで確認しておきます。

$ aws sqs get-queue-attributes --queue-url https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test --attribute-names All
{
    "Attributes": {
        "QueueArn": "arn:aws:sqs:ap-northeast-1:XXXXXXXXXXXX:import-test",
        "ApproximateNumberOfMessages": "0",
        "ApproximateNumberOfMessagesNotVisible": "0",
        "ApproximateNumberOfMessagesDelayed": "0",
        "CreatedTimestamp": "1700669456",
        "LastModifiedTimestamp": "1700669456",
        "VisibilityTimeout": "10",
        "MaximumMessageSize": "262144",
        "MessageRetentionPeriod": "345600",
        "DelaySeconds": "0",
        "ReceiveMessageWaitTimeSeconds": "0",
        "SqsManagedSseEnabled": "true"
    }
}

(AWS のアカウント ID にはマスクをしています)
VisibilityTimeout は確かに 10 秒に設定されています。
また、以下の属性値が次のように設定されていることがわかります。

  • "MaximumMessageSize": "262144"
  • "MessageRetentionPeriod": "345600",
  • "DelaySeconds": "0",
  • "ReceiveMessageWaitTimeSeconds": "0",

Terraform の import

インポートの前に - リソースを記述する Terraform のコード

インポートをする前に上の題材のリソースを記述する Terraform のコードを書いてみます。

デフォルトとは異なる可視性タイムアウト visibility_timeout_secondsを記述しています。

resource "aws_sqs_queue" "import_test" {
  name                       = "import-test"
  visibility_timeout_seconds = 10
}

従来の方法: terraform import コマンド

従来は、terraform import コマンドを用いて、以下のようにやる必要がありました(この方法は現在でも使えます)。

  • Terraform のコードに、対応するリソースのブロックを記述し、リソースのインポートする「箱」を作っておきます。
    • このとき、リソースタイプをリソース名が設定されていれば十分で、リソースのパラメータは空のままで OK です。
resource "aws_sqs_queue" "import_test" {

}
  • 上の Terraform のコードがあるディレクトリで次のように terraform import を実行します。
$ terraform import aws_sqs_queue.import_test \
https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test

このコマンドは、aws_sqs_queue.import_test という Terraform のリソースに物理リソース ID https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test のリソースを取り込むことを指示しています。
上の SQS の場合には、リソース ID に Queue の URL を指定していますが、何を指定するかはリソースごとに異なり、Terraform の各リソースのマニュアルの一番下に書いてあります。SQS の場合はこちら

  • 上のコマンドを実行すると、次のように表示されます。
$ terraform import aws_sqs_queue.import_test https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test
aws_sqs_queue.import_test: Importing from ID "https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test"...
aws_sqs_queue.import_test: Import prepared!
  Prepared aws_sqs_queue for import
aws_sqs_queue.import_test: Refreshing state... [id=https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

このリソースのインポートが成功したことを示しています。

実際に、terraform state show コマンドで Terraform にインポートされていることが確認できます。

$ terraform state show aws_sqs_queue.import_test
# aws_sqs_queue.import_test:
resource "aws_sqs_queue" "import_test" {
    arn                               = "arn:aws:sqs:ap-northeast-1:XXXXXXXXXXXX:import-test"
    content_based_deduplication       = false
    delay_seconds                     = 0
    fifo_queue                        = false
    id                                = "https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test"
    kms_data_key_reuse_period_seconds = 300
    max_message_size                  = 262144
    message_retention_seconds         = 345600
    name                              = "import-test"
    receive_wait_time_seconds         = 0
    sqs_managed_sse_enabled           = true
    tags                              = {}
    tags_all                          = {}
    url                               = "https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test"
    visibility_timeout_seconds        = 10
}
  • これを Terraform のコードに転記して、リソースのパラメータではない一部パラメータを削除します。
    • terraform plan を実行すると、「設定できないパラメータ」としてエラーが出てくるので、対応するパラメータを削除すれば OK です。
    • このケースでは、terraform state show で表示されたコードをそのまま転記して terraform plan を実行すると、arn, id, url が「設定できないパラメータ」としてエラーになりましたので、これらを削除しました。
  • 最終的なコードはこのようになりました。
resource "aws_sqs_queue" "import_test" {
    content_based_deduplication       = false
    delay_seconds                     = 0
    fifo_queue                        = false
    kms_data_key_reuse_period_seconds = 300
    max_message_size                  = 262144
    message_retention_seconds         = 345600
    name                              = "import-test"
    receive_wait_time_seconds         = 0
    sqs_managed_sse_enabled           = true
    tags                              = {}
    tags_all                          = {}
    visibility_timeout_seconds        = 10
}
  • 冒頭で書いたリソースのコードと比べると、冒頭のコードには明示せずにデフォルトが用いられたパラメータのキーと値も出力されています。そして、その値は、aws sqs get-queue-attributes で確認した値と同じ値になっています。
    • AWS CLI の aws sqs create-queue コマンドにはよらず、冒頭のコードを terraform apply してデプロイし、そのリソースを import しようとしたときに得られるコードが上のものと一致することは検証済みです。
  • このコードに対して terraform plan を実行すると、次のように No changes が得られ、Terraform の管理下に実際のリソースと差分がないように置かれました。
$ terraform plan
aws_sqs_queue.import_test: Refreshing state... [id=https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
  • この一連の操作では、AWS 上のリソースに変更を加える可能性がある操作を一切していません。既存のリソースを変更することなく、安全に IaC の管理下に置くことができます。

Terraform 1.5 から使える import ブロックによる方法

Terraform 1.5 からコード内に import ブロックを記述し、そのコードを terraform apply
することで既存リソースをインポートすることができるようになりました[1]

  • コードに次のような import ブロックを記述します。
import {
  to = aws_sqs_queue.import_test
  id = "https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test"
}

この to, id は、それぞれ、上の terraform import コマンドの第1, 第2引数に対応しています。また、terraform import のときには「箱」(resource ブロック)をあらかじめ作っておく必要がありましたが、import ブロックを使う場合にはその必要はありません。

  • 次に terraform plan を実行しますが、そのときに --generate-config-out というオプションでインポートしたリソースのコードの出力先を指定します。
$ terraform plan -generate-config-out=out.tf
aws_sqs_queue.import_test: Preparing import... [id=https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test]
aws_sqs_queue.import_test: Refreshing state... [id=https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test]

Terraform will perform the following actions:

  # aws_sqs_queue.import_test will be imported
  # (config will be generated)
    resource "aws_sqs_queue" "import_test" {
        arn                               = "arn:aws:sqs:ap-northeast-1:XXXXXXXXXXXX:import-test"
        content_based_deduplication       = false
        delay_seconds                     = 0
        fifo_queue                        = false
        id                                = "https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test"
        kms_data_key_reuse_period_seconds = 300
        max_message_size                  = 262144
        message_retention_seconds         = 345600
        name                              = "import-test"
        receive_wait_time_seconds         = 0
        sqs_managed_sse_enabled           = true
        tags                              = {}
        tags_all                          = {}
        url                               = "https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test"
        visibility_timeout_seconds        = 10
    }

Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
╷
│ Warning: Config generation is experimental
│
│ Generating configuration during import is currently experimental, and the generated configuration format may change in future
│ versions.
╵

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Terraform has generated configuration and written it to out.tf. Please review the configuration and edit it as necessary before adding
it to version control.

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run
"terraform apply" now.

  • そうすると、--generate-config-out オプションに指定した out.tf に Terraform のコードとしてそのまま使えるコードが出力されます。terraform import コマンドを使ったときには手動で取り除いた arn, id, url が自動で取り除かれています。
# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform from "https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test"
resource "aws_sqs_queue" "import_test" {
  content_based_deduplication       = false
  deduplication_scope               = null
  delay_seconds                     = 0
  fifo_queue                        = false
  fifo_throughput_limit             = null
  kms_data_key_reuse_period_seconds = 300
  kms_master_key_id                 = null
  max_message_size                  = 262144
  message_retention_seconds         = 345600
  name                              = "import-test"
  name_prefix                       = null
  policy                            = null
  receive_wait_time_seconds         = 0
  redrive_allow_policy              = null
  redrive_policy                    = null
  sqs_managed_sse_enabled           = true
  tags                              = {}
  tags_all                          = {}
  visibility_timeout_seconds        = 10
}
  • そして、terraform apply を実行します。この apply は「import を実行する」という意味の apply で、実際にAWS 上のリソースを操作することはありません。それは、terraform apply で表示される実行計画でも確認できます。
$ terraform apply
aws_sqs_queue.import_test: Preparing import... [id=https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test]
aws_sqs_queue.import_test: Refreshing state... [id=https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test]

Terraform will perform the following actions:

  # aws_sqs_queue.import_test will be imported
    resource "aws_sqs_queue" "import_test" {
        arn                               = "arn:aws:sqs:ap-northeast-1:XXXXXXXXXXXX:import-test"
        content_based_deduplication       = false
        delay_seconds                     = 0
        fifo_queue                        = false
        id                                = "https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test"
        kms_data_key_reuse_period_seconds = 300
        max_message_size                  = 262144
        message_retention_seconds         = 345600
        name                              = "import-test"
        receive_wait_time_seconds         = 0
        sqs_managed_sse_enabled           = true
        tags                              = {}
        tags_all                          = {}
        url                               = "https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test"
        visibility_timeout_seconds        = 10
    }

Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: 

いつもの terraform apply による出力に見えますが、下のほうにある

Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.

1 to import となっているのが通常の apply と異なるところで、import を実行することを示しています。そして、0 to add, 0 to change, 0 to destroy となっていることから、AWS のリソースの操作はしないことが分かります。
import ブロックを使うこの方法でも、terraform import コマンドを使う場合と同様、AWS のリソースを操作することなく、IaC でのインポートができます。

なお、コードに記述した import ブロックはそのままにしてimport をしたという記録にしてもよいですし、削除しても動作には影響はあたえません。

Terraform で import を行う場合には、AWS リソースの操作をすることがないということが私にとっては大きな安心材料になっています。また、リソースのパラメータは自動的に列挙してくれるので、自分が認識していなかった設定も正しくインポートされますし、何らかの原因で実際のリソースとコードの間に差分が生じたときも、漏れなく検知をしてくれます(毎日、CI で Terraform のコードと実際のリソースとのドリフトチェックをしています)。

cdk import

同様の操作を cdk import でやってみます。
以下の検証において、AWS CDK はこの記事執筆時点の最新バージョン(2.110.0)を用いています。

AWS CDK は、TypeScript, Python などで AWS のリソースを記述することができるもので、これらの言語で書かれたコードを CloudFormation のテンプレートに変換し、そのテンプレートを使って CloudFormation のスタックを作り、そのスタックで AWS のリソースを構築しています。
cdk import も、CloudFormation の import 機能[2]を活用したものになります。

CloudFormation または AWS CDK による既存のリソースのインポートの手順

AWS の CloudFormation の import 機能のドキュメントにあるように、既存のリソースを
CloudFormation のスタックに取り込むには、次のような手順を踏みます。

  • 既存のリソースをデプロイできる CloudFormation のテンプレート、またはそれを生成する CDK のコードを作成する。
  • インポートの操作を実施する。
    • CloudFormation の場合は AWS マネジメントコンソールや AWS CLI から実行
    • AWS CDK の場合には cdk import を実行
  • スタックのドリフトを検出する。
    • このドリフトが、CloudFormation のテンプレート(CDK のコードから変換されたものを含む)と実際のリソースとの差分に対応する。
      • 以下では「CloudFormation のテンプレート(CDK のコードから変換されたものを含む)」を単に「テンプレート」と表記します。
  • ドリフトがなくなるようにテンプレートまたはその生成元の CDK のコードを修正して、スタックの更新を実施する。
    • このときに、実際のリソースに変更が加えられないように注意する必要がある。

ドリフトについて

上で見たように、既存のリソースのインポートをした際に、テンプレートと実際のリソースに差分がないかの確認をドリフトが検出されないことで確認します。
(なお、Terraform の場合には terraform plan によって No changes の結果になることを確認すればコードと実際のリソースに差分がないことを確認できました)

ここで、いったん、インポートの話題からは離れて、ドリフトの検出の実際について調べてみます。

今回の題材にしている SQS のキューを構築するための CDK のコードは、たとえば次のようになります。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sqs from 'aws-cdk-lib/aws-sqs';

export class SqsTestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const queue = new sqs.Queue(this, 'SqsTestQueue', {
      queueName: "import-test",
      visibilityTimeout: cdk.Duration.seconds(10),
    });
  }
}

このコードを使って cdk deploy をすることで、題材と同じ仕様の SQS キューがデプロイできます。
このときのテンプレートを cdk synth で次のように出力することができます(SQS のリソースの部分のみを抜粋)。

Resources:
  SqsTestQueue8E896C04:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: import-test
      VisibilityTimeout: 10
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: SqsTestStack/SqsTestQueue/Resource

このテンプレートには、QueueName, VisibilityTimeout のみが Properties に指定されています。

この状態から、AWS マネジメントコンソールでこの SQS キューの設定を次のように変更してみます。

  • 可視性タイムアウトを 10 秒から 30 秒に変更
  • 配信遅延を 0 秒から 10 秒に変更

そして、このスタックに対してドリフトの検出を AWS マネジメントコンソールから実行してみると、次のように可視性タイムアウト (VisibilityTimeout) の差分が検出されています。

しかし、配信遅延 (DelaySeconds) については、テンプレートでの記述(Properties に記述がないためデフォルトの 0 秒で設定される)と実際のリソースの設定(10秒)と差がありますが、ドリフトが検出されていません。

CloudFormation のドリフトはテンプレートに明示的に記述されている値との間で比較されて検出されるものであり、テンプレートに書かれずデフォルトの値を使ってリソースがデプロイされた場合には、ドリフト検出の対象にならないのです[3]。そのために、配信遅延についてはドリフトが検出されなかったのです。

テンプレートに配信遅延の Properties が記述されていれば配信遅延についてもドリフトが検出できることを確認するため、CDK のコードに配信遅延の設定 (deliveryDelay) を加えて、cdk deploy によってスタックの更新をします。

export class SqsTestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const queue = new sqs.Queue(this, 'SqsTestQueue', {
      queueName: "import-test",
      visibilityTimeout: cdk.Duration.seconds(10),
      deliveryDelay: cdk.Duration.seconds(0),
    });
  }
}

(冒頭の import 文は変更がないので省略)

このときに cdk synth で出力されるテンプレートは次のようになっており、PropertiesDelaySeconds が追加されています。

Resources:
  SqsTestQueue8E896C04:
    Type: AWS::SQS::Queue
    Properties:
      DelaySeconds: 10
      QueueName: import-test
      VisibilityTimeout: 10
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: SqsTestStack/SqsTestQueue/Resource

この状態で、可視性タイムアウトと配信遅延を AWS マネジメントコンソールから変更して、このスタックのドリフトの検出をしてみると、可視性タイムアウトと配信遅延の差分が検出されます。

このように、ドリフトとして検出されるためには、たとえデフォルトと同じ値でリソースの構築の観点から省略可能な属性であっても、属性の値をテンプレートやその生成元のCDK のコードに記述することが必要であることがわかります。
cdk import の際にテンプレートと実際のリソースの間の差分を検出するためのドリフト検出でも同じであり、インポートで用いる CDK のコードに指定可能な属性を記述しておかないと、インポートした際のテンプレートと実際のリソースの間の差分を検出できず、そのテンプレートを使って他の環境にデプロイしたときに、属性が同じではないリソースがデプロイされてしまう可能性があることを示しています。

既存リソースのプロパティの値の一部は AWS マネジメントコンソールから取得できますが、網羅的に取得するには AWS CLI を使うのが便利でしょう。また、上で紹介した Terraform の import 機能でもプロパティを網羅的に出力できるので、その出力を CDK のコード記述の参考にするのも方法の一つです。

cdk import を実行してみる

いよいよ、既存の SQS キューに対して cdk import を実行してみます。

CDK プロジェクトを作成し、何もリソースがないスタックを作成するため cdk deploy を実行しておきます。これによって、スタックが作成されます。

$ mkdir sqs_test
$ cd sqs_test
$ cdk init -l typescript
$ cdk deploy

上で見たように、sqs.Queue コンストラクタでは、変換されるテンプレートに出力される属性はCDK コードの属性に明示的に指定したものだけでした。 ドリフトによって既存リソースと CDK コードとの差分を正しく検出できるようにするためには、設定可能な属性を網羅する必要があります。
そのようなコードは例えば次のようなものになります(まだ指定が足りない属性がある可能性もあります)。

export class SqsTestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const queue = new sqs.Queue(this, 'SqsTestQueue', {
      queueName: "import-test",
      visibilityTimeout: cdk.Duration.seconds(10),
      deliveryDelay: cdk.Duration.seconds(0),
      maxMessageSizeBytes: 262144,
      retentionPeriod: cdk.Duration.seconds(345600),
      receiveMessageWaitTime: cdk.Duration.seconds(0),
      dataKeyReuse: cdk.Duration.seconds(0),
    });
  }
}

このコードを使って、cdk import で既存のリソースをインポートしてみます。

$ cdk import
The 'cdk import' feature is currently in preview.
SqsTestStack
SqsTestStack/SqsTestQueue/Resource (AWS::SQS::Queue): enter QueueUrl to import (empty to skip): https://sqs.ap-northeast-1.amazonaws.com/XXXXXXXXXXXX/import-test
SqsTestStack: importing resources into stack...
SqsTestStack: creating CloudFormation changeset...

 ✅  SqsTestStack
Import operation complete. We recommend you run a drift detection operation to confirm your CDK app resource definitions are up-to-date. Read more here: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/detect-drift-stack.html

cdk import を実行するとインポートするキューの Queue Url を聞かれるので入力します。
処理が終了すると、このスタックのテンプレートにこのキューのリソースの記述が入っていることを確認できます。

$ aws cloudformation get-template --stack-name SqsTestStack | jq .TemplateBody | jq -r
Resources:
  SqsTestQueue8E896C04:
    Type: AWS::SQS::Queue
    Properties:
      DelaySeconds: 0
      MaximumMessageSize: 262144
      MessageRetentionPeriod: 345600
      QueueName: import-test
      ReceiveMessageWaitTimeSeconds: 0
      VisibilityTimeout: 30
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: SqsTestStack/SqsTestQueue/Resource
(以下略)

cdk import を実行したときのメッセージの最後に "We recommend you run a drift detection operation to confirm your CDK app resource definitions are up-to-date" (CDK のリソースが実際の最新のリソースと一致しているか、ドリフト検知してね)と出ていますので、それに従って AWS マネジメントコンソール上から「ドリフト検出」を実行してみると、IN_SYNCとなり、ドリフトは検出されませんでした。

テンプレートで記述されてない属性の実際のリソースとの差分は検出できない

次に、テンプレートで属性が記述されていない場合に cdk import によって既存のリソースの内容を正しく把握しインポートできるのかを確認するため、可視性タイムアウトを明示的に指定していない次のコードを考えます。

export class SqsTestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const queue = new sqs.Queue(this, 'SqsTestQueue', {
      queueName: "import-test",
    });
  }
}

同様に cdk import を実行するとスタックに既存リソースがインポートされます。そして、ドリフトを確認すると IN_SYNC と表示され、ドリフトは検出されませんでした。つまり、既存のリソースと(CDK コードから変換された) テンプレートの間に差分がないことを示しているように見えます。
これでめでたし、としたいところですが、実際のリソースでは可視性タイムアウトをデフォルトの 30 秒から変えて 10 秒に設定しています。
既存リソースの可視性タイムアウトは 10 秒であるのに、この CDK のコードを使って別の環境に cdk deploy をすると、可視性タイムアウトがデフォルトの 30 秒の SQS キューが構築されてしまうことになり、環境間で差異が生じてしまいます。
これは、同じコードからは同じ属性のリソースがデプロイできるという IaC の意義を大きく損なってしまいます。

このように、インポートしたいリソースの属性をテンプレートやその生成元の CDK コードで網羅しておかないと、たとえ「ドリフトなし」と判定されても、テンプレート通りのリソースになっているとは限らないことになります。
(なお、Terraform ではコードに書かれていない(つまりデフォルト値が使われている)属性の値についても実際のリソースと比較するところが挙動として異なります)

import の操作でリソースの変更をしてしまう??

さらに検証をする中で、怖い、と感じてしまう事象に遭遇しました。

次に、あえて可視性タイムアウトを実際のリソースの 10 秒とは異なる 30 秒で指定して、cdk import をしてみます(cdk import をし直すために cdk destroy をして、リソースの手動作成、スタックの作成もやり直す)。

export class SqsTestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const queue = new sqs.Queue(this, 'SqsTestQueue', {
      queueName: "import-test",
      visibilityTimeout: cdk.Duration.seconds(30),   // <-----------この行を修正
      deliveryDelay: cdk.Duration.seconds(0),
      maxMessageSizeBytes: 262144,
      retentionPeriod: cdk.Duration.seconds(345600),
      receiveMessageWaitTime: cdk.Duration.seconds(0),
      dataKeyReuse: cdk.Duration.seconds(0),
    });
  }
}

このコードを用いて cdk import を実行したのち、ドリフト検出をすれば、CDK コードで指定している可視性タイムアウトの 30 秒と実際のリソースの可視性の 10 秒の間にドリフトが検出されると期待したのですが、実際にはドリフト検出されませんでした。

おや、と思って実際の SQS キューの設定を確認したところ、なんと、SQS キューのリソースの可視性タイムアウトが 30 秒に更新されていました。
つまり、インポートの操作を意図していたのに、リソースに修正が加わってしまったのです。

CloudFormation のスタックのイベント一覧を見ると、次のようにインポートイベントのあとに "Apply stack-level tags to imported resource if applicable." と書かれたスタックの更新が行われています。

そして、CloudTrail で API のコール履歴を確認すると、CloudFormation が SQS キューのパラメータ設定を変更する SetQueueAttributes をコールしていました。

そのパラメータに可視性タイムアウトが 30 秒に指定されていましたので、この API コールによって既存のリソースが操作されたと考えて間違えないでしょう。
cdk import でリソースをインポートするだけのはずが、リソースの操作まで行われてしまっていたという状況です。今回はあえてパラメータを実際のリソースと別のものを設定しましたが、意図せずに間違えてしまうと、それはドリフトではなく、実際のリソースにその値が適用されてしまうことがあることを示しています。怖い。。。(ただ、これは SQS の話であり、他のリソースでは事情が違う可能性もあります。)

cdk import でリソースのインポート操作によって既存リソースへの変更がないかを検証しておく必要がありそう

SQS の場合には、コードに指定したプロパティの値が実際のリソースと異なっている場合、コードに指定した値が反映されてしまうという事象に当方の検証では遭遇しました。また、AWS のドキュメントにあるように、ドリフトとしてコードと実際のリソースの差分が検出された場合には、その差分を埋めるようなスタックの更新が必要になります。スタックの更新はリソースを操作に至る可能性がある操作であり、いきなり cdk deploy するのではなく cdk deploy -m prepare-change-setを使って変更セットを作ってその内容を確認する、または必要に応じて別環境で検証するなど、慎重に進める必要があろうかと思います。

おわりに

Terraform と AWS CDK のインポート機能を比べてみました。
AWS の製品である CloudFormation や AWS CDK と、サードパーティ製品である Terraform は、様々な場面で比較されますが、インポート機能については、Terraform の方が少ない操作で安全に作業ができると私個人は感じています。

脚注
  1. https://developer.hashicorp.com/terraform/language/import ↩︎

  2. https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/resource-import-existing-stack.html ↩︎

  3. https://dev.classmethod.jp/articles/cfn-detect-drift-accuracy/ ↩︎

株式会社ログラス テックブログ

Discussion