Skip to content

Commit 92430bb

Browse files
authoredNov 12, 2025
feat(mysql): 创建 MySQL 主从复制集群和单节点实例的 Terraform 配置文件(#1
feat(mysql): 创建 MySQL 主从复制集群和单节点实例的 Terraform 配置文件
1 parent c3a62b7 commit 92430bb

20 files changed

+426
-0
lines changed
 

‎.gitignore‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.terraform/
2+
*.tfstate
3+
*.tfstate.lock.info
4+
*.tfstate.backup
5+
crash.log
6+
custom-plugins/
7+
*.lock.hcl
8+
*.auto.*
9+
env.sh

‎mysql/common/common_variables.tf‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
variable "instance_type" {
2+
type = string
3+
description = "MySQL instance type"
4+
default = "ecs.t1.c1m2"
5+
validation {
6+
condition = var.instance_type != "" && contains([
7+
"ecs.t1.c1m2",
8+
"ecs.t1.c2m4",
9+
"ecs.t1.c4m8",
10+
"ecs.t1.c12m24",
11+
"ecs.t1.c32m64",
12+
"ecs.t1.c24m48",
13+
"ecs.t1.c8m16",
14+
"ecs.t1.c16m32",
15+
"ecs.g1.c16m120",
16+
"ecs.g1.c32m240",
17+
"ecs.c1.c1m2",
18+
"ecs.c1.c2m4",
19+
"ecs.c1.c4m8",
20+
"ecs.c1.c8m16",
21+
"ecs.c1.c16m32",
22+
"ecs.c1.c24m48",
23+
"ecs.c1.c12m24",
24+
"ecs.c1.c32m64",
25+
], var.instance_type)
26+
error_message = "instance_type parameter must be one of the allowed instance types"
27+
}
28+
}
29+
30+
variable "instance_system_disk_size" {
31+
type = number
32+
description = "System disk size in GiB"
33+
default = 20
34+
35+
validation {
36+
condition = var.instance_system_disk_size > 0
37+
error_message = "instance_system_disk_size parameter must be a positive integer"
38+
}
39+
}
40+
41+
variable "mysql_username" {
42+
type = string
43+
description = "MySQL username"
44+
default = "admin"
45+
46+
validation {
47+
condition = length(var.mysql_username) >= 1 && length(var.mysql_username) <= 32
48+
error_message = "mysql_username parameter must be between 1 and 32 characters long"
49+
}
50+
}
51+
52+
variable "mysql_password" {
53+
type = string
54+
description = "MySQL password"
55+
sensitive = true
56+
57+
validation {
58+
condition = length(var.mysql_password) >= 8
59+
error_message = "mysql_password parameter must be at least 8 characters long"
60+
}
61+
62+
validation {
63+
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))
64+
error_message = "mysql_password parameter must contain at least one lowercase letter, one uppercase letter, one digit, and one special character"
65+
}
66+
}
67+
68+
variable "mysql_db_name" {
69+
type = string
70+
description = "Initial MySQL database name (optional)"
71+
default = ""
72+
73+
validation {
74+
condition = var.mysql_db_name == "" || 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)
75+
error_message = "mysql_db_name must be 1-64 chars, only alphanumeric/underscore, and not a reserved name (mysql, information_schema, performance_schema, sys)"
76+
}
77+
}

‎mysql/common/common_versions.tf‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
terraform {
2+
required_version = "> 0.12.0"
3+
4+
required_providers {
5+
qiniu = {
6+
source = "hashicorp/qiniu"
7+
version = "~> 1.0.0"
8+
}
9+
random = {
10+
source = "hashicorp/random"
11+
version = "~> 3.0"
12+
}
13+
}
14+
}
15+
16+
provider "qiniu" {}
17+
18+
provider "random" {}

‎mysql/common/image_data.tf‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
data "qiniu_compute_images" "available_official_images" {
2+
type = "Official"
3+
state = "Available"
4+
}
5+
6+
locals {
7+
// 选用的系统镜像ID
8+
ubuntu_image_id = one([
9+
for item in data.qiniu_compute_images.available_official_images.items : item
10+
if item.os_distribution == "Ubuntu" && item.os_version == "24.04 LTS"
11+
]).id
12+
}

‎mysql/replication/data.tf‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# 为 MySQL 复制用户生成随机密码
2+
resource "random_password" "replication_password" {
3+
length = 16
4+
special = true
5+
lower = true
6+
upper = true
7+
numeric = true
8+
}
9+
10+
locals {
11+
// MySQL 复制用户名称
12+
replication_username = var.mysql_replication_username
13+
14+
// 随机生成的 MySQL 复制用户密码
15+
replication_password = random_password.replication_password.result
16+
}
17+
18+
# 用于生成资源后缀
19+
resource "random_string" "random_suffix" {
20+
length = 6
21+
upper = false
22+
lower = true
23+
special = false
24+
}
25+
26+
locals {
27+
// 资源组随机后缀
28+
cluster_suffix = random_string.random_suffix.result
29+
}

‎mysql/replication/image_data.tf‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../common/image_data.tf

‎mysql/replication/main.tf‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# MySQL Replication Cluster Configuration
2+
3+
# 创建置放组
4+
resource "qiniu_compute_placement_group" "mysql_pg" {
5+
name = format("mysql-repl-%s", local.cluster_suffix)
6+
description = format("Placement group for MySQL replication cluster %s", local.cluster_suffix)
7+
strategy = "Spread"
8+
}
9+
10+
# 创建 MySQL 主库
11+
resource "qiniu_compute_instance" "mysql_primary_node" {
12+
instance_type = var.instance_type
13+
placement_group_id = qiniu_compute_placement_group.mysql_pg.id
14+
name = format("mysql-primary-%s", local.cluster_suffix)
15+
description = format("Primary node for MySQL replication cluster %s", local.cluster_suffix)
16+
image_id = local.ubuntu_image_id
17+
system_disk_size = var.instance_system_disk_size
18+
19+
user_data = base64encode(templatefile("${path.module}/mysql_master.sh", {
20+
mysql_server_id = "1"
21+
mysql_admin_username = var.mysql_username
22+
mysql_admin_password = var.mysql_password
23+
mysql_replication_username = local.replication_username
24+
mysql_replication_password = local.replication_password
25+
mysql_db_name = var.mysql_db_name
26+
}))
27+
}
28+
29+
30+
# 创建 MySQL 从库节点
31+
resource "qiniu_compute_instance" "mysql_replication_nodes" {
32+
depends_on = [qiniu_compute_instance.mysql_primary_node]
33+
34+
count = var.mysql_replica_count
35+
instance_type = var.instance_type
36+
placement_group_id = qiniu_compute_placement_group.mysql_pg.id
37+
name = format("mysql-repl-%02d-%s", count.index + 1, local.cluster_suffix)
38+
description = format("Replica node %02d for MySQL replication cluster %s", count.index + 1, local.cluster_suffix)
39+
image_id = local.ubuntu_image_id
40+
system_disk_size = var.instance_system_disk_size
41+
42+
user_data = base64encode(templatefile("${path.module}/mysql_slave.sh", {
43+
mysql_master_ip = qiniu_compute_instance.mysql_primary_node.private_ip_addresses[0].ipv4
44+
mysql_server_id = tostring(count.index + 2) // 从库ID从2开始递增
45+
mysql_replication_username = local.replication_username
46+
mysql_replication_password = local.replication_password
47+
}))
48+
}
49+
50+

‎mysql/replication/mysql_master.sh‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Install MySQL if not already installed
5+
echo "Checking for MySQL installation..."
6+
if ! command -v mysql &> /dev/null; then
7+
echo "MySQL not found, installing..."
8+
apt-get update
9+
DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-client-8.0 mysql-server-8.0 mysql-router mysql-shell
10+
fi
11+
12+
echo "This is the primary node."
13+
14+
# 允许外部IP访问
15+
sed -i 's/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf
16+
17+
# 确保删除旧的server uuid配置文件,防止uuid冲突
18+
rm -f /var/lib/mysql/auto.cnf
19+
20+
# 配置主从复制
21+
tee /etc/mysql/mysql.conf.d/replication.cnf >/dev/null <<EOF
22+
[mysqld]
23+
server_id = ${mysql_server_id}
24+
log_bin = /var/log/mysql/mysql-bin.log # binlog 路径前缀
25+
binlog_format = ROW # binlog 格式
26+
gtid_mode = ON # 开启 GTID 模式
27+
expire_logs_days = 7 # 自动清理7天前的binlog
28+
max_binlog_size = 100M # 单个binlog文件最大大小
29+
enforce_gtid_consistency = ON # 强制保证 GTID 一致性(避免非事务操作)
30+
EOF
31+
32+
# 重启 MySQL 服务
33+
systemctl restart mysql
34+
35+
# 等待 MySQL 服务重启完成
36+
while ! mysqladmin ping --silent; do sleep 1; done
37+
38+
mysql -uroot <<EOF
39+
ALTER USER 'root'@'localhost' IDENTIFIED BY '${mysql_admin_password}';
40+
CREATE USER IF NOT EXISTS '${mysql_admin_username}'@'%' IDENTIFIED BY '${mysql_admin_password}';
41+
ALTER USER '${mysql_admin_username}'@'%' IDENTIFIED BY '${mysql_admin_password}';
42+
GRANT ALL PRIVILEGES ON *.* TO '${mysql_admin_username}'@'%' WITH GRANT OPTION;
43+
44+
CREATE USER IF NOT EXISTS '${mysql_replication_username}'@'%' IDENTIFIED WITH mysql_native_password BY '${mysql_replication_password}';
45+
ALTER USER '${mysql_replication_username}'@'%' IDENTIFIED WITH mysql_native_password BY '${mysql_replication_password}';
46+
GRANT REPLICATION SLAVE ON *.* TO '${mysql_replication_username}'@'%';
47+
FLUSH PRIVILEGES;
48+
EOF
49+
50+
# 如果 mysql_db_name 不为空,则创建数据库
51+
if [[ -n "${mysql_db_name}" ]]; then
52+
mysql -u"${mysql_admin_username}" -p"${mysql_admin_password}" <<EOF
53+
CREATE DATABASE IF NOT EXISTS \`${mysql_db_name}\`;
54+
EOF
55+
fi
56+
57+
# 查看数据库
58+
mysql -u"${mysql_admin_username}" -p"${mysql_admin_password}" -e "SHOW DATABASES;"

‎mysql/replication/mysql_slave.sh‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Install MySQL if not already installed
5+
echo "Checking for MySQL installation..."
6+
if ! command -v mysql &> /dev/null; then
7+
echo "MySQL not found, installing..."
8+
apt-get update
9+
DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-client-8.0 mysql-server-8.0 mysql-router mysql-shell
10+
fi
11+
12+
echo "This is a replica node."
13+
14+
# 允许外部IP访问
15+
sed -i 's/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf
16+
17+
# 确保删除旧的server uuid配置文件,防止uuid冲突
18+
rm -f /var/lib/mysql/auto.cnf
19+
20+
# 配置主从复制
21+
tee /etc/mysql/mysql.conf.d/replication.cnf >/dev/null <<EOF
22+
[mysqld]
23+
server_id = ${mysql_server_id}
24+
relay_log = /var/lib/mysql/mysql-relay-bin # 中继日志路径
25+
read_only = ON # 设置从库为只读
26+
super_read_only = ON # 设置root用户也是只读模式
27+
gtid_mode = ON # 开启 GTID 模式
28+
enforce_gtid_consistency = ON # 强制保证 GTID 一致性(避免非事务操作)
29+
EOF
30+
31+
# 重启 MySQL 服务
32+
systemctl restart mysql
33+
34+
# 等待 MySQL 服务重启完成
35+
while ! mysqladmin ping --silent; do sleep 1; done
36+
37+
# 轮询等待主库启动
38+
while ! mysqladmin ping -h "${mysql_master_ip}" -u"${mysql_replication_username}" -p"${mysql_replication_password}" --silent; do
39+
echo "Waiting for MySQL master at ${mysql_master_ip} to be ready..."
40+
sleep 2
41+
done
42+
43+
# 配置从库,将自动将主库所有变更都同步过来,包括用户配置
44+
mysql -uroot <<EOF
45+
CHANGE MASTER TO
46+
MASTER_HOST = '${mysql_master_ip}',
47+
MASTER_USER = '${mysql_replication_username}',
48+
MASTER_PASSWORD = '${mysql_replication_password}',
49+
MASTER_AUTO_POSITION = 1;
50+
START SLAVE;
51+
SHOW SLAVE STATUS\G;
52+
EOF

‎mysql/replication/outputs.tf‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
output "mysql_primary_endpoint" {
2+
value = format("%s:3306", qiniu_compute_instance.mysql_primary_node.private_ip_addresses[0].ipv4)
3+
description = "MySQL primary address string in the format: <primary_ip>:<port>"
4+
}
5+
6+
output "mysql_replica_endpoints" {
7+
value = [
8+
for instance in qiniu_compute_instance.mysql_replication_nodes :
9+
format("%s:3306", instance.private_ip_addresses[0].ipv4)
10+
]
11+
description = "List of MySQL replica endpoints in the format: <replica_ip>:<port>"
12+
}
13+
14+
output "mysql_replication_username" {
15+
value = local.replication_username
16+
description = "MySQL replication username"
17+
}
18+
19+
output "mysql_replication_password" {
20+
value = local.replication_password
21+
description = "MySQL replication password (randomly generated)"
22+
sensitive = true
23+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
variable "mysql_replica_count" {
3+
type = number
4+
description = "Number of MySQL replica nodes"
5+
default = 2
6+
7+
validation {
8+
condition = var.mysql_replica_count >= 1 && var.mysql_replica_count <= 2
9+
error_message = "mysql_replica_count must be between 1 and 2"
10+
}
11+
}
12+
13+
variable "mysql_replication_username" {
14+
type = string
15+
description = "MySQL replication username"
16+
default = "replication"
17+
18+
validation {
19+
condition = length(var.mysql_replication_username) >= 1 && length(var.mysql_replication_username) <= 32
20+
error_message = "mysql_replication_username parameter must be between 1 and 32 characters long"
21+
}
22+
}

‎mysql/replication/variables.tf‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../common/common_variables.tf

‎mysql/replication/versions.tf‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../common/common_versions.tf

‎mysql/standalone/data.tf‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# 生成资源后缀,避免命名冲突
2+
resource "random_string" "resource_suffix" {
3+
length = 6
4+
upper = false
5+
lower = true
6+
special = false
7+
}
8+
9+
locals {
10+
standalone_suffix = random_string.resource_suffix.result
11+
}

‎mysql/standalone/image_data.tf‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../common/image_data.tf

‎mysql/standalone/main.tf‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
resource "qiniu_compute_instance" "mysql_primary_node" {
2+
instance_type = var.instance_type // 虚拟机实例规格
3+
name = format("mysql-standalone-%s", local.standalone_suffix)
4+
description = format("Standalone MySQL node %s", local.standalone_suffix)
5+
image_id = local.ubuntu_image_id // 预设的MysSQL系统镜像ID
6+
system_disk_size = var.instance_system_disk_size // 系统盘大小,单位是GiB
7+
user_data = base64encode(templatefile("${path.module}/mysql_standalone.sh", {
8+
mysql_username = var.mysql_username,
9+
mysql_password = var.mysql_password,
10+
mysql_db_name = var.mysql_db_name,
11+
}))
12+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Install MySQL if not already installed
5+
6+
echo "Checking for MySQL installation..."
7+
8+
if ! command -v mysql &> /dev/null; then
9+
echo "MySQL not found, installing..."
10+
apt-get update
11+
DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-client-8.0 mysql-server-8.0 mysql-router mysql-shell
12+
fi
13+
14+
echo "Setting up MySQL standalone instance..."
15+
16+
# 允许外部IP访问
17+
sed -i 's/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf
18+
19+
# 确保删除旧的server uuid配置文件,防止uuid冲突
20+
rm -f /var/lib/mysql/auto.cnf
21+
22+
# 重启 MySQL 服务
23+
systemctl restart mysql
24+
25+
# 等待 MySQL 服务重启完成
26+
while ! mysqladmin ping --silent; do sleep 1; done
27+
28+
# 配置基础用户
29+
mysql -uroot <<EOF
30+
ALTER USER 'root'@'localhost' IDENTIFIED BY '${mysql_password}';
31+
CREATE USER '${mysql_username}'@'%' IDENTIFIED BY '${mysql_password}';
32+
GRANT ALL PRIVILEGES ON *.* TO '${mysql_username}'@'%' WITH GRANT OPTION;
33+
FLUSH PRIVILEGES;
34+
EOF
35+
36+
# 如果 mysql_db_name 不为空,则创建数据库
37+
if [[ -n "${mysql_db_name}" ]]; then
38+
mysql -u"${mysql_username}" -p"${mysql_password}" <<EOF
39+
CREATE DATABASE IF NOT EXISTS \`${mysql_db_name}\`;
40+
EOF
41+
fi
42+
43+
echo "MySQL standalone setup completed successfully!"

‎mysql/standalone/outputs.tf‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
output "mysql_primary_endpoint" {
2+
value = format("%s:3306", qiniu_compute_instance.mysql_primary_node.private_ip_addresses[0].ipv4)
3+
description = "MySQL primary address string in the format: <primary_ip>:<port>"
4+
}

‎mysql/standalone/variables.tf‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../common/common_variables.tf

‎mysql/standalone/versions.tf‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../common/common_versions.tf

0 commit comments

Comments
 (0)
Please sign in to comment.