Manage Configuration#

Note

这个项目的配置管理功能由 config_patterns Python 库提供. 建议你先阅读该项目的文档, 有一个大该的了解既可.

Declare Configuration Schema#

每个 Environment 下会有多个 Server (environment 和 server 的概念请参考 这篇文档). 一个 Server 的详细配置是在 acore_server_config/config/define/server.py 模块中被定义的. Server 类的实例代表了一个 Server 的配置数据. 下面是该模块的源码:

acore_server_config/config/define/server.py
  1# -*- coding: utf-8 -*-
  2
  3"""
  4todo: doc string
  5"""
  6
  7import typing as T
  8import dataclasses
  9
 10from acore_constants.api import ServerLifeCycle
 11
 12
 13@dataclasses.dataclass
 14class Server:
 15    """
 16    Per Game Server configuration.
 17
 18    :param id: Server id, the naming convention is ``${env_name}-${server_name}``.
 19    :param ec2_ami_id: the AMI id for the game server.
 20    :param ec2_instance_type: the EC2 instance type for the game server.
 21    :param ec2_subnet_id: the EC2 subnet id for the game server.
 22    :param ec2_key_name: the EC2 ssh key name for the game server.
 23    :param ec2_eip_allocation_id: if you need a static IP, then create
 24        an Elastic IP address and put the allocation id here. otherwise,
 25        use the automatic public IP address.
 26    :param acore_soap_app_version: the acore_soap_app-project git tag for bootstrap.
 27    :param acore_server_bootstrap_version: the acore_server_bootstrap-project
 28        git tag for bootstrap.
 29    :param db_snapshot_id: the snapshot id to create the RDS DB instance.
 30    :param db_instance_class: the RDS instance class for the game database.
 31    :param db_engine_version: the RDS engine version (all the way to minor).
 32    :param db_admin_username: the RDS admin username, usually this is admin.
 33    :param db_admin_password: the RDS admin password, we need this password.
 34        to create the database user for game server.
 35    :param db_username: the database user for game server.
 36    :param db_password: the database password for game server.
 37    :param lifecycle: the logic "game server (both EC2 and RDS)" lifecycle definition.
 38    :param authserver_conf: custom config for authserver.conf.
 39    :param worldserver_conf: custom config for worldserver.conf.
 40    :param mod_lua_engine_conf: custom config for mod_LuaEngine.conf.
 41    """
 42
 43    id: T.Optional[str] = dataclasses.field(default=None)
 44    # EC2 related
 45    ec2_ami_id: T.Optional[str] = dataclasses.field(default=None)
 46    ec2_instance_type: T.Optional[str] = dataclasses.field(default=None)
 47    ec2_subnet_id: T.Optional[str] = dataclasses.field(default=None)
 48    ec2_key_name: T.Optional[str] = dataclasses.field(default=None)
 49    ec2_eip_allocation_id: T.Optional[str] = dataclasses.field(default=None)
 50    acore_soap_app_version: T.Optional[str] = dataclasses.field(default=None)
 51    acore_db_app_version: T.Optional[str] = dataclasses.field(default=None)
 52    acore_server_bootstrap_version: T.Optional[str] = dataclasses.field(default=None)
 53    # RDS related
 54    db_snapshot_id: T.Optional[str] = dataclasses.field(default=None)
 55    db_instance_class: T.Optional[str] = dataclasses.field(default=None)
 56    db_engine_version: T.Optional[str] = dataclasses.field(default=None)
 57    db_admin_username: T.Optional[str] = dataclasses.field(default=None)
 58    db_admin_password: T.Optional[str] = dataclasses.field(default=None)
 59    db_username: T.Optional[str] = dataclasses.field(default=None)
 60    db_password: T.Optional[str] = dataclasses.field(default=None)
 61    # EC2 and RDS related
 62    lifecycle: T.Optional[str] = dataclasses.field(default=None)
 63    # authserver.conf, worldserver.conf, ...
 64    authserver_conf: T.Dict[str, str] = dataclasses.field(default_factory=dict)
 65    worldserver_conf: T.Dict[str, str] = dataclasses.field(default_factory=dict)
 66    mod_lua_engine_conf: T.Dict[str, str] = dataclasses.field(default_factory=dict)
 67
 68    def __post_init__(self):
 69        if self.lifecycle not in [
 70            ServerLifeCycle.running,
 71            ServerLifeCycle.smart_running,
 72            ServerLifeCycle.stopped,
 73            ServerLifeCycle.deleted,
 74        ]:  # pragma: no cover
 75            raise ValueError(f"{self.lifecycle!r} is not a valid lifecycle definition!")
 76
 77    def is_ready_for_create_new_server(self) -> bool:
 78        """
 79        Check if the configuration is sufficient for creating new server.
 80
 81        See: https://acore-server.readthedocs.io/en/latest/search.html?q=Operation+and+Workflow&check_keywords=yes&area=default#
 82        """
 83        not_none_fields = [
 84            "id",
 85            "ec2_ami_id",
 86            "ec2_instance_type",
 87            "ec2_subnet_id",
 88            "ec2_key_name",
 89            "acore_soap_app_version",
 90            "acore_db_app_version",
 91            "acore_server_bootstrap_version",
 92            "db_instance_class",
 93            "db_engine_version",
 94            "db_admin_username",
 95            "db_admin_password",
 96            "db_username",
 97            "db_password",
 98        ]
 99        for field in not_none_fields:
100            if getattr(self, field) is None:
101                return False
102        return True
103
104    def is_ready_for_create_cloned_server(self) -> bool:
105        """
106        Check if the configuration is sufficient for creating cloned server.
107
108        See: https://acore-server.readthedocs.io/en/latest/search.html?q=Operation+and+Workflow&check_keywords=yes&area=default#
109        """
110        not_none_fields = [
111            "id",
112            "ec2_instance_type",
113            "ec2_subnet_id",
114            "ec2_key_name",
115            "acore_soap_app_version",
116            "acore_db_app_version",
117            "acore_server_bootstrap_version",
118            "db_instance_class",
119            # "db_engine_version", # clone 的时候不需要指定 engine version, 会自动继承
120            # "db_admin_username", # clone 的时候不需要指定 admin username, 会自动继承
121            "db_admin_password",
122            "db_username",
123            "db_password",
124        ]
125        for field in not_none_fields:
126            if getattr(self, field) is None:
127                return False
128        return True
129
130    def is_ready_for_create_updated_server(self) -> bool:
131        """
132        Check if the configuration is sufficient for creating updated server.
133
134        See: https://acore-server.readthedocs.io/en/latest/search.html?q=Operation+and+Workflow&check_keywords=yes&area=default#
135        """
136        not_none_fields = [
137            "id",
138            "ec2_ami_id",
139            "ec2_instance_type",
140            "ec2_subnet_id",
141            "ec2_key_name",
142            "acore_soap_app_version",
143            "acore_db_app_version",
144            "acore_server_bootstrap_version",
145            "db_instance_class",
146            # "db_engine_version", # update server 的时候不需要指定 engine version, 因为我们会用已经存在的数据库
147            # "db_admin_username", # update server 的时候不需要指定 admin username, 因为我们会用已经存在的数据库
148            "db_admin_password",
149            "db_username",
150            "db_password",
151        ]
152        for field in not_none_fields:
153            if getattr(self, field) is None:
154                return False
155        return True
156
157    def is_ready_for_stop_server(self) -> bool:
158        """
159        Check if the configuration is sufficient for stopping server.
160
161        See: https://acore-server.readthedocs.io/en/latest/search.html?q=Operation+and+Workflow&check_keywords=yes&area=default#
162        """
163        not_none_fields = [
164            "id",
165        ]
166        for field in not_none_fields:
167            if getattr(self, field) is None:
168                return False
169        return True
170
171    def is_ready_for_start_server(self) -> bool:
172        """
173        Check if the configuration is sufficient for starting server.
174
175        See: https://acore-server.readthedocs.io/en/latest/search.html?q=Operation+and+Workflow&check_keywords=yes&area=default#
176        """
177        not_none_fields = [
178            "id",
179        ]
180        for field in not_none_fields:
181            if getattr(self, field) is None:
182                return False
183        return True
184
185    def is_ready_for_delete_server(self) -> bool:
186        """
187        Check if the configuration is sufficient for deleting server.
188
189        See: https://acore-server.readthedocs.io/en/latest/search.html?q=Operation+and+Workflow&check_keywords=yes&area=default#
190        """
191        not_none_fields = [
192            "id",
193        ]
194        for field in not_none_fields:
195            if getattr(self, field) is None:
196                return False
197        return True
198
199
200@dataclasses.dataclass
201class ServerMixin:
202    servers: T.Dict[str, Server] = dataclasses.field(default_factory=dict)
203
204    @property
205    def server_blue(self) -> Server:
206        return self.servers["blue"]
207
208    @property
209    def server_green(self) -> Server:
210        return self.servers["green"]
211
212    @property
213    def server_black(self) -> Server:
214        return self.servers["black"]
215
216    @property
217    def server_white(self) -> Server:
218        return self.servers["white"]
219
220    @property
221    def server_yellow(self) -> Server:
222        return self.servers["yello"]
223
224    @property
225    def server_orange(self) -> Server:
226        return self.servers["orange"]

Local Configuration Data#

配置数据的源头是由管理员在本地编写好数据文件然后部署到 AWS S3 中的. 本地的配置文件分两个, 普通配置数据的 config/config.json, 和敏感配置数据的 ${HOME}/.projects/acore_server_config/config-secret.json 文件 (这是一个本地路径). 普通配置数据文件会被 check in 到 Git 中, 而敏感配置数据不会.

而读取 Configuration Data 会有两种情况:

  1. 管理员在本地开发时从本地数据文件中读取数据.

  2. 游戏服务器在 EC2 上运行程序并读取数据.

下面我们分情况来介绍.

1. Load Configuration Data From Local Files#

acore_server_config/config/init.py 模块实现了从本地文件中读取配置数据的功能. 你只要运行 from acore_server_config.config.init import config 既可获得一个 config 对象. 该模块的源码如下:

acore_server_config/config/init.py
 1# -*- coding: utf-8 -*-
 2
 3"""
 4注, 这个模块不属于公开 API 的一部分, 仅仅用于在 acore_server_config 项目本身内部用于部署
 5配置数据. 如果你在其他项目中引用了这个项目, 请使用 acore_server_config.api 模块中的内容.
 6"""
 7
 8import json
 9
10from ..paths import path_config_json, path_config_secret_json
11from ..runtime import IS_LOCAL
12
13from .define import EnvEnum, Env, Config
14
15if IS_LOCAL:
16    # ensure that the config-secret.json file exists
17    # it should be at the ${HOME}/.projects/wserver_infra/config-secret.json
18    # this code block is only used to onboard first time user of this
19    # project template. Once you know about how to handle the config-secret.json file,
20    # you can delete this code block.
21    if not path_config_secret_json.exists():  # pragma: no cover
22        path_config_secret_json.parent.mkdir(parents=True, exist_ok=True)
23        path_config_secret_json.write_text(
24            json.dumps(
25                {
26                    "_shared": {},
27                    EnvEnum.sbx.value: {"password": f"{EnvEnum.sbx.value}.password"},
28                    EnvEnum.tst.value: {"password": f"{EnvEnum.tst.value}.password"},
29                    EnvEnum.prd.value: {"password": f"{EnvEnum.prd.value}.password"},
30                },
31                indent=4,
32            )
33        )
34
35    # read non-sensitive config and sensitive config from local file system
36    config = Config.read(
37        env_class=Env,
38        env_enum_class=EnvEnum,
39        path_config=path_config_json,
40        path_secret_config=path_config_secret_json,
41    )
42else:
43    raise NotImplementedError

2. Load Configuration Data From AWS S3 on EC2#

acore_server_config/config/loader.py 模块实现了从 AWS S3 中读取配置数据的功能. 它有两个关键的类:

  1. ConfigLoader: 让管理员从 AWS S3 上读取配置数据. 管理员在部署了配置数据后可以用这个类从 AWS S3 将其读回来. 该操作常用于 Debug.

  2. Ec2ConfigLoader: 让运行在 EC2 上的脚本自己发现 (自省) 自己是哪个服务器, 然后到对应的 AWS S3 object 中读取属于自己的配置数据. 游戏服务器启动时的自动化脚本就会用到这个类.

下面是该模块的源码:

acore_server_config/config/loader.py
  1# -*- coding: utf-8 -*-
  2
  3"""
  4该模块为使用 ``acore_server_config`` 库的外部项目提供了一套能读取服务器 config 的接口.
  5相比之下 ``acore_server_config.config.init`` 模块是为 ``acore_server_config`` 库
  6内部用来从本地文件系统读取配置数据的, 外部项目不应该使用它.
  7
  8该模块有两个 Public API:
  9
 10- :class:`Ec2ConfigLoader`: 用于在 EC2 上运行脚本, 用 "自省" 的方式获得自己的配置数据.
 11- :class:`ConfigLoader`: 用于在任意其他环境显式的加载配置数据.
 12"""
 13
 14import dataclasses
 15import typing as T
 16
 17from s3pathlib import S3Path
 18from simple_aws_ec2.api import Ec2Instance
 19from acore_constants.api import TagKey
 20
 21from ..boto_ses import bsm as default_bsm
 22
 23from .define import EnvEnum, Env, Config, Server
 24
 25
 26if T.TYPE_CHECKING:  # pragma: no cover
 27    from boto_session_manager import BotoSesManager
 28
 29
 30def _get_default_s3folder_config(bsm: "BotoSesManager") -> str:
 31    """
 32    获得默认的 S3 配置数据的根目录.
 33    """
 34    return (
 35        S3Path(f"s3://{bsm.aws_account_id}-{bsm.aws_region}-artifacts")
 36        .joinpath(
 37            "projects",
 38            "acore_server_config",
 39            "config",
 40        )
 41        .to_dir()
 42    ).uri
 43
 44
 45def _get_default_parameter_name_prefix() -> str:
 46    """
 47    获得默认的 AWS Parameter Store 的参数名前缀. 这取决于我们 deploy 的时候用的前缀是什么.
 48    """
 49    return "acore_server_config"
 50
 51
 52def get_this_server_id(bsm: "BotoSesManager") -> str:  # pragma: no cover
 53    """
 54    在 EC2 上通过 "自省", 获得这个服务器的 server_id. 它的 naming convention 是
 55    ``${env_name}-${server_name}``.
 56    """
 57    ec2_inst = Ec2Instance.from_ec2_inside(bsm.ec2_client)
 58    server_id = ec2_inst.tags[TagKey.SERVER_ID]
 59    return server_id
 60
 61
 62def parse_server_id(server_id: str) -> T.Tuple[str, str]:
 63    """
 64    解析 server_id, 返回 (env_name, server_name) 的 tuple.
 65    """
 66    env_name, server_name = server_id.split("-", 1)
 67    return env_name, server_name
 68
 69
 70def get_config(
 71    bsm: "BotoSesManager" = default_bsm,
 72    parameter_name_prefix: T.Optional[str] = None,
 73    env_name: T.Optional[str] = None,
 74    s3folder_config: T.Optional[str] = None,
 75) -> Config:
 76    """
 77    获取这个 ``Config`` 对象的数据. 详细的数据结构请参考
 78    :class:`acore_server_config.config.main.Config`.
 79
 80    :param bsm: BotoSesManager 实例.
 81    :param parameter_name_prefix: the parameter name prefix, the full name will
 82        be ${parameter_name_prefix}-${env_name}.
 83    :param env_name: the environment name of the env specific config you want to
 84        load from, stx, tst, prd, etc. If None, then load the master config.
 85    :param s3folder_config: S3 配置数据的根目录, 默认为
 86        s3://aws_account_id}-{aws_region}-artifacts/projects/acore_server_config/config/
 87    """
 88    if parameter_name_prefix is None:
 89        parameter_name_prefix = _get_default_parameter_name_prefix()
 90    if env_name is None:  # pragma: no cover
 91        parameter_name = parameter_name_prefix
 92    else:
 93        parameter_name = f"{parameter_name_prefix}-{env_name}"
 94
 95    if s3folder_config is None:
 96        s3folder_config = _get_default_s3folder_config(bsm=bsm)
 97
 98    config = Config.read(
 99        env_class=Env,
100        env_enum_class=EnvEnum,
101        bsm=bsm,
102        parameter_name=parameter_name,
103        s3folder_config=s3folder_config,
104    )
105
106    return config
107
108
109# [ec2configloader-start]
110@dataclasses.dataclass
111class Ec2ConfigLoader:
112    """
113    用于在 EC2 上运行脚本, 用 "自省" 的方式获得自己的配置数据. 开始时请使用
114    :meth:`Ec2ConfigLoader.load` 方法获得当前 EC2 的配置数据.
115
116    用法:
117
118    .. code-block:: python
119
120        >>> server = Ec2ConfigLoader.load(...)
121        >>> server
122        Server(id='sbx-blue', db_admin_password='sbx*dummy4test', db_username='myuser', db_password='sbx*dummy4test')
123    """
124
125    @classmethod
126    def load(
127        cls,
128        parameter_name_prefix: T.Optional[str] = None,
129        s3folder_config: T.Optional[str] = None,
130        server_id: T.Optional[str] = None,
131        bsm: "BotoSesManager" = default_bsm,
132    ) -> Server:
133        """
134        获得当前 EC2 的配置数据, 返回一个
135        :class:`~acore_server_config.config.define.server.Server` 对象.
136
137        :param parameter_name_prefix: the parameter name prefix, the full name will
138            be ${parameter_name_prefix}-${env_name}.
139        :param s3folder_config: S3 配置数据的根目录, 默认为
140            s3://aws_account_id}-{aws_region}-artifacts/projects/acore_server_config/config/
141        :param server_id: 强制指定 server_id, 跳过 "自省" 阶段. 常用于测试. 这个 server_id
142            的格式为: ${env_name}-${server_name}, 例如: sbx-blue
143        :param bsm: ``boto_session_manager.BotoSesManager`` object, if not provided,
144            then use the current runtime default AWS CLI profile.
145        """
146        if server_id is None:  # pragma: no cover
147            server_id = get_this_server_id(bsm=bsm)
148        env_name, server_name = parse_server_id(server_id=server_id)
149        config = get_config(
150            bsm=bsm,
151            parameter_name_prefix=parameter_name_prefix,
152            env_name=env_name,
153            s3folder_config=s3folder_config,
154        )
155        env = config.get_env(env_name)
156        return env.servers[server_name]
157# [ec2configloader-end]
158
159
160# [configloader-start]
161@dataclasses.dataclass
162class ConfigLoader:
163    """
164    用于在任意其他环境显式的加载配置数据. 开始时请使用 :meth:`ConfigLoader.new` 方法创建
165    一个新的 Loader 对象, 将配置数据加载到内存中. 然后再对特定的 Server 的配置数据进行访问.
166
167    用法:
168
169    .. code-block:: python
170
171        >>> config_loader = ConfigLoader.new(env_name="sbx")
172        >>> for server_name, server in config_loader.iter_servers():
173        ...
174        >>> server = config_loader.get_server(server_name="blue")
175        >>> server
176        Server(id='sbx-blue', db_admin_password='sbx*dummy4test', db_username='myuser', db_password='sbx*dummy4test')
177    """
178
179    _env: Env = dataclasses.field(init=False)  # a cache of the env specific config
180
181    @classmethod
182    def new(
183        cls,
184        env_name: str,
185        parameter_name_prefix: T.Optional[str] = None,
186        s3folder_config: T.Optional[str] = None,
187        bsm: "BotoSesManager" = default_bsm,
188    ) -> "ConfigLoader":
189        """
190        创建一个新的 ConfigLoader 对象,
191
192        :param env_name: the environment name of the env specific config you want to
193            load from, stx, tst, prd, etc. If None, then load the master config.
194        :param parameter_name_prefix: the parameter name prefix, the full name will
195            be ${parameter_name_prefix}-${env_name}.
196        :param s3folder_config: S3 配置数据的根目录, 默认为
197            s3://aws_account_id}-{aws_region}-artifacts/projects/acore_server_config/config/
198        :param bsm: ``boto_session_manager.BotoSesManager`` object, if not provided,
199            then use the current runtime default AWS CLI profile.
200        """
201        config = get_config(
202            bsm=bsm,
203            parameter_name_prefix=parameter_name_prefix,
204            env_name=env_name,
205            s3folder_config=s3folder_config,
206        )
207        env = config.get_env(env_name)
208        config_loader = cls()
209        config_loader._env = env
210        return config_loader
211
212    def iter_servers(self) -> T.Iterable[T.Tuple[str, Server]]:
213        """
214        遍历所有的 server. 返回许多 (server_name, server) 的 tuple. 这类似于字典中的
215        ``dict.items()`` 方法
216        """
217        return self._env.servers.items()
218
219    def get_server(self, server_name: str) -> Server:
220        """
221        获得特定 server 的配置数据.
222
223        :param server_name: 服务器的名字 (不包括环境名, 包括环境名的字符串是 server_id).
224            例如 "blue", "green" 等.
225        """
226        return self._env.servers[server_name]
227# [configloader-end]

Note

TODO: 介绍一下我们的 boostrap 程序是如何用这个库来将配置数据应用到游戏服务器上的.

Deploy Configuration Data#

管理员用 1. Load Configuration Data From Local Files 中的方法从本地配置文件中读取配置数据后, 就可以将其部署到 AWS S3 上了. config/deploy_parameters.py 脚本里有示例代码. 我个人也是用这个脚本来部署配置数据的. 该脚本的源码如下:

config/deploy_parameters.py
 1# -*- coding: utf-8 -*-
 2
 3"""
 4将所有服务器的配置数据部署到 AWS S3. 在这个项目中, 因为集群上的服务器数量多, 配置的内容复杂,
 5最终配置数据可能会很大. 只有用 AWS S3 才可以存储任意多, 任意大的数据.
 6"""
 7
 8from s3pathlib import S3Path
 9from acore_server_config.boto_ses import bsm
10from acore_server_config.config.init import config
11
12s3folder_config = (
13    S3Path(f"s3://{bsm.aws_account_id}-{bsm.aws_region}-artifacts")
14    .joinpath(
15        "projects",
16        "acore_server_config",
17        "config",
18    )
19    .to_dir()
20)
21config.deploy(bsm=bsm, s3folder_config=s3folder_config)
22# config.delete(bsm=bsm, s3folder_config=s3folder_config, include_history=True)

每次部署的时候, 我们不会 overwrite 已经存在的配置数据, 而是自动创建一个新的版本. 在 AWS S3 上的目录结构如下:

# 配置数据的根目录
s3://bucket/projects/acore_server_config/config/
# 这个目录下的配置数据文件是包含了所有环境的配置数据
s3://bucket/projects/acore_server_config/config/acore_server_config/
# 这几个目录只包含了属于自己环境的配置数据
s3://bucket/projects/acore_server_config/config/acore_server_config-sbx/
s3://bucket/projects/acore_server_config/config/acore_server_config-tst/
s3://bucket/projects/acore_server_config/config/acore_server_config-prd/
# 我们就拿 production 为例, 其他几个文件夹下的结构类似
# 每个环境的配置都会有一个 latest 文件和所有的历史版本文件, latest 中的数据永远和最新的历史版本一样
s3://bucket/projects/acore_server_config/config/acore_server_config-prd/acore_server_config-prd-latest.json
s3://bucket/projects/acore_server_config/config/acore_server_config-prd/acore_server_config-prd-000001.json
s3://bucket/projects/acore_server_config/config/acore_server_config-prd/acore_server_config-prd-000002.json
s3://bucket/projects/acore_server_config/config/acore_server_config-prd/acore_server_config-prd-000003.json

Note

之所以不用 parameter store 是因为配置数据可能会很大

I Lost My Local Secret Configuration File#

如果你是管理员, 并且不慎将本地的敏感配置数据文件删除了, 那么你可以到 AWS S3 上去找到 s3://bucket/projects/acore_server_config/config/acore_server_config/acore_server_config-latest.json 文件将其下载到本地, 然后把 secret_data key 下面的内容复制到 ${HOME}/.projects/acore_server_config/config-secret.json 文件中既可.

Update Game Server Config By Restarting EC2#

当你要更改游戏服务器配置时, 你通常需要将游戏服务器临时关闭, 更新配置, 然后重新启动. 由于我们的 EC2 重启时有 bootstrap 程序, 会从 AWS S3 从新读取并应用配置数据. 所以你可以简单的关闭 world server, 然后关闭 EC2, 用 Deploy Configuration Data 中的方法更新数据, 然后启动 EC2 既可.

Update Game Server Config Without Restarting EC2#

这一节介绍了如何在只重启 world server, 但是不重启 EC2 的情况下更新配置数据.

TODO 以后再补充说如何做.