-
Notifications
You must be signed in to change notification settings - Fork 1
feat(mysql): 创建 MySQL 主从复制集群和单节点实例的 Terraform 配置文件 #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5b807e4
929d01c
6adfdc5
9f9d3e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| .terraform/ | ||
| *.tfstate | ||
| *.tfstate.lock.info | ||
| *.tfstate.backup | ||
| crash.log | ||
| custom-plugins/ | ||
| *.lock.hcl | ||
| *.auto.* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| data "qiniu_compute_images" "available_official_images" { | ||
| type = "Official" | ||
| state = "Available" | ||
| } | ||
|
|
||
| locals { | ||
| ubuntu_image_id = [ | ||
| for item in data.qiniu_compute_images.available_official_images.items : item | ||
| if item.os_distribution == "Ubuntu" && item.os_version == "24.04 LTS" | ||
| ][0].id | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Unsafe Array Access Directly accessing Recommendation: Add error handling: ubuntu_images = [
for item in data.qiniu_compute_images.available_official_images.items : item
if item.os_distribution == "Ubuntu" && item.os_version == "24.04 LTS"
]
ubuntu_image_id = length(local.ubuntu_images) > 0 ? local.ubuntu_images[0].id : (
# Trigger a clear error message
tobool("ERROR: Ubuntu 24.04 LTS image not found")
) |
||
|
|
||
| replication_username = "replicator" | ||
|
Comment on lines
+9
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| instance_final_state = "Running" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # MySQL Replication Cluster Configuration | ||
|
|
||
| # 用于生成资源后缀 | ||
| resource "random_string" "random_suffix" { | ||
| length = 6 | ||
| upper = false | ||
| lower = true | ||
| special = false | ||
| } | ||
|
|
||
| locals { | ||
| replication_cluster_suffix = random_string.random_suffix.result | ||
| } | ||
|
|
||
| # 创建置放组 | ||
| resource "qiniu_compute_placement_group" "mysql_pg" { | ||
| name = format("mysql-repl-%s", local.replication_cluster_suffix) | ||
| description = format("Placement group for MySQL replication cluster %s", local.replication_cluster_suffix) | ||
| strategy = "Spread" | ||
| } | ||
|
|
||
| # 创建 MySQL 主库 | ||
| resource "qiniu_compute_instance" "mysql_primary_node" { | ||
| instance_type = var.instance_type | ||
| placement_group_id = qiniu_compute_placement_group.mysql_pg.id | ||
| name = format("mysql-primary-%s", local.replication_cluster_suffix) | ||
| description = format("Primary node for MySQL replication cluster %s", local.replication_cluster_suffix) | ||
| state = local.instance_final_state | ||
| image_id = local.ubuntu_image_id | ||
| system_disk_size = var.instance_system_disk_size | ||
|
|
||
| user_data = base64encode(templatefile("${path.module}/mysql_master.sh", { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical Security: Passwords Exposed in Terraform State Passing passwords directly in
Recommendation: Use a secrets management solution (AWS Secrets Manager, HashiCorp Vault, etc.) and retrieve credentials at runtime, or generate random passwords within the instance and store them securely. References: CWE-312, CWE-532 |
||
| mysql_server_id = "1" | ||
| mysql_admin_username = var.mysql_username | ||
| mysql_admin_password = var.mysql_password | ||
| mysql_replication_username = local.replication_username | ||
| mysql_replication_password = var.mysql_password | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| mysql_db_name = var.mysql_db_name | ||
| })) | ||
| } | ||
|
|
||
|
|
||
| # 创建 MySQL 从库节点 | ||
| resource "qiniu_compute_instance" "mysql_replication_nodes" { | ||
| depends_on = [qiniu_compute_instance.mysql_primary_node] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Performance: Unnecessary Serialization The Since Recommendation: Remove |
||
|
|
||
| count = var.mysql_replica_count | ||
| instance_type = var.instance_type | ||
| placement_group_id = qiniu_compute_placement_group.mysql_pg.id | ||
| name = format("mysql-repl-%02d-%s", count.index + 1, local.replication_cluster_suffix) | ||
| description = format("Replica node %02d for MySQL replication cluster %s", count.index + 1, local.replication_cluster_suffix) | ||
| state = local.instance_final_state | ||
| image_id = local.ubuntu_image_id | ||
| system_disk_size = var.instance_system_disk_size | ||
|
|
||
| user_data = base64encode(templatefile("${path.module}/mysql_slave.sh", { | ||
| mysql_master_ip = qiniu_compute_instance.mysql_primary_node.private_ip_addresses[0].ipv4 | ||
| mysql_server_id = tostring(count.index + 2) // 从库ID从2开始递增 | ||
| mysql_replication_username = local.replication_username | ||
| mysql_replication_password = var.mysql_password | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| })) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| #!/bin/bash | ||
| set -e | ||
|
|
||
| echo "This is the primary node." | ||
|
|
||
| # 允许外部IP访问 | ||
| sed -i 's/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical Security: MySQL Exposed Without Firewall Rules Binding to Impact: MySQL instances accessible from entire network, vulnerable to brute force attacks and exploitation. Recommendation: Add security group/firewall resources in Terraform to restrict port 3306 access to specific IP ranges or VPC subnets only. |
||
|
|
||
| # 确保删除旧的server uuid配置文件,防止uuid冲突 | ||
| rm -f /var/lib/mysql/auto.cnf | ||
|
|
||
| # 配置主从复制 | ||
| tee /etc/mysql/mysql.conf.d/replication.cnf >/dev/null <<EOF | ||
| [mysqld] | ||
| server_id = ${mysql_server_id} | ||
| log_bin = /var/log/mysql/mysql-bin.log # binlog 路径前缀 | ||
| binlog_format = ROW # binlog 格式 | ||
| gtid_mode = ON # 开启 GTID 模式 | ||
| expire_logs_days = 7 # 自动清理7天前的binlog | ||
| max_binlog_size = 100M # 单个binlog文件最大大小 | ||
| enforce_gtid_consistency = ON # 强制保证 GTID 一致性(避免非事务操作) | ||
| EOF | ||
|
|
||
| # 重启 MySQL 服务 | ||
| systemctl restart mysql | ||
|
|
||
| # 等待 MySQL 服务重启完成 | ||
| while ! mysqladmin ping --silent; do sleep 1; done | ||
|
|
||
| mysql -uroot <<EOF | ||
| ALTER USER 'root'@'localhost' IDENTIFIED BY '${mysql_admin_password}'; | ||
| CREATE USER '${mysql_admin_username}'@'%' IDENTIFIED BY '${mysql_admin_password}';为管理用户 '${mysql_admin_username}' 授权时使用了 '%' 作为主机,这意味着该用户可以从任何 IP 地址连接。如果实例有公网 IP,这将带来安全风险。建议将主机限制在特定的 IP 范围或 VPC 子网内,以增强安全性。 | ||
|
|
||
|
|
||
| GRANT ALL PRIVILEGES ON *.* TO '${mysql_admin_username}'@'%' WITH GRANT OPTION; | ||
zhangzqs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| CREATE USER '${mysql_replication_username}'@'%' IDENTIFIED WITH mysql_native_password BY '${mysql_replication_password}'; | ||
| GRANT REPLICATION SLAVE ON *.* TO '${mysql_replication_username}'@'%'; | ||
| FLUSH PRIVILEGES; | ||
| EOF | ||
|
|
||
| # 如果 mysql_db_name 不为空,则创建数据库 | ||
| if [[ -n "${mysql_db_name}" ]]; then | ||
| mysql -u"${mysql_admin_username}" -p"${mysql_admin_password}" <<EOF | ||
| CREATE DATABASE IF NOT EXISTS \`${mysql_db_name}\`; | ||
| EOF | ||
| fi | ||
|
|
||
| # 查看数据库 | ||
| mysql -u"${mysql_admin_username}" -p"${mysql_admin_password}" -e "SHOW DATABASES;" | ||
zhangzqs marked this conversation as resolved.
Show resolved
Hide resolved
zhangzqs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||
| #!/bin/bash | ||||||||||||||||||||||
| set -e | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| echo "This is a replica node." | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # 允许外部IP访问 | ||||||||||||||||||||||
| sed -i 's/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # 确保删除旧的server uuid配置文件,防止uuid冲突 | ||||||||||||||||||||||
| rm -f /var/lib/mysql/auto.cnf | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # 配置主从复制 | ||||||||||||||||||||||
| tee /etc/mysql/mysql.conf.d/replication.cnf >/dev/null <<EOF | ||||||||||||||||||||||
| [mysqld] | ||||||||||||||||||||||
| server_id = ${mysql_server_id} | ||||||||||||||||||||||
| relay_log = /var/lib/mysql/mysql-relay-bin # 中继日志路径 | ||||||||||||||||||||||
| read_only = ON # 设置从库为只读 | ||||||||||||||||||||||
| super_read_only = ON # 设置root用户也是只读模式 | ||||||||||||||||||||||
| gtid_mode = ON # 开启 GTID 模式 | ||||||||||||||||||||||
| enforce_gtid_consistency = ON # 强制保证 GTID 一致性(避免非事务操作) | ||||||||||||||||||||||
| EOF | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # 重启 MySQL 服务 | ||||||||||||||||||||||
| systemctl restart mysql | ||||||||||||||||||||||
| sleep 1 # 等待 MySQL 服务重启完成 | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # 轮询等待主库启动 | ||||||||||||||||||||||
| while ! mysqladmin ping -h "${mysql_master_ip}" -u"${mysql_replication_username}" -p"${mysql_replication_password}" --silent; do | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. High Priority: Unbounded Polling Loop This while loop has no timeout, which could cause infinite waiting if the master fails to start. Recommendation: Add timeout and exponential backoff: MAX_RETRIES=60
RETRY_COUNT=0
SLEEP_TIME=2
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
if mysqladmin ping -h "${mysql_master_ip}" -u"${mysql_replication_username}" -p"${mysql_replication_password}" --silent; then
echo "MySQL master is ready!"
break
fi
echo "Waiting for MySQL master... (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)"
sleep $SLEEP_TIME
RETRY_COUNT=$((RETRY_COUNT + 1))
SLEEP_TIME=$((SLEEP_TIME > 10 ? 10 : SLEEP_TIME + 1))
done
if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
echo "ERROR: MySQL master failed to become ready"
exit 1
fi |
||||||||||||||||||||||
| echo "Waiting for MySQL master at ${mysql_master_ip} to be ready..." | ||||||||||||||||||||||
| sleep 2 | ||||||||||||||||||||||
| done | ||||||||||||||||||||||
|
Comment on lines
+28
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 在命令行中通过
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # 配置从库,将自动将主库所有变更都同步过来,包括用户配置 | ||||||||||||||||||||||
| mysql -uroot <<EOF | ||||||||||||||||||||||
| CHANGE MASTER TO | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deprecated MySQL Syntax MySQL 8.0.23+ deprecated Recommendation: Update to modern syntax: CHANGE REPLICATION SOURCE TO
SOURCE_HOST = '${mysql_master_ip}',
SOURCE_USER = '${mysql_replication_username}',
SOURCE_PASSWORD = '${mysql_replication_password}',
SOURCE_AUTO_POSITION = 1;
START REPLICA;
SHOW REPLICA STATUS\G; |
||||||||||||||||||||||
| MASTER_HOST = '${mysql_master_ip}', | ||||||||||||||||||||||
| MASTER_USER = '${mysql_replication_username}', | ||||||||||||||||||||||
| MASTER_PASSWORD = '${mysql_replication_password}', | ||||||||||||||||||||||
| MASTER_AUTO_POSITION = 1; | ||||||||||||||||||||||
| START SLAVE; | ||||||||||||||||||||||
| SHOW SLAVE STATUS\G; | ||||||||||||||||||||||
| EOF | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| output "mysql_primary_endpoint" { | ||
| value = qiniu_compute_instance.mysql_primary_node.private_ip_addresses[0].ipv4 | ||
| description = "MySQL primary node private IP address" | ||
| } | ||
|
|
||
| output "mysql_replica_endpoints" { | ||
| value = [for instance in qiniu_compute_instance.mysql_replication_nodes : instance.private_ip_addresses[0].ipv4] | ||
| description = "MySQL replica nodes private IP addresses" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| variable "instance_type" { | ||
| type = string | ||
| description = "MySQL instance type" | ||
| validation { | ||
| condition = var.instance_type != "" | ||
| error_message = "instance_type parameter is required but not provided" | ||
| } | ||
| } | ||
|
|
||
| variable "instance_system_disk_size" { | ||
| type = number | ||
| description = "System disk size in GiB" | ||
| default = 20 | ||
|
|
||
| validation { | ||
| condition = var.instance_system_disk_size > 0 | ||
| error_message = "instance_system_disk_size parameter must be a positive integer" | ||
| } | ||
| } | ||
|
|
||
| variable "mysql_replica_count" { | ||
| type = number | ||
| description = "Number of MySQL replica nodes" | ||
| default = 2 | ||
|
|
||
| validation { | ||
| condition = var.mysql_replica_count >= 1 && var.mysql_replica_count <= 10 | ||
| error_message = "mysql_replica_count must be between 1 and 10" | ||
| } | ||
| } | ||
|
|
||
| variable "mysql_username" { | ||
| type = string | ||
| description = "MySQL admin username" | ||
|
|
||
| validation { | ||
| condition = length(var.mysql_username) >= 1 && length(var.mysql_username) <= 32 | ||
| error_message = "mysql_username parameter must be between 1 and 32 characters long" | ||
| } | ||
| } | ||
|
|
||
| variable "mysql_password" { | ||
| type = string | ||
| description = "MySQL admin password" | ||
| sensitive = true | ||
|
|
||
| validation { | ||
| condition = length(var.mysql_password) >= 8 | ||
| error_message = "mysql_password parameter must be at least 8 characters long" | ||
| } | ||
|
|
||
| validation { | ||
| condition = can(regex("[a-z]", var.mysql_password)) && can(regex("[A-Z]", var.mysql_password)) && can(regex("[0-9]", var.mysql_password)) && can(regex("[!-/:-@\\[-`{-~]", var.mysql_password)) | ||
| error_message = "mysql_password parameter must contain at least one lowercase letter, one uppercase letter, one digit, and one special character" | ||
| } | ||
| } | ||
|
|
||
| variable "mysql_db_name" { | ||
| type = string | ||
| description = "Initial MySQL database name (optional)" | ||
| default = "" | ||
|
|
||
| validation { | ||
| condition = var.mysql_db_name == "" ? true : ( | ||
| length(var.mysql_db_name) >= 1 && | ||
| length(var.mysql_db_name) <= 64 && | ||
| can(regex("^[a-zA-Z0-9_]*$", var.mysql_db_name)) && | ||
| !contains(["mysql", "information_schema", "performance_schema", "sys"], var.mysql_db_name) | ||
| ) | ||
| error_message = "mysql_db_name must be 1-64 chars, only alphanumeric/underscore, and not a reserved name (mysql, information_schema, performance_schema, sys)" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,3 @@ | ||||||||||||||||||||||||||||||||||
| provider "qiniu" {} | ||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing Provider Version Constraints No version constraints specified for providers, which can lead to unpredictable behavior across environments. Recommendation:
Suggested change
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| provider "random" {} | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| data "qiniu_compute_images" "available_official_images" { | ||
| type = "Official" | ||
| state = "Available" | ||
| } | ||
|
|
||
| locals { | ||
| ubuntu_image_id = [ | ||
| for item in data.qiniu_compute_images.available_official_images.items : item | ||
| if item.os_distribution == "Ubuntu" && item.os_version == "24.04 LTS" | ||
| ][0].id | ||
|
Comment on lines
+7
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 当前查找 Ubuntu 镜像 ID 的方式比较脆弱。如果 |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # 生成资源后缀,避免命名冲突 | ||
| resource "random_string" "resource_suffix" { | ||
| length = 6 | ||
| upper = false | ||
| lower = true | ||
| special = false | ||
| } | ||
|
|
||
| locals { | ||
| standalone_suffix = random_string.resource_suffix.result | ||
| } | ||
|
|
||
| resource "qiniu_compute_instance" "mysql_primary_node" { | ||
| instance_type = var.instance_type // 虚拟机实例规格 | ||
| name = format("mysql-standalone-%s", local.standalone_suffix) | ||
| description = format("Standalone MySQL node %s", local.standalone_suffix) | ||
| image_id = local.ubuntu_image_id // 预设的MysSQL系统镜像ID | ||
| system_disk_size = var.instance_system_disk_size // 系统盘大小,单位是GiB | ||
| user_data = base64encode(templatefile("${path.module}/mysql_standalone.sh", { | ||
| mysql_username = var.mysql_username, | ||
| mysql_password = var.mysql_password, | ||
| mysql_db_name = var.mysql_db_name, | ||
| })) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| #!/bin/bash | ||
| set -e | ||
|
|
||
| # Install MySQL if not already installed | ||
|
|
||
| echo "Checking for MySQL installation..." | ||
|
|
||
| if ! command -v mysql &> /dev/null; then | ||
| echo "MySQL not found, installing..." | ||
| apt-get update | ||
| DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-client-8.0 mysql-server-8.0 mysql-router mysql-shell | ||
| fi | ||
|
|
||
| echo "Setting up MySQL standalone instance..." | ||
|
|
||
| # 允许外部IP访问 | ||
| sed -i 's/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| # 确保删除旧的server uuid配置文件,防止uuid冲突 | ||
| rm -f /var/lib/mysql/auto.cnf | ||
|
|
||
| # 重启 MySQL 服务 | ||
| systemctl restart mysql | ||
|
|
||
| # 等待 MySQL 服务重启完成 | ||
| while ! mysqladmin ping --silent; do sleep 1; done | ||
|
|
||
| # 配置基础用户 | ||
| mysql -uroot <<EOF | ||
| ALTER USER 'root'@'localhost' IDENTIFIED BY '${mysql_password}'; | ||
| CREATE USER '${mysql_username}'@'%' IDENTIFIED BY '${mysql_password}'; | ||
| GRANT ALL PRIVILEGES ON *.* TO '${mysql_username}'@'%' WITH GRANT OPTION; | ||
| FLUSH PRIVILEGES; | ||
| EOF | ||
|
|
||
| # 如果 mysql_db_name 不为空,则创建数据库 | ||
| if [[ -n "${mysql_db_name}" ]]; then | ||
| mysql -u"${mysql_username}" -p"${mysql_password}" <<EOF | ||
| CREATE DATABASE IF NOT EXISTS \`${mysql_db_name}\`; | ||
| EOF | ||
| fi | ||
|
|
||
| echo "MySQL standalone setup completed successfully!" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| output "mysql_primary_endpoint" { | ||
| value = qiniu_compute_instance.mysql_primary_node.private_ip_addresses[0].ipv4 | ||
| description = "MySQL primary node private IP address" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
当前查找 Ubuntu 镜像 ID 的方式比较脆弱。如果
data.qiniu_compute_images.available_official_images.items没有返回任何匹配 "Ubuntu" 和 "24.04 LTS" 的镜像,[0].id的访问会直接导致 Terraform 执行失败。建议使用one()函数来确保只找到一个匹配的镜像,如果找不到或找到多个,它会返回一个更明确的错误信息。