7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

マネジメントコンソールでAWSリソースを構築して、terraformコードを作ってからインポートしたら想像以上に辛かった話

Last updated at Posted at 2025-04-08

皆さんIaCしてますか?
私はAWSだとCloudFormationやterraform、AzureだとARM TemplateやBicepをよく使っています。また、かなり後発ですが最近CDKを触る機会があり、勉強し始めたところです。

そんなところで、今回はAWSをterraformで管理する際のお話をさせて頂きます。
image.png

いきなりですがterraformにインポート機能がありますよね。
この機能ってterraform以外で構築したリソースをterraform管理のstateファイルに取り込めるということで、便利そうだなと気になっていました。
※terraformのインポート機能についてはリンク先が参考になります

今回の記事は、そのインポート機能を使ったら、思っていた以上に大変で辛かった話をしたいと思います。

背景

私が参加したプロジェクトで、ステージング環境はマネジメントコンソールで、本番環境はterraformで構築するといったことがありました。
最初からterraformで作れよって突っ込みがありそうですが、意外にこういうプロジェクトってありませんか?

そんなこんなで、両環境を構築した後に、ステージング環境もterraform管理にしようとなりました。
そこ利用したのがterraformのインポート機能です。

インポートを実行するには、まず初めにインポートするリソース分resourceブロックを用意する必要があります。
resourceブロックの例は以下の通りです。

resource "aws_instance" "sample1" {
}

このresourceブロックをリソース分用意するというのがまぁ大変で、リソースが多ければ多いほど用意する数が増えるので、とてつもない数用意する必要がありました。実際このresourceブロックの準備は別のメンバーがやってくれたので、お前が大変さを語るな!と怒られそうです。

resourceブロックの準備が終わり、いざインポート!ということで、terraform planを実行して結果を見たら、あらびっくり!
とてつもない数のaddchangedestroyが出てきました。
スクショを取っておけば良かった。。。という後悔ですが、皆さんに見て頂きたい位、とんでもない数が出ました。

インポートするにあたり、この差分を解消するか、設定変更もしくは作り直しを受け入れるしかないので、この差分を1つずつチェックする必要があるのですが、これがまぁ大変。
ということで、この記事では私の環境で出た差分と、その原因についてご紹介しますので、見て頂いた方の差分調査にかける時間を少しでも減らすことが出来れば幸いです。

いきなり結論

流行りのいきなり結論ということで、なんで私の環境でそんなに差分が出たのか?
結論は、同じ設定のリソースを作ろうとしてもマネジメントコンソールでの構築とterraformでの構築では一部の設定値が異なったり、片方にしかない設定が存在するからです。

勘違いが無いようにお伝えすると、明示的に指定している設定値については基本的に問題ありません。オプションとなっていて、特に設定する必要がないと思って記載しない設定であったり、デフォルト値で問題ないと思い明示的に記載しない設定で差異が発生していました。そもそもマネジメントコンソールでは設定出来ない項目もあったりするので、初見だと差分の調査に時間をかなり要しますが、一度経験してしまえば二回目以降は差分を調査する時間を削減できると思います。

なので、是非この記事を上手く活用して頂ければと思います!

AWSリソースで発生した差分

今回私がインポートした主なAWSサービスは以下のサービスです。

  • VPC
  • VPC関連リソース(Subnet, RouteTable,SG,ManagedPrefixList,VPCEndpoint等)
  • Route53
  • API Gateway
  • ALB
  • Lambda
  • ECS on Fargate
  • Aurora for PostgreSQL
  • DynamoDB
  • OpenSearch
  • S3
  • ECR
  • SSM ParameterStore
  • SecretManage
  • EventBridge
  • CodeDeploy

これ以降で、上記のリソース群の中から差分が発生したリソースの差分内容について記述します。

SG

aws_security_group

revoke_rules_on_delete

この項目は、マネジメントコンソールでは存在しない設定項目で、terraform独自の設定項目となるため、差分が発生します。

terraform独自の設定項目ということで設定内容としては、依存関係があるSGを削除するときに依存関係を解消してからSGを削除してくれるといった設定になります。具体的には特定のAWSサービスでは必要なルールを自動的にSGに追加することがあると思いますが、この場合SGを削除しようとしても依存関係を削除しないとSGが削除できません。そこで、この設定をtrueにするとアタッチされたすべてのルールを先に削除してから、SG自体を削除するような挙動になるため、SGを削除出来るといった内容になります。また、この設定自体は対象となるSGにterraform側で自動的に付けられるため、一部のSGにのみに設定されていました。

resource "aws_security_group" "example_sg" {
  name                   = "example"
  description            = "example"
  vpc_id                 = aws_vpc.main.id
  revoke_rules_on_delete = true
}

ALB

aws_lb_listener

stickiness

マネジメントコンソールにてListenerのデフォルトアクションで転送を選んだ場合、この設定をオフにしていても関連項目にデフォルト値(3600)で入ってしまいます。一方で、terraformで構築した場合、この設定自体はオフで関連項目(duration)は記載していないため、何も設定されていない状態になっているため、そこが差分として発生します。

resource "aws_lb_listener" "example" {
  load_balancer_arn = aws_lb.example.id

  default_action {
    type         = "forward"
    forward {
      target_group {
        arn      = aws_lb_target_group.lb-tg.arn
        weight   = 1
      }

      stickiness {
        duration = 3600
        enabled  = false
      }
    }
  }
}

aws_lb_listener_rule

stickiness

マネジメントコンソールにてListener Ruleで転送を選んだ場合、この設定をオフにしていても関連項目にデフォルト値(3600)で入ってしまいます。一方で、terraformで構築した場合、この設定自体はオフで関連項目(duration)は記載していないため、何も設定されていない状態になっているため、そこが差分として発生します。

resource "aws_lb_listener_rule" "host_based_routing" {
  listener_arn = aws_lb_listener.front_end.arn
  priority     = 99

  action {
    type = "forward"
    forward {
      target_group {
        arn    = aws_lb_target_group.main.arn
        weight = 1
      }

      stickiness {
        enabled  = false
        duration = 3600
      }
    }
  }

  condition {
    host_header {
      values = ["my-service.*.terraform.io"]
    }
  }
}

aws_lb_target_group

lambda_multi_value_headers_enabled

この項目はマネジメントコンソールで作成した場合、明示的に設定しなければ何もはいりません。一方で、terraformで作成した場合は明示的に指定しなくてもfalseで入るため、差分として表示されます。

resource "aws_lb_target_group" "test" {
  name                               = "tf-example-lb-tg"
  port                               = 80
  protocol                           = "HTTP"
  vpc_id                             = aws_vpc.main.id
  lambda_multi_value_headers_enabled = false
}

proxy_protocol_v2

この項目はマネジメントコンソールで作成した場合、明示的に設定しなければ何もはいりません。一方で、terraformで作成した場合は明示的に指定しなくてもfalseで入るため、差分として表示されます。

resource "aws_lb_target_group" "test" {
  name              = "tf-example-lb-tg"
  port              = 80
  protocol          = "HTTP"
  vpc_id            = aws_vpc.main.id
  proxy_protocol_v2 = false
}

healthy_threshold

この項目はマネジメントコンソールでの作成とterraformでの作成のデフォルト値の違いがあります。マネジメントコンソールで作成した場合、設定内容を変更しなければデフォルトで5が入ります。一方で、terraformで作成した場合、明示的に指定しないとデフォルトで3が入るため、差分として表示されます。

resource "aws_lb_target_group" "tcp-example" {
  name     = "tf-example-lb-nlb-tg"
  port     = 80
  protocol = "TCP"
  vpc_id   = aws_vpc.main.id

  health_check {
    path                = "/health"
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }
}

unhealthy_threshold

この項目はマネジメントコンソールでの作成とterraformでの作成のデフォルト値の違いがあります。マネジメントコンソールで作成した場合、設定内容を変更しなければデフォルトで2が入ります。一方で、terraformで作成した場合、明示的に指定しないとデフォルトで3が入るため、差分として表示されます。

resource "aws_lb_target_group" "tcp-example" {
  name     = "tf-example-lb-nlb-tg"
  port     = 80
  protocol = "TCP"
  vpc_id   = aws_vpc.main.id

  health_check {
    path                = "/health"
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }
}

Lambda

aws_lambda_function

publish

この項目はマネジメントコンソールで作成した場合、何も設定されません(Versioningを設定していないから?)。一方で、terraformで作成した場合は明示的に指定しなくてもfalseで入るため、差分として表示されます。

resource "aws_lambda_function" "test_lambda" {
  # If the file is not in the current working directory you will need to include a
  filename      = "lambda_function_payload.zip"
  function_name = "lambda_function_name"
  role          = aws_iam_role.iam_for_lambda.arn
  handler       = "index.test"
  publish       = false

  source_code_hash = data.archive_file.lambda.output_base64sha256

  runtime = "nodejs18.x"

  environment {
    variables = {
      foo = "bar"
    }
  }
}

lambda_function_event_invoke_config

function_name

この項目はマネジメントコンソールで作成した場合、LambdaのARNが入ります。一方で、terraformで作成する場合はLambdaのARN、もしくはLambda関数の名前で指定可能です。今回我々はterraformでLambda関数の名前を記載していまっていたため、差分として表示されました。

resource "aws_lambda_function_event_invoke_config" "example" {
  function_name                = aws_lambda_function.test_lambda.arn
  maximum_event_age_in_seconds = 900
  maximum_retry_attempts       = 0
}

Aurora

aws_rds_cluste

enable_global_write_forwardingenable_local_write_forwarding

この項目はマネジメントコンソールで作成した場合、明示的に設定しなければ何もはいりません。一方で、terraformで作成した場合は明示的に指定しなくてもfalseで入るため、差分として表示されます。

resource "aws_rds_cluster" "example" {
  cluster_identifier             = "example"
  engine                         = "aurora-postgresql"
  engine_mode                    = "provisioned"
  engine_version                 = "13.6"
  database_name                  = "test"
  master_username                = "test"
  master_password                = "must_be_eight_characters"
  storage_encrypted              = true
  enable_global_write_forwarding = false
  enable_local_write_forwarding  = false

  serverlessv2_scaling_configuration {
    max_capacity             = 1.0
    min_capacity             = 0.0
    seconds_until_auto_pause = 3600
  }
}

manage_master_user_password

この項目は、マネジメントコンソールでは存在しない設定項目で、terraform独自の設定項目となるため、差分が発生します。

terraform独自の設定項目ということで設定内容としては、RDSでSecretManagerを使ったログインを設定する際に、それをterraform側で認識させるための設定になります。この設定をtrueするとterraform側でSecretManagerを使ったログインで設定していることを認識し、関連リソースの設定等を行います。

resource "aws_rds_cluster" "example" {
  cluster_identifier             = "example"
  engine                         = "aurora-postgresql"
  engine_mode                    = "provisioned"
  engine_version                 = "13.6"
  database_name                  = "test"
  master_username                = "test"
  master_password                = "must_be_eight_characters"
  storage_encrypted              = true
  manage_master_user_password    = true

  serverlessv2_scaling_configuration {
    max_capacity             = 1.0
    min_capacity             = 0.0
    seconds_until_auto_pause = 3600
  }
}

aws_rds_cluster_parameter_group

description

この項目はマネジメントコンソールで作成した場合、DB Cluster Parameter Groupと入ります。一方で、terraformで作成した場合はManaged by Terraformで入るため、差分として表示されました。

resource "aws_rds_cluster_parameter_group" "default" {
  name        = "rds-cluster-pg"
  family      = "aurora5.6"
  description = "Managed by Terraform"

  parameter {
    name  = "character_set_server"
    value = "utf8"
  }
}

aws_db_parameter_group

description

この項目はマネジメントコンソールで作成した場合、DB Instance Parameter Groupと入ります。一方で、terraformで作成した場合はManaged by Terraformで入るため、差分として表示されました。

resource "aws_db_parameter_group" "default" {
  name   = "rds-pg"
  family = "mysql5.6"
  description = "Managed by Terraform"

  parameter {
    name  = "character_set_server"
    value = "utf8"
  }
}

aws_db_subnet_group

description

この項目はマネジメントコンソールで作成した場合、aurora subnet groupと入ります。一方で、terraformで作成した場合はManaged by Terraformで入るため、差分として表示されました。

resource "aws_db_subnet_group" "default" {
  name       = "main"
  subnet_ids = [aws_subnet.frontend.id, aws_subnet.backend.id]
  description = "Managed by Terraform"

  tags = {
    Name = "My DB subnet group"
  }
}

aws_rds_cluster_instance

force_destroy

この項目はマネジメントコンソールで作成した場合、明示的に設定しなければ何もはいりません。一方で、terraformで作成した場合は明示的に指定しなくてもfalseで入るため、差分として表示されます。

resource "aws_rds_cluster_instance" "cluster_instances" {
  count              = 2
  identifier         = "aurora-cluster-demo-${count.index}"
  cluster_identifier = aws_rds_cluster.default.id
  instance_class     = "db.r4.large"
  engine             = aws_rds_cluster.default.engine
  engine_version     = aws_rds_cluster.default.engine_version
  force_destroy      = false
}

ElastiCache for Redis

aws_elasticache_replication_group

auth_token_update_strategy

この項目はマネジメントコンソールで作成した場合、明示的に設定しなければ何もはいりません。一方で、terraformで作成した場合は明示的に指定しなくてもROTATEで入るため、差分として表示されます。

resource "aws_elasticache_replication_group" "example" {
  replication_group_id = "example"
  description          = "example with authentication"
  node_type            = "cache.t2.micro"
  num_cache_clusters   = 1
  port                 = 6379
  subnet_group_name    = aws_elasticache_subnet_group.example.name
  security_group_ids   = [aws_security_group.example.id]
  parameter_group_name = "default.redis5.0"
  engine_version       = "5.0.6"

  transit_encryption_enabled = false
  auth_token_update_strategy = "ROTATE"
}

DynamoDB

aws_appautoscaling_policy

name

この項目はマネジメントコンソールで作成した場合、明示的に設定出来ない項目となり、$<テーブル名>-scaling-policyで設定される。一方で、terraformでは明示的に設定出来る項目となり、名前を指定しまったため差分として表示されました。

resource "aws_appautoscaling_policy" "dynamodb_table_read_policy" {
  name               = "$<Table Name>-scaling-policy"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.dynamodb_table_read_target.resource_id
  scalable_dimension = aws_appautoscaling_target.dynamodb_table_read_target.scalable_dimension
  service_namespace  = aws_appautoscaling_target.dynamodb_table_read_target.service_namespace

  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "DynamoDBReadCapacityUtilization"
    }

    target_value = 70
  }
}

OpenSearch

aws_cloudwatch_log_resource_policy

作成単位

OpenSearchでログ出力を行う場合、マネジメントコンソールではリソースポリシーが出力するログ毎に1つ自動で作成され、ログをCloudWatch Logsに出力することが出来る。一方で、terraformでは明示的にリソースを定義して作成する必要があるため、1つのリソースポリシーに出力するログ全ての設定を記載したため、差分として表示さました。

# エラーログ用ポリシー準備
data "aws_iam_policy_document" "application_logs_iam_policy" {
  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = [
      "arn:aws:logs:ap-northeast-1:xxxxxxxxxxxx:log-group:/aws/OpenSearchService/domains/<OpenSearch名>/application-logs:*"
    ]

    principals {
      identifiers = ["es.amazonaws.com"]
      type        = "Service"
    }
  }
}

# インデックススローログ用ポリシー準備
data "aws_iam_policy_document" "index_logs_iam_policy" {
  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = [
      "arn:aws:logs:ap-northeast-1:xxxxxxxxxxxx:log-group:/aws/OpenSearchService/domains/<OpenSearch名>/index-logs:*"
    ]

    principals {
      identifiers = ["es.amazonaws.com"]
      type        = "Service"
    }
  }
}

# 検索スローログ用ポリシー準備
data "aws_iam_policy_document" "search_logs_iam_policy" {
  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = [
      "arn:aws:logs:ap-northeast-1:xxxxxxxxxxxx:log-group:/aws/OpenSearchService/domains/<OpenSearch名>/search-logs:*"
    ]

    principals {
      identifiers = ["es.amazonaws.com"]
      type        = "Service"
    }
  }
}

# エラーログ用リソースポリシー作成
resource "aws_cloudwatch_log_resource_policy" "application-logs" {
  policy_name     = "Application-logs"
  policy_document = data.aws_iam_policy_document.application_logs_iam_policy.json
}

# インデックススローログ用リソースポリシー作成
resource "aws_cloudwatch_log_resource_policy" "index-logs" {
  policy_name     = "Application-logs"
  policy_document = data.aws_iam_policy_document.index_logs_iam_policy.json
}

# 検索スローログ用リソースポリシー作成
resource "aws_cloudwatch_log_resource_policy" "search-logs" {
  policy_name     = "Application-logs"
  policy_document = data.aws_iam_policy_document.search_logs_iam_policy.json
}

S3

aws_s3_bucket

force_destroy

この項目は、マネジメントコンソールでは存在しない設定項目で、terraform独自の設定項目となるため、差分が発生します。

terraform独自の設定項目ということで設定内容としては、S3を削除するときにオブジェクトがあるとエラーになりますが、この設定をtrueにすることでオブジェクトも含めて削除することを有効化出来る設定です。この設定自体はterraformのドキュメントだとオプションになっているのですが、何も指定しなくてもデフォルトの設定値falseで入るため、差分として表示されます。

resource "aws_s3_bucket" "s3_buckets" {
  for_each      = local.s3_bucket_map
  bucket        = each.key
  force_destroy = true
}

aws_s3_bucket_versioning

expected_bucket_owner

この項目はまだ調査中ですが、一部の画面から作成したS3にアカウントIDが入っていました。一部のリソースだけなので、何かしら作る時の手順で入ったのかなと思いますが、詳しい方いれば教えてください。

こちらterraformで作成した場合は明示的に指定しないとnullで入るため、差分として表示されます。

resource "aws_s3_bucket_versioning" "bucket_versioning" {
  bucket                = aws_s3_bucket.s3_bucket.id
  expected_bucket_owner = "012345678912"

  versioning_configuration {
    status              = "Enabled"
  }
}

aws_s3_object

オブジェクト

今回はオブジェクトもterraformで作成していたのですが、オブジェクトのインポートが上手くいきませんでした。

この理由としては、昔からよくある問題ですがフォルダオブジェクトがなかったためインポートが上手く機能していませんでした。具体的には、terraformにはtest/というオブジェクトを記載していました。

resource "aws_s3_object" "s3_object" {
  bucket = aws_s3_bucket.s3_bucket.id
  key    = "test/"
}

一方で、マネジメントコンソールで構築した際はtest/index.htmlといったようにファイルごとアップロードしていたため、フォルダオブジェクトが存在せず、terraformコードで記載しているオブジェクトが差分として表示されていました。

まとめ

インポート機能については非常に便利な機能です。これは間違いなく言えることです。
今回多くの差分が発生しましたが、発生した差分は実際に設定値を間違って設定して差分が出ていたというよりは環境特有の問題が多く、どれも利用者が意図しないものでした。
インポートを実行するには、これらの差分を1つ1つ確認していく必要があるので、かなりの負荷がかかります。

私が今回実施したように、環境毎インポートするというのがレアなケースなのかもしれませんが、そういった用途で使う方も少なからずいると思っています。
そのため、今回の記事がそういった方々の助けに少しでもなれば幸いです。

また、今回の記事で記載したサービスと差分は、私が今回のプロジェクトで利用したサービスと設定値で発生したものになります。別のリソースを利用すれば同じようにそのリソースにも差分が発生する項目があるかもしれませんし、同じサービスでも設定が異なるとまた違った項目が発生することもあると思います。そのため、今回の記事はあくまでも一例で、差分が発生するリソース一覧や設定一覧ではないことをご認識頂ければ幸いです。

皆さんのIaC生活が実りあるものになりますように!

We Are Hiring!

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?