Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
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.*
14 changes: 14 additions & 0 deletions mysql/replication/data.tf
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
Comment on lines +7 to +10

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

当前查找 Ubuntu 镜像 ID 的方式比较脆弱。如果 data.qiniu_compute_images.available_official_images.items 没有返回任何匹配 "Ubuntu" 和 "24.04 LTS" 的镜像,[0].id 的访问会直接导致 Terraform 执行失败。建议使用 one() 函数来确保只找到一个匹配的镜像,如果找不到或找到多个,它会返回一个更明确的错误信息。

  ubuntu_image_id = one([for item in data.qiniu_compute_images.available_official_images.items : item if item.os_distribution == "Ubuntu" && item.os_version == "24.04 LTS"]).id

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Unsafe Array Access

Directly accessing [0] without checking if the array is empty will cause Terraform to fail if no Ubuntu 24.04 LTS image is found.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

镜像版本 "24.04 LTS" 和复制用户名 "replicator" 被硬编码了。为了提高模块的灵活性和可维护性,建议将它们定义为变量,并提供合理的默认值。

instance_final_state = "Running"
}
64 changes: 64 additions & 0 deletions mysql/replication/main.tf
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", {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Security: Passwords Exposed in Terraform State

Passing passwords directly in user_data causes multiple security vulnerabilities:

  • Passwords stored in plaintext in Terraform state files
  • Exposed in cloud provider logs and instance metadata
  • Visible in process listings during execution

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

将管理员密码 var.mysql_password 同时用作复制用户的密码存在严重安全风险。如果复制用户的密码泄露,攻击者可能会尝试用它来登录管理账户。建议为复制用户创建一个独立的密码。您可以通过一个新的变量(例如 var.mysql_replication_password)来传递,或者使用 random_password 资源生成一个随机密码。

    mysql_replication_password = var.mysql_replication_password

mysql_db_name = var.mysql_db_name
}))
}


# 创建 MySQL 从库节点
resource "qiniu_compute_instance" "mysql_replication_nodes" {
depends_on = [qiniu_compute_instance.mysql_primary_node]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这里的 depends_on 是多余的。因为在第57行 user_data 中已经通过 qiniu_compute_instance.mysql_primary_node.private_ip_addresses[0].ipv4 引用了主节点,Terraform 会自动推断出依赖关系。显式声明 depends_on 会使代码变得冗余,建议移除。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: Unnecessary Serialization

The depends_on forces all replica nodes to wait for complete master provisioning (including user_data execution), which blocks parallelization.

Since mysql_slave.sh already has a polling loop to wait for the master, this dependency can be removed to allow parallel instance provisioning, significantly reducing deployment time.

Recommendation: Remove depends_on to enable ~30-60% faster deployments.


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

将管理员密码 var.mysql_password 同时用作复制用户的密码存在严重安全风险。如果复制用户的密码泄露,攻击者可能会尝试用它来登录管理账户。建议为复制用户创建一个独立的密码。您可以通过一个新的变量(例如 var.mysql_replication_password)来传递,或者使用 random_password 资源生成一个随机密码。

    mysql_replication_password = var.mysql_replication_password

}))
}


50 changes: 50 additions & 0 deletions mysql/replication/mysql_master.sh
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Security: MySQL Exposed Without Firewall Rules

Binding to 0.0.0.0 exposes MySQL on all network interfaces, but the Terraform configuration doesn't include any security groups or firewall rules to restrict access.

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;

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;"
42 changes: 42 additions & 0 deletions mysql/replication/mysql_slave.sh
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 服务重启完成

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

使用 sleep 1 来等待 MySQL 重启完成是不可靠的。在负载较高或启动较慢的机器上,1秒可能不足以让 MySQL 完全准备好,导致后续命令失败。建议使用一个循环来检查 MySQL 服务的状态,例如通过 mysqladmin ping,直到服务可用为止。

Suggested change
sleep 1 # 等待 MySQL 服务重启完成
while ! mysqladmin ping --silent; do sleep 1; done


# 轮询等待主库启动
while ! mysqladmin ping -h "${mysql_master_ip}" -u"${mysql_replication_username}" -p"${mysql_replication_password}" --silent; do
Copy link

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在命令行中通过 -p 传递密码会将密码暴露在进程列表中,可能被系统上的其他用户看到,存在安全风险。建议使用 MYSQL_PWD 环境变量来传递密码,这样更安全。

Suggested change
while ! mysqladmin ping -h "${mysql_master_ip}" -u"${mysql_replication_username}" -p"${mysql_replication_password}" --silent; do
echo "Waiting for MySQL master at ${mysql_master_ip} to be ready..."
sleep 2
done
export MYSQL_PWD="${mysql_replication_password}"
while ! mysqladmin ping -h "${mysql_master_ip}" -u"${mysql_replication_username}" --silent; do
echo "Waiting for MySQL master at ${mysql_master_ip} to be ready..."
sleep 2
done
unset MYSQL_PWD


# 配置从库,将自动将主库所有变更都同步过来,包括用户配置
mysql -uroot <<EOF
CHANGE MASTER TO
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deprecated MySQL Syntax

MySQL 8.0.23+ deprecated MASTER/SLAVE terminology in favor of SOURCE/REPLICA.

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
9 changes: 9 additions & 0 deletions mysql/replication/outputs.tf
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"
}
72 changes: 72 additions & 0 deletions mysql/replication/variables.tf
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)"
}
}
3 changes: 3 additions & 0 deletions mysql/replication/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
provider "qiniu" {}
Copy link

Choose a reason for hiding this comment

The 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 "qiniu" {}
terraform {
required_providers {
qiniu = {
source = "qiniu/qiniu"
version = "~> 1.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
required_version = ">= 1.0"
}
provider "qiniu" {}


provider "random" {}
11 changes: 11 additions & 0 deletions mysql/standalone/data.tf
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

当前查找 Ubuntu 镜像 ID 的方式比较脆弱。如果 data.qiniu_compute_images.available_official_images.items 没有返回任何匹配 "Ubuntu" 和 "24.04 LTS" 的镜像,[0].id 的访问会直接导致 Terraform 执行失败。建议使用 one() 函数来确保只找到一个匹配的镜像,如果找不到或找到多个,它会返回一个更明确的错误信息。

  ubuntu_image_id = one([for item in data.qiniu_compute_images.available_official_images.items : item if item.os_distribution == "Ubuntu" && item.os_version == "24.04 LTS"]).id

}
24 changes: 24 additions & 0 deletions mysql/standalone/main.tf
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,
}))
}
43 changes: 43 additions & 0 deletions mysql/standalone/mysql_standalone.sh
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

对于单机版 MySQL,将 bind-address 设置为 0.0.0.0 会使数据库服务暴露在所有网络接口上。如果实例有公网 IP,这将是一个严重的安全风险,允许任何人尝试连接数据库。建议默认绑定到 127.0.0.1,仅在需要远程访问时通过变量进行配置。


# 确保删除旧的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!"
4 changes: 4 additions & 0 deletions mysql/standalone/outputs.tf
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"
}
Loading