flask编写一个简易版的DnsPod
DNS是当前互联网最重要的基础设施,一般企业内也会部署自己的私有DNS服务器,在我们公司就是部署BIND9做内部域名解析,用DnsPod做外部域名解析,为了配合Nginx的运维自动化,所以对外封装了DnsPod的api接口,对内自己用flask封装了一套api。目前的过程是使用数据库记录DNS的解析记录,通过模板生成DNS的配置文件,并推送到DNS服务器,然后reload生效,过程比较复杂。
近期尝试了直接使用BIND的DLZ(Dynamically Loadable Zones)功能,BIND直接使用MySQL做后端解析记录的存储,并且使用Flask为BIND做了数据库管理后台和RESTful API。
注意:下面会从源码编译一个支持MySQL做后端存储的BIND9,但并不是所有的BIND9都支持DLZ,编译前可以检查下configure命令有没有--with-dlz-mysql 的选项 。
除MySQL外,DLZ也支持Postgres、ldap等作为后端存储。
本文内容涉及到Linux基础命令、Docker、Python等多方面的知识,比较有意思,希望能让你有所收获。。
1 BIND9的编译和配置
1.1 使用dockerfile编译Bind9
下面我使用了Dockerfile来进行编译,Dockerfile的内容如下:
FROM centos:7 as builder
WORKDIR /tmp/build
ADD https://ftp.ripe.net/mirrors/sites/ftp.isc.org/isc/bind/9.12.4-P2/bind-9.12.4-P2.tar.gz .
RUN yum -y update && \
yum install -y mysql-devel gcc gcc-c++ make file python python-ply perl
RUN tar xvf bind-9.12.4-P2.tar.gz && cd bind-9.12.4-P2 && \
./configure --prefix=/data/program/bind9 --with-dlz-mysql && \
make -j 4 && \
make install
FROM centos:7 as runner
WORKDIR /data/program/bind9
RUN yum -y install mysql-devel
COPY --from=builder /data/program/bind9 ./
CMD ["/data/program/bind9/sbin/named", "-c", "/data/program/bind9/etc/named.conf", "-g"]
此处使用了Docker的多阶段编译,减少最后成品容器的大小。
1.2 下面开始docker容器的build过程
docker build . -t bind9-with-dlz-mysql
完成之后,会在本机生成一个 bind9-with-dlz-mysql:latest
的容器,后续我们也会将BIND9运行在该容器中。
1.3 准备MySQL
这里就不写MySQL的部署过程了,只提供创建数据库(本例中为bind9)和数据表(本例中为record)的语句。
mysql> create database bind9;
mysql> use bind9;
mysql> CREATE TABLE `record` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`zone` varchar(256) NOT NULL,
`host` varchar(256) NOT NULL DEFAULT '@',
`type` enum('MX','CNAME','NS','SOA','A','PTR') NOT NULL,
`data` varchar(256) DEFAULT NULL,
`ttl` int(11) NOT NULL DEFAULT '60',
`mx_priority` int(11) DEFAULT NULL,
`refresh` int(11) NOT NULL DEFAULT '3600',
`retry` int(11) NOT NULL DEFAULT '3600',
`expire` int(11) NOT NULL DEFAULT '86400',
`minimum` int(11) NOT NULL DEFAULT '3600',
`serial` bigint(20) NOT NULL DEFAULT '2020082916',
`resp_person` varchar(64) NOT NULL DEFAULT 'sb',
`primary_ns` varchar(64) NOT NULL DEFAULT 'ns.xnow.me.',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
mysql> insert INTO `record` (zone,host,type,data) values ('xnow.me','a','A','1.1.1.1');
mysql> insert INTO `record` (zone,host,type,data) values ('xnow.me','b','CNAME','a.xnow.me.');
1.4 准备BIND的配置文件named.conf
named.conf的内容如下,是从BIND的官方example中抄下来的,做了些许微调。
controls { };
options {
directory ".";
port 53;
pid-file "named.pid";
session-keyfile "session.key";
listen-on { any; };
listen-on-v6 { none; };
recursion no;
};
dlz "mysql-dlz" {
database "mysql
{host=127.0.0.1 dbname=bind9 ssl=false port=3306 user=root pass=root}
{select zone from record where zone = '$zone$' limit 1}
{select ttl, type, mx_priority, case when lower(type)='txt' then concat('\"', data, '\"') when lower(type) = 'soa' then concat_ws('', data, resp_person, serial, refresh, retry, expire,minimum) else data end from record where zone = '$zone$' and host ='$record$'}";
};
和DLZ相关的配置在最后一部分,配置了MySQL的数据库信息,包括用地址、端口号、用户名和密码等。后面两行select语句是从MySQL数据库中查询DNS记录的。
1.5 启动和测试
启动容器,根据dockerfile中的定义,会在前台输出请求日志
docker run --rm -v $PWD/named.conf:/data/program/bind9/etc/named.conf --name=named9 --net=host bind9-with-dlz-mysql:latest
新开一个终端窗口,使用 host 命令(也可以使用nslookup或者dig)进行测试。上文中,我们在创建表结构之后,注入了一条A记录和一条CNAME记录,这里请求试试看。
host -t A a.xnow.me 127.0.0.1
host -t A b.xnow.me 127.0.0.1
2. 使用Flask编写Bind9的后台和API
这里主要使用了2个模块:
- flask-admin ,生成数据库的管理后台
- flask-restplus, 生成api和 api swagger文档
首先安装下python的依赖,此处使用的是Python3.7
pip3 install flask flask-restplus flask-admin mysql flask-sqlalchemy sqlalchemy-serializer
2.1 Python代码
app.py代码内容如下:
import enum
from flask import Flask,request, jsonify, abort
from flask_admin import Admin, BaseView, expose
from flask_admin.contrib.sqla import ModelView
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy_serializer import SerializerMixin
from flask_restplus import Api, Resource, fields
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://root:root@localhost:3306/bind9?charset=utf8"
app.config['SECRET_KEY'] = "123456"
api = Api(app, version='1.0', title="Bind9 API")
admin = Admin(app, name="域名管理系统")
db = SQLAlchemy(app)
class RecordTypeEnum(enum.Enum):
A = 1
CNAME = 2
NS = 3
SOA = 4
PTR = 5
MX = 6
TXT = 7
AAAA = 8
class Record(db.Model, SerializerMixin): # 表数据结构
__tablename = "record"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
zone = db.Column(db.String(256), nullable=False)
host = db.Column(db.String(256), nullable=False)
type = db.Column(db.Enum(RecordTypeEnum), nullable=False)
data = db.Column(db.String(256), nullable=False)
ttl = db.Column(db.Integer, nullable=False, default=600)
mx_priority = db.Column(db.Integer)
refresh = db.Column(db.Integer, nullable=False, default=3600)
retry = db.Column(db.Integer, nullable=False, default=3600)
expire = db.Column(db.Integer, nullable=False, default=86400)
minimum = db.Column(db.Integer, nullable=False, default=3600)
serial = db.Column(db.Integer, nullable=False, default=2020082916)
resp_person = db.Column(db.String(256))
primary_ns = db.Column(db.String(256))
record = api.model('Record', {
'id': fields.Integer(readonly=True),
'zone': fields.String(required=True),
'host': fields.String(required=True),
'type': fields.String(required=True),
'data': fields.String(required=True),
'ttl': fields.Integer(default=60),
'resp_person': fields.String(required=True),
'primary_ns': fields.String(required=True),
})
class RecordView(Resource): # 创建api
@api.param("id", "record id")
def get(self):
id = request.args.get("id")
return Record.query.filter_by(id=id).first().to_dict() or abort(404)
@api.doc("add a dns record")
@api.expect(record)
@api.marshal_with(record, 201)
def post(self):
rcd = Record(**api.payload)
db.session.add(rcd)
db.session.commit()
return rcd, 201
admin.add_view(ModelView(Record, db.session))
api.add_resource(RecordView, '/api/record')
app.run(debug=True)
执行:
export FLASK_APP=app.py
flask run
2.2 使用
http://localhost:5000/admin # 这里是数据库的后台管理系统
http://localhost:5000/api/record # 这是管理解析记录的API,代码中只支持了 POST
和GET
两个方法。
http://localhost:5000/ # 这里是生成的swagger文档
基本具备了DnsPod的web管理功能和api管理功能。当然只是很简陋的版本,如果要用在生产环境中,还需要不断的完善。
3. DLZ的优缺点
使用这种模式的优点很明显:灵活、优雅。但是缺点也有,每次请求DNS解析都要查询数据库,可能存在如下的风险:
- 生产环境的MySQL数据库肯定是分布式主从部署,DNS请求查询数据库会导致解析的网络延迟增加(TCP三次握手)。
- MySQL的可靠性要求往往低于DNS,MySQL迁移过程中导致的偶尔请求中断可能会导致部分DNS解析失败。
- DNS的QPS高峰可能达到数万,对MySQL造成一定的并发压力。
- 如果MySQL主从切换依赖域名解析,可能导致MySQL主故障后,依赖DNS,而DNS因为主数据库故障而无法解析,导致的死锁问题。
但是在某些场景中,这个方案是十分有用的,比如存在缓存DNS服务的情况下,主MySQL或者主DNS临时无法解析不会导致客户端的请求失败。所以,我个人认为,在企业局域网中,这个方案不一定最佳,但是在广域网上,这个方案是十分优秀的。
当前暂无评论 »