火山引擎服务实践:控制台与 Pulumi 两种方式

需求与整体说明

一个新项目要在火山引擎上开一整套基础服务:PostgreSQL、Redis、MongoDB、Elasticsearch、Kafka、对象存储、负载均衡和 Kubernetes 集群,外加访问控制(IP 白名单)、备份策略和一批团队账号。

同一件事有两种做法,本文都记录下来:

  • 控制台手动创建 —— 打开网页照着向导点,参数有默认值和联动校验,适合第一次摸一个产品。
  • Pulumi 代码化 —— 把资源写成 Python 代码,pulumi up 创建、pulumi destroy 回收,适合要复制多套环境或者批量操作的场景。

两条路径的取舍放在文末对比。

方式一 控制台手动创建

白名单设置

各个服务都要设防火墙,只放通白名单。默认开通公网访问,需要单独申请公网 IP。

IP 白名单内容:

1
2
3
4
5
6
7
8
# lan
127.0.0.0/8,10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16

# wan-office
# <PUBLIC_IP>

# wan-server
<PUBLIC_IP>,<PUBLIC_IP>,<PUBLIC_IP>

备份策略

各服务的备份按数据重要程度分档,能重建的一律不备份:

服务 保留时长 全量 增量
PostgreSQL 30 天 每周一三五七 每 2 小时
MongoDB 30 天 每天
Elasticsearch 不备份
Milvus 不备份
对象存储 不备份

PostgreSQL

pg

创建完成后到「实例 - 账号管理」新增一个高权限账号。

Redis

redis

MongoDB

mongodb
mongodb

只给第一个节点地址申请公网地址即可。

Elasticsearch

es

给 Kibana 开通公网访问即可,ES 本身走 http。

Kafka

kafka

不需要开通公网访问。记得打开自动创建 Topic 和 Group。

对象存储

对象存储

在「权限管理 - 存储桶授权策略管理」里配置目录级别的匿名只读,下面是两套环境的策略。

cms 环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"Statement": [
{
"Sid": "所有用户-cms/目录-只读",
"Effect": "Allow",
"Principal": [
"*"
],
"Action": "tos:Get*",
"Resource": "trn:tos:::yyyy/zzzz-*/cms/*"
},
{
"Sid": "所有用户-config/目录-只读",
"Effect": "Allow",
"Principal": [
"*"
],
"Action": "tos:Get*",
"Resource": "trn:tos:::yyy/config/*"
}
]
}

prod 环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"Statement": [
{
"Sid": "所有用户-aigc/目录-只读",
"Effect": "Allow",
"Principal": [
"*"
],
"Action": "tos:Get*",
"Resource": "trn:tos:::xxx/aigc/*"
},
{
"Sid": "所有用户-tts/目录-只读",
"Effect": "Allow",
"Principal": [
"*"
],
"Action": "tos:Get*",
"Resource": "trn:tos:::xxx/tts/*"
},
{
"Sid": "所有用户-ugc/目录-只读",
"Effect": "Allow",
"Principal": [
"*"
],
"Action": "tos:Get*",
"Resource": "trn:tos:::xxx/ugc/*"
}
]
}

向量检索服务 Milvus

向量检索这块用的是阿里云向量检索服务 Milvus,和火山上的其他服务混着用:

阿里云向量检索服务Milvus

负载均衡

负载均衡

Kubernetes

先建集群,再建节点池,然后才能发布服务:

创建k8s集群1
创建k8s集群2
创建节点池3
创建节点池4
创建服务5
服务详细6
资源确认7

集群建好后还有一些附加配置要过一遍:

k8s额外操作1
k8s额外操作2
k8s额外操作3
k8s额外操作4

方式二 Pulumi 代码化

Pulumi 是一个开源的基础设施即代码(IaC)工具,用真正的编程语言而不是 DSL 来描述和管理云资源。相比上面一屏屏点,代码能进 Git、能 review、能重复执行。

安装方式见 https://www.pulumi.com/docs/iac/download-install/,火山引擎 provider 的资源清单见 https://www.pulumi.com/registry/packages/volcengine

安装与初始化

新建项目目录,选 Python 模板,然后把 provider 和凭据配好。--secret 会把 key 加密后存进 stack 配置,不会明文落盘:

1
2
3
4
5
6
7
8
mkdir volcengine && cd volcengine
pulumi new python
source venv/bin/activate
pip install pulumi-volcengine

pulumi config set volcengine:accessKey <your_secret_id> --secret
pulumi config set volcengine:secretKey <your_secret_key> --secret
pulumi config set volcengine:region cn-beijing # 或 cn-shanghai

PostgreSQL 实例

创建实例

对应控制台里那一屏 PostgreSQL 表单,规格、可用区、存储、参数都在代码里写清楚:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import pulumi
import pulumi_volcengine as volcengine

foo_instance = volcengine.rds_postgresql.Instance("fooInstance",
db_engine_version="PostgreSQL_14",
node_spec="rds.postgres.1c2g",
primary_zone_id='cn-shanghai-a',
secondary_zone_id='cn-shanghai-a',
storage_space=40,
subnet_id='subnet-xxxx',
instance_name="test-1",
charge_info=volcengine.rds_postgresql.InstanceChargeInfoArgs(
charge_type="PostPaid",
),
project_name="default",
tags=[volcengine.rds_postgresql.InstanceTagArgs(
key="tfk1",
value="tfv1",
)],
parameters=[
volcengine.rds_postgresql.InstanceParameterArgs(
name="auto_explain.log_analyze",
value="off",
),
volcengine.rds_postgresql.InstanceParameterArgs(
name="auto_explain.log_format",
value="text",
),
])

pulumi.export('foo_instance', foo_instance) # 输出实例信息

执行创建:

1
pulumi up

查询已有实例

已经存在的实例可以用 get 导入进来查看,注意这里用的是 volcengine.rds.Instance 而不是创建时的 rds_postgresql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pulumi
import pulumi_volcengine as volcengine

# 实例名称与 ID
instance_name = "test-1"
instance_id = "xxxx" # 上文创建后返回的 ID

# 获取已存在的 PostgreSQL 实例
existing_instance = volcengine.rds.Instance.get(instance_name, instance_id)

# 导出实例的详细信息
pulumi.export("instance_name", existing_instance.instance_name)
pulumi.export("instance_id", existing_instance.id)
pulumi.export("instance_type", existing_instance.instance_type)
pulumi.export("db_engine_version", existing_instance.db_engine_version)
pulumi.export("all", existing_instance)

同样执行:

1
pulumi up

返回的信息比控制台详情页还全,连接地址、节点分布、计费方式、参数一次性都在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
- postgres_instance_id: {
- backup_use : 0
- charge_details : [
- [0]: {
- auto_renew : false
- charge_end_time : ""
- charge_start_time : "2024-01-21T07:52:50.000Z"
- charge_status : "Normal"
- charge_type : "PostPaid"
- overdue_reclaim_time : ""
- overdue_time : ""
- period : 0
- period_unit : "Month"
- temp_modify_end_time : ""
- temp_modify_start_time: ""
}
]
- charge_info : {
- auto_renew : false
- charge_type: "PostPaid"
- period : 0
- period_unit: "Month"
}
- create_time : "2024-01-21T07:52:50.000Z"
- data_sync_mode : "Async"
- db_engine_version: "PostgreSQL_14"
- endpoints : [
- [0]: {
- addresses : [
- [0]: {
- dns_visibility: false
- domain : "postgres143ad36e6258.rds-pg.ivolces.com"
- eip_id : ""
- ip_address : ""
- network_type : "Private"
- port : "5432"
- subnet_id : "subnet-xxxx"
}
]
- auto_add_new_nodes : "Enable"
- description : ""
- enable_read_only : "Disable"
- enable_read_write_splitting: "Disable"
- endpoint_id : "postgres-143ad36e6258-cluster"
- endpoint_name : "默认终端"
- endpoint_type : "Cluster"
- read_only_node_weights : [
- [0]: {
- node_id : ""
- node_type: "Primary"
- weight : 300
}
]
- read_write_mode : "ReadWrite"
}
]
- id : "postgres-143ad36e6258"
- instance_id : "postgres-143ad36e6258"
- instance_name : "test-1"
- instance_status : "Running"
- instance_type : "HA"
- memory : 2
- node_number : 2
- node_spec : "rds.postgres.1c2g"
- nodes : [
- [0]: {
- create_time: "2024-01-21T07:52:50.000Z"
- instance_id: "postgres-143ad36e6258"
- memory : 2
- node_id : "postgres-143ad36e6258-cxhr"
- node_spec : "rds.postgres.1c2g"
- node_status: "Running"
- node_type : "Secondary"
- region_id : "cn-shanghai"
- update_time: ""
- v_cpu : 0
- zone_id : "cn-shanghai-a"
}
- [1]: {
- create_time: "2024-01-21T07:52:50.000Z"
- instance_id: "postgres-143ad36e6258"
- memory : 2
- node_id : "postgres-143ad36e6258"
- node_spec : "rds.postgres.1c2g"
- node_status: "Running"
- node_type : "Primary"
- region_id : "cn-shanghai"
- update_time: ""
- v_cpu : 0
- zone_id : "cn-shanghai-a"
}
]
- parameters : [
- [0]: {
- name : "auto_explain.log_format"
- value: "text"
}
- [1]: {
- name : "auto_explain.log_analyze"
- value: "off"
}
]
- primary_zone_id : "cn-shanghai-a"
- project_name : "default"
- region_id : "cn-shanghai"
- secondary_zone_id: "cn-shanghai-a"
- storage_space : 40
- storage_type : "LocalSSD"
- subnet_id : "subnet-xxxx"
- tags : [
- [0]: {
- key : "tfk1"
- value: "tfv1"
}
]
- update_time : "2024-01-21T07:55:52.000Z"
- urn : "urn:pulumi:dev::volc-postgres::volcengine:rds_postgresql/instance:Instance::fooInstance"
- v_cpu : <null>
- vpc_id : "vpc-xxxx"
- zone_id : "cn-shanghai-a"
- zone_ids : [
- [0]: "cn-shanghai-a"
]
}

销毁实例

测试环境用完一条命令回收,这是控制台方式很难做到的:

1
pulumi destroy

IAM 用户与用户组

这是代码化收益最明显的场景。八个人分属产品、开发、运维三个团队,有人跨团队,有人只要控制台密码,有人只要访问密钥。手工点要开八次用户表单加十几次授权,写成循环则是一遍过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""火山引擎 IAM 用户和用户组管理模块

本模块用于管理火山引擎云平台的 IAM 用户和用户组:
- 为产品团队、开发团队和运维团队创建用户组
- 创建用户并将其分配到相应的用户组
- 设置安全的登录配置和密码策略
- 为不同用户组分配适当的访问权限策略
- 为每个用户创建访问密钥

"""

import pulumi
import pulumi_volcengine as volcengine
import os

# 用户信息配置
# 包含每个用户的团队归属、显示名称、用户名和认证方式
# teams 字段是一个列表,支持用户同时属于多个团队
# auth_type 可以是:"password"(仅密码登录)、"access_key"(仅访问密钥)、"both"(两者都要)或 "none"(都不要)
# password 字段是可选的,如果不设置则使用默认密码
USER_CONFIG = [
{"teams": ["product"], "display_name": "产品经理-张三", "user_name": "prod1", "auth_type": "password"},
{"teams": ["development"], "display_name": "开发工程师-李四", "user_name": "dev1", "auth_type": "both", "password": "<CHANGE_ME>"},
{"teams": ["development"], "display_name": "开发工程师-王五", "user_name": "dev2", "auth_type": "both", "password": "<CHANGE_ME>"},
{"teams": ["development", "ops"], "display_name": "开发工程师-赵六", "user_name": "dev3", "auth_type": "both"},
{"teams": ["development"], "display_name": "开发工程师-孙七", "user_name": "dev4", "auth_type": "both"},
{"teams": ["product", "development"], "display_name": "产品经理-周八", "user_name": "prod2", "auth_type": "none"},
{"teams": ["ops"], "display_name": "运维工程师-吴九", "user_name": "ops1", "auth_type": "access_key"},
{"teams": ["ops", "development"], "display_name": "运维工程师-郑十", "user_name": "ops2", "auth_type": "access_key"}
]

# 用户组配置
# 定义各团队的用户组信息,包括描述、显示名称和用户组名称
TEAM_GROUPS = {
"product": {
"description": "产品团队用户组 - 用于管理产品团队成员的权限",
"display_name": "产品团队",
"user_group_name": "product-group"
},
"development": {
"description": "开发团队用户组 - 用于管理开发团队成员的权限",
"display_name": "开发团队",
"user_group_name": "dev-group"
},
"ops": {
"description": "运维团队用户组 - 用于管理运维团队成员的权限",
"display_name": "运维团队",
"user_group_name": "ops-group"
}
}

# 创建密钥存储目录
SECRET_DIR = "./secrets"
os.makedirs(SECRET_DIR, exist_ok=True)

# 创建用户组
# 为每个团队创建对应的 IAM 用户组
team_user_groups = {}
for team_name, group_config in TEAM_GROUPS.items():
team_user_groups[team_name] = volcengine.iam.UserGroup(
group_config["user_group_name"],
description=group_config["description"],
display_name=group_config["display_name"],
user_group_name=group_config["user_group_name"]
)

# 创建用户并配置用户组关联
iam_users = []
for user_info in USER_CONFIG:
# 创建 IAM 用户
iam_user = volcengine.iam.User(
user_info["user_name"],
display_name=user_info["display_name"],
user_name=user_info["user_name"]
)
iam_users.append(iam_user)

# 根据认证方式配置用户登录信息和访问密钥
if user_info["auth_type"] in ["password", "both"]:
# 配置用户登录信息和密码策略
# 使用用户配置的密码或默认密码
password = user_info.get("password", "<CHANGE_ME>")
user_login_profile = volcengine.iam.LoginProfile(
f"{user_info['user_name']}-login",
user_name=iam_user.user_name,
password=password, # 使用配置的密码或默认密码
login_allowed=True, # 允许登录
password_reset_required=False # 是否强制首次登录修改密码
)

if user_info["auth_type"] in ["access_key", "both"]:
# 创建用户访问密钥,密钥内容落到 ./secrets 目录
access_key = volcengine.iam.AccessKey(
f"{user_info['user_name']}-ak",
user_name=iam_user.user_name,
secret_file=os.path.join(SECRET_DIR, f"{user_info['user_name']}.sk"),
status="active"
)

# 将用户关联到所有所属的团队用户组
for team_name in user_info["teams"]:
user_group_attachment = volcengine.iam.UserGroupAttachment(
f"{user_info['user_name']}-{team_name}-group",
user_group_name=team_user_groups[team_name].user_group_name,
user_name=iam_user.user_name
)

# 配置用户组权限策略
# 为不同团队的用户组分配适当的系统权限
team_policies = []

# 产品团队权限配置
# - 只读访问权限
# - 工单系统完全访问权限
product_team_policies = ["ReadOnlyAccess", "TicketFullAccess"]
for policy_name in product_team_policies:
policy = volcengine.iam.UserGroupPolicyAttachment(
f"product-{policy_name.lower()}-policy",
user_group_name=team_user_groups["product"].user_group_name,
policy_name=policy_name,
policy_type="System"
)
team_policies.append(policy)

# 开发团队权限配置
# - 只读访问权限
# - 容器服务完全访问权限
# - 工单系统完全访问权限
dev_team_policies = ["ReadOnlyAccess", "VKEInnerFullAccess", "TicketFullAccess"]
for policy_name in dev_team_policies:
policy = volcengine.iam.UserGroupPolicyAttachment(
f"development-{policy_name.lower()}-policy",
user_group_name=team_user_groups["development"].user_group_name,
policy_name=policy_name,
policy_type="System"
)
team_policies.append(policy)

# 运维团队权限配置
# - 管理员完全访问权限
ops_policy = volcengine.iam.UserGroupPolicyAttachment(
"ops-administrator-policy",
user_group_name=team_user_groups["ops"].user_group_name,
policy_name="AdministratorAccess",
policy_type="System"
)
team_policies.append(ops_policy)

# 导出资源信息供参考
# 导出创建的用户组、用户和已应用的策略信息
pulumi.export('user_groups', {name: group.user_group_name for name, group in team_user_groups.items()})
pulumi.export('users', [user.user_name for user in iam_users])
pulumi.export('applied_policies', [policy.policy_name for policy in team_policies])

人员变动时改 USER_CONFIGpulumi up,增删和授权变更都由 Pulumi 算差异,不用自己记上次点到哪一步。

两种方式对比与取舍

维度 控制台 Pulumi
上手成本 打开网页就能用,参数有向导和默认值 要装 CLI、建项目、配 provider 凭据
可复现性 靠截图和记忆,换个环境重建容易漏参数 代码进 Git,pulumi up 结果一致
变更记录 只能翻操作日志 diff 即变更说明,可 review
批量操作 用户、授权这类重复动作纯手工,易错 循环生成,八个用户和三组授权一遍过
回收测试资源 一个个找一个个删 pulumi destroy 一次清干净
参数发现 表单会列出所有可选项和联动限制 得翻 provider 文档,字段名和控制台不完全对应
凭据管理 登录态即权限 需要长期 accessKey/secretKey,靠 --secret 加密存进 stack 配置

实际怎么分:

  • 一次性、参数需要探索的,用控制台。 比如 VPC 和子网规划、备份策略、告警规则。这类东西建一次基本不动,而且控制台表单会明确告诉你哪些选项互斥、哪些可用区有货 —— Pulumi 里的 subnet_idprimary_zone_id 这些值本来也得先从控制台确认。
  • 要复制多套或者会反复调整的,用 Pulumi。 数据库实例、K8s 节点池这类 dev/staging/prod 各来一份的资源,代码化以后环境差异就是一个 stack 配置的区别。
  • 批量且规则化的,一定用 Pulumi。 IAM 是最典型的例子,人员和权限矩阵写在代码里,谁在哪个组、有没有 access key 一目了然,比在控制台里逐个核对可靠得多。
  • provider 没覆盖到的,退回控制台。 火山引擎 provider 的资源覆盖度和文档不如控制台完整,本文的 Pulumi 示例也只做到 RDS 和 IAM;查询已有实例甚至要从 rds_postgresql 换成 rds 命名空间。遇到这类情况别硬扛,控制台点完再考虑要不要 import 进 Pulumi 管理。