皆さんIaCしてますか?
私はAWSだとCloudFormationやterraform、AzureだとARM TemplateやBicepをよく使っています。また、かなり後発ですが最近CDKを触る機会があり、勉強し始めたところです。
そんなところで、今回はAWSをterraformで管理する際のお話をさせて頂きます。
いきなりですがterraformにインポート機能がありますよね。
この機能ってterraform以外で構築したリソースをterraform管理のstateファイルに取り込めるということで、便利そうだなと気になっていました。
※terraformのインポート機能についてはリンク先が参考になります
今回の記事は、そのインポート機能を使ったら、思っていた以上に大変で辛かった話をしたいと思います。
背景
私が参加したプロジェクトで、ステージング環境はマネジメントコンソールで、本番環境はterraformで構築するといったことがありました。
最初からterraformで作れよって突っ込みがありそうですが、意外にこういうプロジェクトってありませんか?
そんなこんなで、両環境を構築した後に、ステージング環境もterraform管理にしようとなりました。
そこ利用したのがterraformのインポート機能です。
インポートを実行するには、まず初めにインポートするリソース分resourceブロックを用意する必要があります。
resourceブロックの例は以下の通りです。
resource "aws_instance" "sample1" {
}
このresourceブロックをリソース分用意するというのがまぁ大変で、リソースが多ければ多いほど用意する数が増えるので、とてつもない数用意する必要がありました。実際このresourceブロックの準備は別のメンバーがやってくれたので、お前が大変さを語るな!と怒られそうです。
resourceブロックの準備が終わり、いざインポート!ということで、terraform planを実行して結果を見たら、あらびっくり!
とてつもない数のadd
、change
、destroy
が出てきました。
スクショを取っておけば良かった。。。という後悔ですが、皆さんに見て頂きたい位、とんでもない数が出ました。
インポートするにあたり、この差分を解消するか、設定変更もしくは作り直しを受け入れるしかないので、この差分を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_forwardingとenable_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!