基于compose单机部署 etcd + coredns

相关配置和脚本

目录结构

# 位于/home/dns目录下
coredns_stand-alone
.
├── compose-coredns.yaml
├── coredns
│   └── conf
│       └── Corefile
├── etcd
│   ├── certs
│   │   ├── ca-config.json
│   │   ├── ca.csr
│   │   ├── ca-csr.json
│   │   ├── ca-key.pem
│   │   ├── ca.pem
│   │   ├── server.csr
│   │   ├── server-csr.json
│   │   ├── server-key.pem
│   │   └── server.pem
│   └── data
└── etcd-cert-gen.sh

执行流程

# 请执行按照前文提到的目录结构来存放这些文件
# 服务部署文件在当前项目的demo文件夹下
# Docker和Docker-compose的安装就略过
# 格式化脚本使用dos2unix
# 由于脚本文件在Windows下编辑过,其换行符与Unix不同
# 安装dos2unix
yum install -y dos2unix
# 脚本格式化
dos2unix etcd-cert-gen.sh
# 进入项目目录
cd coredns_stand-alone
# 脚本赋权
chmod +x etcd-cert-gen.sh
# 执行脚本,生成etcd的tls证书
./etcd-cert-gen.sh
# 启动coredns容器
docker-compose -f ./compose-coredns.yaml up -d

.env

############################################################
######                 Global Setting                 ######
############################################################
COMPOSE_PROJECT_NAME=coredns

# etcd
# etcd uses gcr.io/etcd-development/etcd as a primary container registry, and quay.io/coreos/etcd as secondary.
# ETCD_IMAGE_NAME=registry.cn-hangzhou.aliyuncs.com/coreos_etcd/etcd:v3
ETCD_IMAGE_NAME=quay.io/coreos/etcd:v3.3.20
ETCD_API_VERSION=3
ETCD_DATA_DIR=./etcd/data
ETCD_CERT_DIR=./etcd/certs

# coredns
COREDNS_IMAGE_NAME=coredns/coredns:1.6.9
COREDNS_CONF_DIR=./coredns/conf

compose-coredns.yaml

version: '3'

services:
  # etcd service
  etcd0:
    image: ${ETCD_IMAGE_NAME}
    container_name: etcd0
    restart: always
    environment:
    - ETCDCTL_API=${ETCD_API_VERSION}
    - TZ=CST-8
    - LANG=zh_CN.UTF-8
    command:
    - "/usr/local/bin/etcd"
    # 成员
    - "--name=etcd0"
    - "--data-dir=/etcd-data"
    - "--advertise-client-urls=https://0.0.0.0:2379"
    - "--listen-client-urls=https://0.0.0.0:2379"
    #- "--listen-peer-urls=https://0.0.0.0:2380"
    # 集群
    #- "--initial-advertise-peer-urls=https://0.0.0.0:2380"
    #- "--initial-cluster-token=etcd-cluster"
    #- "--initial-cluster"
    #- "etcd0=https://0.0.0.0:2380"
    #- "--initial-cluster-state=new"
    # 安全
    - "--trusted-ca-file=/etcd-certs/ca.pem"
    - "--cert-file=/etcd-certs/etcd.pem"
    - "--key-file=/etcd-certs/etcd-key.pem"
    #- "--peer-trusted-ca-file=/etcd-certs/etcd-root-ca.pem"
    #- "--peer-cert-file=/etcd-certs/etcd.pem"
    #- "--peer-key-file=/etcd-certs/etcd-key.pem"
    # 日志
    - "--debug=true"
    volumes:
    - ${ETCD_DATA_DIR}:/etcd-data:rw
    - ${ETCD_CERT_DIR}:/etcd-certs:ro
    ports:
    - 2379:2379
    - 2380:2380
  
  # coredns service
  coredns:
    image: ${COREDNS_IMAGE_NAME}
    container_name: coredns
    restart: always
    network_mode: host
    depends_on:
    - etcd0
    command: -conf /etc/coredns/Corefile
    volumes:
    - ${COREDNS_CONF_DIR}:/etc/coredns:ro
    - ${ETCD_CERT_DIR}:/etcd-certs:ro

Corefile

.:53 {
    # 监听tcp和udp的53端口
    # 配置启用etcd插件,后面可以指定域名,例如 etcd test.com {}
    etcd {
        # 启用存根区域功能。 stubzone仅在位于指定的第一个区域下方的etcd树中完成
        stubzones
        # etcd里面的路径。默认为/coredns,以后所有的dns记录就是存储在该存根路径底下
        path /coredns
        # etcd访问地址,多个空格分开
        endpoint https://127.0.0.1:2379
        # upstream设置要使用的上游解析程序解决指向外部域名的在etcd(认为CNAME)中找到的外部域名。
        upstream 114.114.114.114:53 8.8.8.8:53 8.8.4.4:53 /etc/resolv.conf
        # 如果区域匹配但不能生成记录,则将请求传递给下一个插件
        fallthrough
        # 可选参数,etcd认证证书设置
        # 格式: tls CERT KEY CACERT
        tls /etcd-certs/etcd.pem /etcd-certs/etcd-key.pem /etcd-certs/ca.pem
        # 指定访问etcd用户名和密码(根据实际情况使用)
        # credentials USERNAME PASSWORD
    }
    # 健康
    health
    # 监控插件
    prometheus
    # 缓存时间
    cache 160
    # 自动加载时间间隔
    reload 6s
    # 负载均衡,开启DNS记录轮询策略
    loadbalance
    # 上面etcd未查询到的请求转发给设置的DNS服务器解析
    forward . 8.8.8.8:53 8.8.4.4:53 /etc/resolv.conf
    # 打印日志
    log
    # 输出错误
    errors
}

etcd-cert-gen.sh

#!/usr/bin/env bash

# 文档在Window下编辑过,需要转换为Unix格式。
# 安装工具: yum install -y dos2unix
# 然后执行命令: dos2unix ./etcd-cert-gen.sh

CFSSL_FILE="/usr/local/bin/cfssl"
CFSSL_JSON_FILE="/usr/local/bin/cfssljson"
CFSSL_CERTINFO_FILE="/usr/local/bin/cfssl-certinfo"

# 下载curl工具
# -----------------------
yum install -y curl

# 下载cfssl工具
# -----------------------
if [[ ! -f "$CFSSL_FILE" ]]; then
  curl -L https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -o ${CFSSL_FILE}
  chmod +x ${CFSSL_FILE}
  echo "------> cfssl has been installed successfully !! <------"
else
  echo "------> cfssl has already installed !! <------"
fi

if [[ ! -f "$CFSSL_JSON_FILE" ]]; then
  curl -L https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 -o ${CFSSL_JSON_FILE}
  chmod +x ${CFSSL_JSON_FILE}
  echo "------> cfssljson has been installed successfully !! <------"
else
  echo "------> cfssljson has already installed !! <------"
fi

if [[ ! -f "$CFSSL_CERTINFO_FILE" ]]; then
  curl -L https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 -o ${CFSSL_CERTINFO_FILE}
  chmod +x ${CFSSL_CERTINFO_FILE}
  echo "------> cfssl-certinfo has been installed successfully !! <------"
else
  echo "------> cfssl-certinfo has already installed !! <------"
fi

# 创建证书目录
# -----------------------
mkdir -p ./etcd/certs
cd  ./etcd/certs

# CA机构配置:有效期10年
# -----------------------
cat > ca-config.json <<EOF
{
  "signing": {
    "default": {
      "expiry": "87600h"
    },
    "profiles": {
      "server": {
         "expiry": "87600h",
         "usages": [
            "signing",
            "key encipherment",
            "server auth",
            "client auth"
        ]
      },
      "client": {
         "expiry": "87600h",
         "usages": [
            "signing",
            "key encipherment",
            "server auth",
            "client auth"
        ]
      }
    }
  }
}
EOF

# CA机构配置: 机构名称Comman Name,所在地Country国家, State省, Locality市
# -----------------------
cat > ca-csr.json <<EOF
{
    "CN": "etcd CA",
    "key": {
        "algo": "rsa",
        "size": 4096
    },
    "names": [
        {
            "C": "CN",
            "L": "HuNan",
            "O": "thyc",
            "ST": "ChangSha"
        }
    ]
}
EOF

# 如果是ETCD集群的话就直接在下面的hosts中添加IP或者域名。
# 向CA机构申请:证书注册 (中国,湖南省,长沙市), 提供服务的IP
# Organization Name, Common Name
# -----------------------
cat > server-csr.json <<EOF
{
    "CN": "etcd",
    "hosts": [
    "127.0.0.1",
    "192.168.1.111",
    "etcd0"
    ],
    "key": {
        "algo": "rsa",
        "size": 4096
    },
    "names": [
        {
            "C": "CN",
            "L": "HuNan",
            "O": "thyc",
            "ST": "ChangSha"
        }
    ]
}
EOF

# 使用定义好的配置初始化CA
cfssl gencert -initca ca-csr.json | cfssljson -bare ca -

# 生成服务器证书
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server server-csr.json | cfssljson -bare etcd

etcd client(java)

pom依赖

   <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <!--日志-->
        <!-- https://mvnrepository.com/artifact/log4j/log4j -->
        <!--<dependency>-->
            <!--<groupId>log4j</groupId>-->
            <!--<artifactId>log4j</artifactId>-->
            <!--<version>1.2.17</version>-->
        <!--</dependency>-->

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-nop</artifactId>
            <version>1.7.25</version>
            <scope>test</scope>
        </dependency>

        <!--<dependency>-->
            <!--<groupId>dnsjava</groupId>-->
            <!--<artifactId>dnsjava</artifactId>-->
            <!--<version>2.1.8</version>-->
        <!--</dependency>-->

        <dependency>
            <groupId>io.etcd</groupId>
            <artifactId>jetcd-core</artifactId>
            <version>0.5.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/io.netty/netty-tcnative-boringssl-static -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-tcnative-boringssl-static</artifactId>
            <version>2.0.29.Final</version>
            <scope>runtime</scope>
        </dependency>

    </dependencies>

    <build>
        <!-- finalName指定打包生成的文件名,默认为工程名-版本号 -->
        <finalName>mini-dns</finalName>
        <plugins>
            <!-- 指定jdk版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <!-- 源码的编译器版本 -->
                    <source>${java.version}</source>
                    <!-- class的编译器版本 -->
                    <target>${java.version}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <!-- 跳过测试 -->
                    <skip>true</skip>
                </configuration>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

EtcdCluster.java

/**
 * etcd node
 *
 * @author wzm
 * @version 1.0.0
 * @date 2020/5/8 17:21
 **/
public class EtcdCluster {
    private String ip;
    private Integer port;
    private Boolean tls;
    private String endpoint;

    public EtcdCluster(String ip, Integer port) {
        this.ip = ip;
        this.port = port;
    }

    protected void setTls(Boolean tls) {
        this.tls = tls;
    }

    public String getEndpoint() {
        return endpoint;
    }

    protected void generateFormattedEndpoint() {
        String http = "http://";
        String https = "https://";
        if (tls) {
            this.endpoint = https + ip + ":" + port.toString();
        }else {
            this.endpoint = http + ip + ":" + port.toString();
        }
    }
}

EtcdClient.java

import com.google.gson.JsonObject;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.KV;
import io.etcd.jetcd.KeyValue;
import io.etcd.jetcd.kv.DeleteResponse;
import io.etcd.jetcd.kv.GetResponse;
import io.grpc.netty.GrpcSslContexts;

import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

/**
 * etcd Client
 *
 * @author wzm
 * @version 1.0.0
 * @date 2020/5/8 16:53
 **/
public class EtcdClient {
    private List<EtcdCluster> etcdClusters;
    private String certPath;
    /**
     * etcd Client
     */
    private static Client client;
    /**
     * Charset
     */
    private static Charset charset = StandardCharsets.UTF_8;
    /**
     * Cache time
     */
    private static final Integer TTL = 600;
    /**
     * DNS name
     */
    private static final String DNS_NAME = "coredns";

    public EtcdClient(List<EtcdCluster> etcdClusters) {
        for (EtcdCluster etcd : etcdClusters) {
            etcd.setTls(false);
            etcd.generateFormattedEndpoint();
        }
        this.etcdClusters = etcdClusters;
        initClient();
    }

    public EtcdClient(List<EtcdCluster> etcdClusters, String certPath) {
        for (EtcdCluster etcd : etcdClusters) {
            etcd.setTls(true);
            etcd.generateFormattedEndpoint();
        }
        this.etcdClusters = etcdClusters;
        this.certPath = certPath;
        initClientWithTls();
    }

    public void close() {
        client.close();
    }

    /**
     * Initialize the etcd client to support clustering
     *
     * @author wzm
     * @date 2019/11/1 10:19
     */
    private void initClient() {
        String[] endpoints = new String[etcdClusters.size()];
        for (int i = 0; i < etcdClusters.size(); i++) {
            endpoints[i] = etcdClusters.get(i).getEndpoint();
        }
        client = Client.builder().endpoints(endpoints).build();
    }

    /**
     * Initialize the etcd client (TLS) to support clustering
     *
     * @author wzm
     * @date 2019/11/1 10:20
     */
    private void initClientWithTls() {
        String[] endpoints = new String[etcdClusters.size()];
        for (int i = 0; i < etcdClusters.size(); i++) {
            endpoints[i] = etcdClusters.get(i).getEndpoint();
        }
        try (InputStream is = getClass().getResourceAsStream(certPath)) {
            //还需要设置证书路径
            client = Client.builder()
                    .endpoints(endpoints)
                    .sslContext(
                            GrpcSslContexts
                                    .forClient()
                                    .trustManager(is)
                                    .build()
                    ).build();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * New/modified
     *
     * @param key        域名
     * @param value      ip
     * @param difference 区别
     * @author wzm
     * @date 2019/11/1 9:45
     */
    public boolean putRecord(String key, String value, String difference) {
        try {
            KV kv = client.getKVClient();
            ByteSequence k = ByteSequence.from(formatKey(key, difference), charset);
            ByteSequence v = ByteSequence.from(formatValue(value, TTL), charset);
            // put the key-value
            kv.put(k, v).join();
            return kv.get(k).join().getCount() == 1;
        } catch (Exception e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * query
     *
     * @param key        key
     * @param difference differ(The same domain name may correspond to more than one IP)
     * @return java.util.Map
     * @author wzm
     * @date 2019/11/1 10:02
     */
    public Map<String, String> getRecord(String key, String difference) {
        try {
            Map<String, String> map = new HashMap<>(1);
            KV kvClient = client.getKVClient();
            ByteSequence k = ByteSequence.from(formatKey(key, difference), charset);
            // get the CompletableFuture
            CompletableFuture<GetResponse> getFuture = kvClient.get(k);
            // get the value from CompletableFuture
            GetResponse response = getFuture.get();
            List<KeyValue> keyValues = response.getKvs();
            for (KeyValue kv : keyValues) {
                map.put(getKeyFromKv(kv), getValueFromKv(kv));
            }
            return map;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * delete
     *
     * @param key        dns name
     * @param difference differ
     * @author wzm
     * @date 2019/11/1 10:17
     */
    public boolean deleteRecord(String key, String difference) {
        try {
            KV kvClient = client.getKVClient();
            CompletableFuture<GetResponse> getFeature = kvClient.get(ByteSequence.from(formatKey(key, difference), charset));
            GetResponse resp = getFeature.get();
            ByteSequence k = ByteSequence.from(formatKey(key, difference), charset);
            DeleteResponse deleteResponse = kvClient.delete(k).get();
            long f = deleteResponse.getDeleted();
            return f == resp.getKvs().size();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Get the key from kv
     *
     * @param kv kv
     * @return java.lang.String
     * @author wzm
     * @date 2019/11/1 9:59
     */
    private static String getKeyFromKv(KeyValue kv) {
        return kv.getKey().toString(charset);
    }

    /**
     * Get the value from kv
     *
     * @param kv kv
     * @return java.lang.String
     * @author wzm
     * @date 2019/11/1 9:59
     */
    private static String getValueFromKv(KeyValue kv) {
        return kv.getValue().toString(charset);
    }


    private static String formatKey(String domainName, String difference) {
        String[] strings = domainName.split("\\.");
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("/" + DNS_NAME);
        for (int i = 0; i < strings.length; i++) {
            String tmp = strings[strings.length - 1 - i];
            stringBuilder.append("/").append(tmp);
        }
        String str = stringBuilder.toString();
        if (difference == null || "".equals(difference.trim())) {
            return str;
        }
        return str + "/" + difference;
    }

    private static String formatValue(String host, int ttl) {
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("host", host);
        jsonObject.addProperty("ttl", ttl);
        return jsonObject.toString();
    }
    //A记录
    //etcdctl put /coredns/com/leffss/www '{"host":"1.1.1.1","ttl":10}'
    //AAAA记录
    //etcdctl put /coredns/com/leffss/www '{"host":"1002::4:2","ttl":10}'
    //CNAME记录
    //etcdctl put /coredns/com/leffss/www '{"host":"www.baidu.com","ttl":10}'
    //SRV记录
    //etcdctl put /coredns/com/leffss/www '{"host":"www.baidu.com","port":80,"ttl":10}'
    //TXT记录
    //etcdctl put /coredns/com/leffss/www '{"text":"This is text!","ttl":10}'
}

源码地址:https://github.com/wenming5112/minidns

上一篇:维吉尼亚密码及其python实现


下一篇:Kivy中显示汉字的问题