アソビューAdvent Calendar 2022の6日目の記事です。
こんにちは!
アソビュー!でサーバーサイドエンジニア兼 Embedded SREを担当している山野です。
弊社ではプロダクト開発チームとインフラ全体の管理を担当するSREチームが存在しますが、アソビューが保有している多様なサービスに対してより個別最適化されたインフラ管理・運用等を行うため、一部のプロダクト開発チームメンバーで構成されたEmbedded SREという体制を導入しています。
Embedded SREについてはこのアドベントカレンダーの別の記事で詳しくご紹介しますので是非ご覧ください!
今回はEmbedded SREの取り組みの中から、terraform resource
ブロックの実装によって意図しないデプロイの発生を未然に防ぐため、moved
ブロックを利用したコード改善を行なった件について書きたいと思います。
この記事で説明しないこと
- terraformの概要・コマンド・基本構文
今回問題になったこと
弊社ではterraformによるインフラリソースをコード管理を行なっていますが、一部の構成ファイル(以降 tfファイル
と記載)の中には、前述の通りインシデントを招く恐れのあるコードが存在しました。
以下がそのコード例となります。
main.tf
terraform { required_version = "~> 1.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 4.41.0" } } } provider "aws" { region = "ap-northeast-1" } module "ssm" { source = "./modules/ssm" application = "demo-app" environment = "development" parameters = [ "DATABASE_USERNAME", "DATABASE_PASSWORD", "APP_API_KEY", "APP_API_SECRET", ] }
module/ssm/ssm.tf
variable "application" { type = string } variable "environment" { type = string } variable "parameters" { type = list(string) } resource "aws_ssm_parameter" "secret_parameter" { count = to(var.parameters) name = join("/", ["", var.application, var.environment, var.parameters[count.index]]) type = "SecureString" value = "dummy" key_id = "alias/aws/ssm" lifecycle { ignore_changes = [ value ] } }
このコードはAWS Systems Managerの パラメータストアリソースを作成するコードです。
この実装ではパラメータストアのキーの情報を配列型でmain.tfファイル
からssm.tfファイル
に渡し、ssm.tfファイル
のresourceブロック > countによるループ処理で受け取った配列要素分のリソース作成を行います。
実際にこのコードを実行すると以下の4つのパラメータが作成されます。
terraformはリソース作成が実行される度にtfstateファイル
というファイルに作成したリソース情報を記録していますが、
resourceブロック内でcountを利用してリソースを作成した場合、
該当のリソース名(リソースを一意に表す名称)はindex番号
でtfstateファイル
に記録されます。
実際リソース作成後にterraform state list
コマンドを実行すると各リソースがindex番号で記録されていることが分かります。
module.ssm.aws_ssm_parameter.secret_parameter[0] module.ssm.aws_ssm_parameter.secret_parameter[1] module.ssm.aws_ssm_parameter.secret_parameter[2] module.ssm.aws_ssm_parameter.secret_parameter[3]
ここで問題になるのがcountでループ処理する配列に対して要素の途中追加や削除変更を行った場合です。
terraformはterraform plan / apply
を実行した際に、前述のtfstateファイル
とtfファイル
の内容を比較し、その変更差分を検知する仕組みとなっています。
配列の内容を変更してしまうと要素に紐づくindex番号は当然ながら変更されます。
これによりterraformがtfファイル
とtfstateファイル
の変更差分比較を行った際に、同じ要素にも関わらずリソース名(=index番号)が異なるため、リソースに変更が入ったと判断 -> 作成済みのリソースを意図せず削除・再度新規リソースを引き起こす恐れがあります。
以下は実際に上記の問題を再現した例です。
main.tfファイル
> module "ssm"
> parameters
配列にの
DATABASE_PASSWORD
とAPP_API_KEY
の間にDATABASE2_USERNAME
・DATABASE2_PASSWORD
を追加しました。
main.tf
module "ssm" { source = "./modules/ssm" application = "demo-app" environment = "development" parameters = [ "DATABASE_USERNAME", "DATABASE_PASSWORD", "APP_API_KEY", "APP_API_SECRET", ] }
↓
module "ssm" { source = "./modules/ssm" application = "demo-app" environment = "development" parameters = [ "DATABASE_USERNAME", "DATABASE_PASSWORD", "DATABASE2_USERNAME", # 途中追加 "DATABASE2_PASSWORD", # 途中追加 "APP_API_KEY", "APP_API_SECRET", ] }
terraform plan
を実行すると元々index番号 2にあたるAPP_API_KEY
がDATABASE2_USERNAME
変更され、更にAPP_API_KEY
はindex番号 4として新しくリソース作成対象になっていることが分かります。
# module.ssm.aws_ssm_parameter.secret_parameter[2] must be replaced -/+ resource "aws_ssm_parameter" "secret_parameter" { ~ arn = "arn:aws:ssm:ap-northeast-1:xxxxx:parameter/demo-app/development/APP_API_KEY" -> (known after apply) ~ data_type = "text" -> (known after apply) ~ id = "/demo-app/development/APP_API_KEY" -> (known after apply) + insecure_value = (known after apply) ~ name = "/demo-app/development/APP_API_KEY" -> "/demo-app/development/DATABASE2_USERNAME" # forces replacement - tags = {} -> null ~ tags_all = {} -> (known after apply) ~ tier = "Standard" -> (known after apply) ~ value = (sensitive value) ~ version = 2 -> (known after apply) # (2 unchanged attributes hidden) } :省略 # module.ssm.aws_ssm_parameter.secret_parameter[4] will be created + resource "aws_ssm_parameter" "secret_parameter" { + arn = (known after apply) + data_type = (known after apply) + id = (known after apply) + insecure_value = (known after apply) + key_id = "alias/aws/ssm" + name = "/demo-app/development/APP_API_KEY" + tags_all = (known after apply) + tier = (known after apply) + type = "SecureString" + value = (sensitive value) + version = (known after apply) }
今回対応したこと
まずリソース名をindex番号管理ではなく、
別の特定(固定)の一位の値を設定するように修正しました。
今回はSSMに登録するパラメータキー名をリソース名となるよう修正しました。
以下が修正例です。
countではなくfor_eachを用いることで上記を実現しています。
resource "aws_ssm_parameter" "secret_parameter" { for_each = toset(var.parameters) name = join("/", ["", var.application, var.environment, each.value]) type = "SecureString" value = "dummy" key_id = "alias/aws/ssm" lifecycle { ignore_changes = [ value ] } }
ただしこの修正だけでは問題があります。
元々リソース名をindex番号としていたものを特定名称に変更するため、
terraformは既存のリソースが削除され別のリソースを新規追加されたと判断します。
以下はterraform plan
を実行した結果です。
作成済みのDATABASE_USERNAME
リソースを削除し、再度作成しようとしていることが分かります。
# module.ssm.aws_ssm_parameter.secret_parameter[0] will be destroyed # (because resource does not use count) - resource "aws_ssm_parameter" "secret_parameter" { - arn = "arn:aws:ssm:ap-northeast-1:xxxxxxx:parameter/demo-app/development/DATABASE_USERNAME" -> null - data_type = "text" -> null - id = "/demo-app/development/DATABASE_USERNAME" -> null - key_id = "alias/aws/ssm" -> null - name = "/demo-app/development/DATABASE_USERNAME" -> null - tags = {} -> null - tags_all = {} -> null - tier = "Standard" -> null - type = "SecureString" -> null - value = (sensitive value) - version = 2 -> null } :省略 # module.ssm.aws_ssm_parameter.secret_parameter["DATABASE_USERNAME"] will be created + resource "aws_ssm_parameter" "secret_parameter" { + arn = (known after apply) + data_type = (known after apply) + id = (known after apply) + insecure_value = (known after apply) + key_id = "alias/aws/ssm" + name = "/demo-app/development/DATABASE_USERNAME" + tags_all = (known after apply) + tier = (known after apply) + type = "SecureString" + value = (sensitive value) + version = (known after apply) }
この事象を回避するためにmovedブロックを利用しました。
movedブロックはterraform バージョン1.1.0から追加された機能で、リソース名を明示的に変更できる機能です。
実装方法としては以下の通り変更したいリソース名をfrom
to
で指定するのみです。
moved { from = {変更前のリソース名} to = {変更後のリソース名} }
以下が実際のコード例です。
main.tf
# : 省略 module "ssm" { source = "./modules/ssm" application = "demo-app" environment = "development" parameters = [ "DATABASE_USERNAME", "DATABASE_PASSWORD", "APP_API_KEY", "APP_API_SECRET", ] } ## moved ブロックを追加 moved { from = module.ssm.aws_ssm_parameter.secret_parameter[0] to = module.ssm.aws_ssm_parameter.secret_parameter["DATABASE_USERNAME"] } moved { from = module.ssm.aws_ssm_parameter.secret_parameter[1] to = module.ssm.aws_ssm_parameter.secret_parameter["DATABASE_PASSWORD"] } moved { from = module.ssm.aws_ssm_parameter.secret_parameter[2] to = module.ssm.aws_ssm_parameter.secret_parameter["APP_API_KEY"] } moved { from = module.ssm.aws_ssm_parameter.secret_parameter[3] to = module.ssm.aws_ssm_parameter.secret_parameter["APP_API_SECRET"] }
これでterraform planを実行すると以下の通りリソースの変更なしと判断されます。
module.ssm.aws_ssm_parameter.secret_parameter["APP_API_KEY"]: Refreshing state... [id=/demo-app/development/APP_API_KEY] module.ssm.aws_ssm_parameter.secret_parameter["APP_API_SECRET"]: Refreshing state... [id=/demo-app/development/APP_API_SECRET] module.ssm.aws_ssm_parameter.secret_parameter["DATABASE_USERNAME"]: Refreshing state... [id=/demo-app/development/DATABASE_USERNAME] module.ssm.aws_ssm_parameter.secret_parameter["DATABASE_PASSWORD"]: Refreshing state... [id=/demo-app/development/DATABASE_PASSWORD] 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. Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
またterraform apply
実行後にterraform state list
を実行すると、
リソース名が正常に変更されていることが分かります。
module.ssm.aws_ssm_parameter.secret_parameter["APP_API_KEY"] module.ssm.aws_ssm_parameter.secret_parameter["APP_API_SECRET"] module.ssm.aws_ssm_parameter.secret_parameter["DATABASE_PASSWORD"] module.ssm.aws_ssm_parameter.secret_parameter["DATABASE_USERNAME"]
これを踏まえてfor_eachでループ処理をする配列(main.tfファイル
> module "ssm"
> parameters
配列)にDATABASE2_USERNAME
・DATABASE2_PASSWORD
を追加しterraform plan
を実行してみました。
リストに追加された2要素のみ差分として検知され、既存のリソースへの影響が発生しないことが分かります。
# module.ssm.aws_ssm_parameter.secret_parameter["DATABASE2_PASSWORD"] will be created + resource "aws_ssm_parameter" "secret_parameter" { + arn = (known after apply) + data_type = (known after apply) + id = (known after apply) + insecure_value = (known after apply) + key_id = "alias/aws/ssm" + name = "/demo-app/development/DATABASE2_PASSWORD" + tags_all = (known after apply) + tier = (known after apply) + type = "SecureString" + value = (sensitive value) + version = (known after apply) } # module.ssm.aws_ssm_parameter.secret_parameter["DATABASE2_USERNAME"] will be created + resource "aws_ssm_parameter" "secret_parameter" { + arn = (known after apply) + data_type = (known after apply) + id = (known after apply) + insecure_value = (known after apply) + key_id = "alias/aws/ssm" + name = "/demo-app/development/DATABASE2_USERNAME" + tags_all = (known after apply) + tier = (known after apply) + type = "SecureString" + value = (sensitive value) + version = (known after apply) } Plan: 2 to add, 0 to change, 0 to destroy.
まとめ
以上がterraformのmovedブロックを使った安全なコードへのリファクタリング対応です。
今回出てきたcountは前述の通り思わぬ副作用を生むので、
理由がない限り利用は避けた方が無難です。
最後に
アソビューでの開発についてもっと知りたい方はこちらをぜひご覧ください! カジュアル面談もありますので、少しでもご興味があればご応募ください!
参考
Resources - Configuration Language | Terraform | HashiCorp Developer The count Meta-Argument - Configuration Language | Terraform | HashiCorp Developer The for_each Meta-Argument - Configuration Language | Terraform | HashiCorp Developer Refactoring | Terraform | HashiCorp Developer