顯示具有 terraform 標籤的文章。 顯示所有文章
顯示具有 terraform 標籤的文章。 顯示所有文章

2025年4月7日 星期一

Terraform - aws_eks_cluster 開始支援 auto mode 之後的雷

AWS EKS cluster 在 auto mode 出現之前, aws_eks_cluster 通常不會去設定這幾個選項, 預設如下:

  bootstrap_self_managed_addons = true

  access_config {
    authentication_mode                         = "CONFIG_MAP"
    bootstrap_cluster_creator_admin_permissions = false #1
  }

但是 auto mode 出現之後, 有用 auto mode 就需要這樣設定:

  bootstrap_self_managed_addons = false

  access_config {
    authentication_mode                         = "API_AND_CONFIG_MAP"
    bootstrap_cluster_creator_admin_permissions = false
  }

沒用 auto mode 就需要這樣設定:

  bootstrap_self_managed_addons = true

  access_config {
    authentication_mode                         = "CONFIG_MAP"
    bootstrap_cluster_creator_admin_permissions = true #2
  }

有發現 #1 跟 #2 設定值不同

在 auto mode 出現之前已經開起來的 EKS cluster 目前還沒出現明顯異常, 但是在 terraform 裡面還是會做成設定一致, 所以就來試著更動已存在的 EKS cluster 這個設定.

  # module.cellar.module.eks.module.cluster["foobar"].aws_eks_cluster.this must be replaced
  -/+ resource "aws_eks_cluster" "this" {
        ...

      ~ access_config {
          ~ bootstrap_cluster_creator_admin_permissions = false -> true # forces replacement
            # (1 unchanged attribute hidden)
        }

        ...
    }

乾~ 更動 bootstrap_cluster_creator_admin_permissions 的設定竟然要把 cluster 砍掉重練!

為了往後新的 EKS cluster 設定的正確性, 只好在 aws_eks_cluster 裡面放一段 lifecycle 讓已存在的 cluster 不受影響:

  lifecycle {
    ignore_changes = [
      access_config[0].bootstrap_cluster_creator_admin_permissions
    ]
  }

2024年6月6日 星期四

Terraform - aws_rds_proxy 接 RDS PostgreSQL 的 init_query 指令

在 Terraform 的文件 db_proxy_default_target_group 只有提供 MySQL 的使用範例:

resource "aws_db_proxy" "example" {
  name                   = "example"
  debug_logging          = false
  engine_family          = "MYSQL"
  idle_client_timeout    = 1800
  require_tls            = true
  role_arn               = aws_iam_role.example.arn
  vpc_security_group_ids = [aws_security_group.example.id]
  vpc_subnet_ids         = [aws_subnet.example.id]

  auth {
    auth_scheme = "SECRETS"
    description = "example"
    iam_auth    = "DISABLED"
    secret_arn  = aws_secretsmanager_secret.example.arn
  }

  tags = {
    Name = "example"
    Key  = "value"
  }
}

resource "aws_db_proxy_default_target_group" "example" {
  db_proxy_name = aws_db_proxy.example.name

  connection_pool_config {
    connection_borrow_timeout    = 120
    init_query                   = "SET x=1, y=2"
    max_connections_percent      = 100
    max_idle_connections_percent = 50
    session_pinning_filters      = ["EXCLUDE_VARIABLE_SETS"]
  }
}

這邊 init_query 是用 “SET x=1, y=2” 當作 proxy 連上 DB 的測通指令, 但是 MySQL 的 SET 指令跟 PostgreSQL 的 SET 指令用法不同, 所以直接把這個範例給 PostgreSQL 用的時候會噴錯誤:

proxy log:
- [INFO] [dbConnection=1140488035] The database connection closed. Reason: An internal error occurred.

DB log:
- ERROR: syntax error at or near "=" at character 11
- STATEMENT: SET x=1, y=2

(以上來自 CloudWatch Log Groups, DB 跟 DB proxy 都有設定送出 log 到這邊)

伸進去 PostgreSQL DB 手動執行指令看看:

postgres=> SET x=1, y=2;
ERROR:  syntax error at or near "="
LINE 1: SET x=1, y=2;
                  ^
postgres=> SET x=1;
ERROR:  unrecognized configuration parameter "x"
postgres=>

確認是 SET 指令造成錯誤.

Google 查了幾下也是有一些回報同樣的問題, 解法幾乎都是把 init_query 拿掉, 但是這樣就少了一個 proxy 連入 DB 後, 測通 DB 是否有正常反應的機制.

所以找個簡單的指令替代 SET 就解決了:

init_query = "VALUES (1,2)"

AWS - IAM Role with Policy

AWS 的 IAM Role 跟 Policy 有兩種連接方式:
- Role 內嵌 Policy.
- Role 和 Policy 是各自建立, 然後再接起來(attach)用.

以 Policy 編修來說, 前者就只能在 portal 上面 CRUD, 後者在 Policy 本身的 portal 介面還可以看 permission 檢查, 歷次編修版本與控管, 相同的 Policy 還可以接到多個 Role 共用, 後來我也盡量都用後者.

不過後者在 Terraform 的寫作上有需要注意的地方, 一般寫法如下:

resource "aws_iam_policy" "this" {}
resource "aws_iam_role" "this" {}
resource "aws_iam_role_policy_attachment" "this" {
  depends_on = [
    aws_iam_role.this,
    aws_iam_policy.this
  ]
}

在 terraform apply 的時候不成問題, 但是在 terraform destroy 會發生 race condition: Policy 還沒從 Role 拆出來, 上面這個寫法會造成 aws_iam_policy 跟 aws_iam_role 同時進行 destroy, 在刪除 aws_iam_policy 就會噴錯誤訊息, 說 Policy 還接在 Role 上面所以不能被砍掉.

雖然再執行一次 terraform destroy 就過了(錯誤是在砍 aws_iam_policy 的時候噴的, 但同時砍 aws_iam_role 的動作有被完成), 但是這樣對於砍站跑路就不夠絲滑.

簡單解決的方式就是在 aws_iam_role 裡面加一個 depends_on aws_iam_policy:

resource "aws_iam_role" "this" {
  ...

  depends_on = [
    aws_iam_policy.this
  ]
}

這樣子在 destroy 的時候, aws_iam_role 就會先被刪除(無論 Policy 拆出來了沒), 再刪除 aws_iam_policy 就不會卡住了.

然後第一個方式在 Terraform 有個地雷, 在 Role 裡面內嵌 Policy 大概是這樣寫:

resource "aws_iam_role" "this" {
  ...

  inline_policy {
    name = "Role-foobar-Policy"
    ...
  }
}

當不要用 inline_pocily 的時候, 把這段刪除成這樣:

resource "aws_iam_role" "this" {
  ...
}

然後 terraform apply 的時候, diff 並沒有出現 - 掉 inline_policy 這塊的訊息, 實際上 Terraform 真的沒做這個刪除, 在 portal 是還看得到 inline_policy 的存在, 得在這邊手動砍掉才算沒了.


隱欌地雷: AWS IAM 有好幾個拆連接的動作會產生 race condition, 表面上 API 回應 ok, 但是實際上裡面還在慢慢斷開, 沒那麼快拆完...

2022年4月27日 星期三

Terraform - google_redis_instance with auth_enabled is true

google_redis_instance 裡面是這樣寫的:

auth_enabled - (Optional) Optional. Indicates whether OSS Redis AUTH is enabled for the instance.
If set to "true" AUTH is enabled on the instance. Default value is "false" meaning AUTH is disabled.

auth_string - (Optional) AUTH String set on the instance. This field will only be populated if auth_enabled is true.

redis 還是打開 auth 才安全, 所以當然就這樣設定:

resource "google_redis_instance" "this" {
  ...
  auth_enabled = true
  auth_string  = "023dbce5e060641d09218027704ca4b3"
  ...
}

接著 terraform apply 下去打開 auth...

Error: Value for unconfigurable attribute

  with module.redis.module.redis-general.google_redis_instance.this, on modules/redis/main.tf line 24, in resource
  "google_redis_instance" "this":
  24:   auth_string  = "023dbce5e060641d09218027704ca4b3"

Can't configure a value for "auth_string": its value will be decided automatically based on the result of applying
this configuration.

所以是會自動生成的意思? 那拿掉 auth_string 的設定, 先 terraform apply 上去之後, 再 terraform show 出來看 auth_string 的內容...

# module.redis.module.redis-general.google_redis_instance.this:
resource "google_redis_instance" "this" {
    alternative_location_id  = "us-west1-c"
    auth_enabled             = true
    auth_string              = (sensitive value)
    ...

竟然看不到... oroz

查了一下, 得用 terraform show -json 才看得到, 執行下去會得到一行很長很長的 json, 那就多用 jq 轉一下: terraform show -json | jq .

"resources": [
  {
    "address": "module.redis.module.redis-general.google_redis_instance.this",
    "mode": "managed",
    "type": "google_redis_instance",
    "name": "this",
    "provider_name": "registry.terraform.io/hashicorp/google",
    "schema_version": 0,
    "values": {
      "alternative_location_id": "us-west1-c",
      "auth_enabled": true,
      "auth_string": "ded6f8e9-5c32-4ebb-b0fb-086a444baa7f",
      ...
    }
  }

然後在上面這一段終於看到啦~

2022年4月25日 星期一

Terraform - provider google version upgrade

起因是為了 resource google_redis_instance 要用到 replica 的功能...

根據 CHANGELOG 文件, provider google 需用 4.17.0 以上版本. (此時最新版是 v4.18.0)

在 versions.tf 裡面原本是這樣設定一個版本來用:

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "3.58.0"
    }
  }

  required_version = "~> 1.0.0"
}

就把 version 改成 ">= 4.17.0"

順便把 required_version 也升級成 "~> 1.1.0" (此時 homebrew terraform 是 v1.1.9)

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 4.17.0"
    }
  }

  required_version = "~> 1.1.0"
}

一般情況下來說, 這樣改完之後再執行 terraform init -upgrade 就會看到原本裝好的 provider google:

- Using previously-installed hashicorp/google v3.58.0

被更新中...

- Installing hashicorp/google v4.18.0...
- Installed hashicorp/google v4.18.0 (signed by HashiCorp)

之後的 terraform init 動作就看到都是 v4.18.0

- Using previously-installed hashicorp/google v4.18.0

然後因為 provider 跨了大版本, 遇到 state file 格式變動, 還要再執行 terraform refresh 更新一遍.


但是實際上...

Initializing provider plugins...
- Finding hashicorp/google versions matching ">= 2.12.0, >= 3.45.0, < 4.0.0, >= 4.17.0"...

然後 terraform init -upgrade 就抓不到能用的升級版本.

後來發現是在某個 resource 裡面有設定 version = "~> 3.0", 莫名其妙多出上面的 < 4.0.0 的條件卡關. 直接把這個改成 version = "~> 4.0" 跟著升級上去, terraform init -upgrade 版本條件就變成:

Initializing provider plugins...
- Finding hashicorp/google-beta versions matching ">= 3.45.0, < 5.0.0"...

就有抓到可用版本(v4.18.0)升級上去了.

2021年10月20日 星期三

Terraform - Optional !?

terraform 你他媽的 Optional...

The following arguments are supported:

project - (Optional) The ID of the project in which the resource belongs.
If it is not provided, the project will be parsed from the identifier of the parent resource. If no project is provided
in the parent identifier and no project is specified, the provider project is used.

然後真的塞 project 進去, 就噴:

Error: Unsupported argument

An argument named "project" is not expected here.

2021年10月18日 星期一

Terraform - googleapi: Error 409: The Cloud SQL instance already exists

今天改了 db 的 resource dependency 要整個砍掉重練看看是不是一鍵順暢建庫成功, 所以先跑了 terraform destroy (要先關掉 db instance 的 deletion protection)再跑 terraform apply 開始蓋, 結果...

googleapi: Error 409: The Cloud SQL instance already exists.

When you delete an instance, you can't reuse the name of the deleted instance until one week from the deletion date.,
instanceAlreadyExists

我以為這種鳥蛋限制只會在 Azure 上面出現.... <囧>

這年頭連 GCP 都要做誤刪資料庫甚至刪庫跑路的災難回復服務了?

這還不是最鳥的問題, 而是用 terraform apply 進行 create sql instance 等了超過 20 分鐘, 才噴這個訊息出來, 這種 instance name check 應該放在最前面檢查吧....!@#$

2021年10月4日 星期一

Terraform - the only supported value for workload pool is ...

昨天 terraform 能正常跑完, 今天換了個 project 卻噴出這個訊息:

Error: googleapi: Error 400: Currently, the only supported value for workload pool is "foobar.svc.id.goog"., badRequest

terraform 這塊的設定檔也沒改, 是怎麼噴掉的?

打開 TF_LOG 開始追 API query 看了老半天, 覺得沒有問題啊, 再仔細看看... 嗯?

"workloadIdentityConfig": {
  "identityNamespace": " foobar.svc.id.goog"
}

怎麼 foobar 前面有個疑似空白的東西, 回去翻 terraform 設定.

這邊是抓 google_project.data.name 來用, 再切到 GCP portal IAM 看看...

Permissions for project " foobar"

project name 還真的前面有個空白存在... <囧>

後來在 console 的 IAM -> Settings 把 Project name 前面的空白拿掉, 存檔, 再跑一次 terraform 就正常了.

2021年9月22日 星期三

Terraform - will be read during apply ?

有時候會在 terraform plan / apply 的時候看到這種恐怖的情況...

  # module.foobar.data.google_service_account.this will be read during apply
  # (config refers to values not yet known)
  ...
  # module.foobar.google_pubsub_subscription.this must be replaced
  ...
  # module.foobar.google_pubsub_topic.this must be replaced
  ...
  # module.foobar.google_storage_bucket.this will be destroyed
  ...
  # module.foobar.google_storage_bucket_iam_member.bucket will be destroyed
  ...
  # module.foobar.google_storage_bucket_iam_member.object will be destroyed
  ...
  # module.foobar.google_pubsub_subscription_iam_member.this will be updated in-place
  ...

在同一個層級(module.foobar), 只要有一個 data object 需要被重讀(will be read during apply)更新內容的話, 後面同級的 resource 幾乎都會被當作受到影響, 而被 terraform 進行取代(replaced)或是砍掉(destroyed)的處置, 要是這些 resource 是 pubsub topic / cloud storage bucket 這類會存資料的地方, 那就會掉資料甚至全滅.

如果直接 terraform apply -target='module.foobar' 執行更新下去, 那通常會很慘, 因為 terraform apply 列出來這些就是這次安排執行的處置, 就算 module.foobar.data.google_service_account.this 執行重讀之後內容還是一樣, 後續的取代或砍掉的處置照樣會執行, 造成誤砍誤殺的結果.

正確的手動解法, 是執行 terraform apply -target='module.foobar.data.google_service_account.this' 先把 data read 的動作獨自執行完成, 若是 data read 回來的內容不變, 那再執行 terraform plan / apply 就不會出現後續的取代或砍掉的處置. 若是內容有變, 顯示出來的可能就只是 object 裡面部分更新(will be updated in-place)的處置, 影響太大的才會被取代或是砍掉.

至於為什麼會出現需要重讀的情況, 大都是因為線上環境的設定跟 terraform 的設定不一致, 或是 gke 被更新版本導致 resource 被異動, 或者可能是 data.google_service_account.this 資料過期了, 所以需要先更新資料.

2021年6月21日 星期一

Terraform - GCP IAM apply / destroy race condition

這是一個用 terraform 處理 GCP project / pubsub subscription / pubsub topic / storage bucket / etc 的 IAM role 常會遇到的 race condition, 目前高達八成確定原因是在 GCP 的相關 API 不是 single action, 而是 atomic action, 而且設後不理沒確認是否執行完成就直接 return.

目前只能用 workaround 解法: 再執行/多執行幾次 terraform 指令. (原因後敘)

假設狀況如下: (真實狀況可能不是/不只這樣)
- bucket foobar 原本就有 role: roles/storage.legacyBucketReader
- 現在要改成 role: roles/storage.legacyBucketOwner
- 執行 terraform apply (無關的部分就省略了):
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: -/+ destroy and then create replacement Terraform will perform the following actions: # module.basement.google_storage_bucket_iam_member.bucket must be replaced -/+ resource "google_storage_bucket_iam_member" "bucket" { ~ role = "roles/storage.legacyBucketReader" -> "roles/storage.legacyBucketOwner" # forces replacement } Plan: 1 to add, 0 to change, 1 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:
表示這個 bucket 的 iam role 會被先拆後建. - 輸入 yes 再按 enter 下去之後, 噴錯誤訊息出來說無法設定之類的.... (省略) - 通常只要再執行一次 terraform apply 再 yes 下去之後就可以正常執行完. - 還是噴一樣錯誤訊息的話, 那就再等一下再執行一次... - The End.

有人會問, 是不是可以用 time_sleep 的寫法讓 destroy 先執行完再 apply ?

首先, 這是一個 resource 被 replace (destroy -> apply) 的動作, 並不是分開的 resource 運作, 所以不適用上面這種方式來處理.

其次, 在 terraform 裡面這應該是個呼叫 GCP API 進行 remove 之後再 add 的行為, 中間沒有也不應該有 delay 動作影響執行效率(這也可能產生別的 race condition), 問題點是在 remove 跟 add 大概是 atomic action, 沒有全部確定執行完就 return 回來, 產生後續的 race condition 問題.

最後, 利用 time_sleep 那個寫法實在是累贅也有問題, 因為使用者通常不會知道 create 要花多久時間(create_duration), 也不會知道 destroy_duration 多久, 只能用預估或是猜的來設定. 若是 GCP 不忙的時候可能 1s 就全部跑完, 卻還要等完剩下的 29s, 或是 1m 才跑完, 設定 30s 照樣還是發生 race condition.

倒不如還是人工 delay 再人工執行同樣的 terraform 指令還比較簡單實用.