手动一步步搭建k8s(Kubernetes)高可用集群

以前一直用 Kargo(基于 ansible) 来搭建 Kubernetes 集群,最近发现 ansible 部署的时候有些东西有点 bug,而且 Kargo 对 rkt 等也做了适配,感觉问题已经有点复杂化了;在 2.2 release 没出来这个时候,准备自己纯手动挡部署一下,Master HA 直接抄 Kargo 的就行了,以下记录一下

一、环境准备

以下文章本着 多写代码少哔哔 的原则,会主要以实际操作为主,不会过多介绍每步细节动作,如果纯小白想要更详细的了解,可以参考 这里

环境总共 5 台虚拟机,2 个 master,3 个 etcd 节点,master 同时也作为 node 负载 pod,在分发证书等阶段将在另外一台主机上执行,该主机对集群内所有节点配置了 ssh 秘钥登录

未分类

网络方案这里采用性能比较好的 Calico,集群开启 RBAC,RBAC 相关可参考:https://mritd.me/2017/07/17/kubernetes-rbac-chinese-translation/

二、证书相关处理

2.1、证书说明

由于 Etcd 和 Kubernetes 全部采用 TLS 通讯,所以先要生成 TLS 证书,证书生成工具采用 cfssl,具体使用方法这里不再详细阐述,生成证书时可在任一节点完成,这里在宿主机执行,证书列表如下

未分类

2.2、CFSSL 工具安装

首先下载 cfssl,并给予可执行权限,然后扔到 PATH 目录下

wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64
wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64
chmod +x cfssl_linux-amd64 cfssljson_linux-amd64
mv cfssl_linux-amd64 /usr/local/bin/cfssl
mv cfssljson_linux-amd64 /usr/local/bin/cfssljson

2.3、生成 Etcd 证书

Etcd 证书生成所需配置文件如下:

  • etcd-root-ca-csr.json
{
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [
    {
      "O": "etcd",
      "OU": "etcd Security",
      "L": "Beijing",
      "ST": "Beijing",
      "C": "CN"
    }
  ],
  "CN": "etcd-root-ca"
}
  • etcd-gencert.json
{
  "signing": {
    "default": {
        "usages": [
          "signing",
          "key encipherment",
          "server auth",
          "client auth"
        ],
        "expiry": "87600h"
    }
  }
}
  • etcd-csr.json
{
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [
    {
      "O": "etcd",
      "OU": "etcd Security",
      "L": "Beijing",
      "ST": "Beijing",
      "C": "CN"
    }
  ],
  "CN": "etcd",
  "hosts": [
    "127.0.0.1",
    "localhost",
    "192.168.1.11",
    "192.168.1.12",
    "192.168.1.13",
    "192.168.1.14",
    "192.168.1.15"
  ]
}

最后生成 Etcd 证书

cfssl gencert --initca=true etcd-root-ca-csr.json | cfssljson --bare etcd-root-ca
cfssl gencert --ca etcd-root-ca.pem --ca-key etcd-root-ca-key.pem --config etcd-gencert.json etcd-csr.json | cfssljson --bare etcd

生成的证书列表如下

未分类

2.4、生成 Kubernetes 证书

Kubernetes 证书生成所需配置文件如下:

  • k8s-root-ca-csr.json
{
  "CN": "kubernetes",
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [
    {
      "C": "CN",
      "ST": "BeiJing",
      "L": "BeiJing",
      "O": "k8s",
      "OU": "System"
    }
  ]
}
  • k8s-gencert.json
{
  "signing": {
    "default": {
      "expiry": "87600h"
    },
    "profiles": {
      "kubernetes": {
        "usages": [
            "signing",
            "key encipherment",
            "server auth",
            "client auth"
        ],
        "expiry": "87600h"
      }
    }
  }
}
  • kubernetes-csr.json
{
    "CN": "kubernetes",
    "hosts": [
        "127.0.0.1",
        "10.254.0.1",
        "192.168.1.11",
        "192.168.1.12",
        "192.168.1.13",
        "192.168.1.14",
        "192.168.1.15",
        "localhost",
        "kubernetes",
        "kubernetes.default",
        "kubernetes.default.svc",
        "kubernetes.default.svc.cluster",
        "kubernetes.default.svc.cluster.local"
    ],
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [
        {
            "C": "CN",
            "ST": "BeiJing",
            "L": "BeiJing",
            "O": "k8s",
            "OU": "System"
        }
    ]
}
  • kube-proxy-csr.json
{
  "CN": "system:kube-proxy",
  "hosts": [],
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "CN",
      "ST": "BeiJing",
      "L": "BeiJing",
      "O": "k8s",
      "OU": "System"
    }
  ]
}
  • admin-csr.json
{
  "CN": "admin",
  "hosts": [],
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "CN",
      "ST": "BeiJing",
      "L": "BeiJing",
      "O": "system:masters",
      "OU": "System"
    }
  ]
}

生成 Kubernetes 证书

cfssl gencert --initca=true k8s-root-ca-csr.json | cfssljson --bare k8s-root-ca

for targetName in kubernetes admin kube-proxy; do
    cfssl gencert --ca k8s-root-ca.pem --ca-key k8s-root-ca-key.pem --config k8s-gencert.json --profile kubernetes $targetName-csr.json | cfssljson --bare $targetName
done

生成后证书列表如下

未分类

2.5、生成 token 及 kubeconfig

生成 token 如下

export BOOTSTRAP_TOKEN=$(head -c 16 /dev/urandom | od -An -t x | tr -d ' ')
cat > token.csv <<EOF
${BOOTSTRAP_TOKEN},kubelet-bootstrap,10001,"system:kubelet-bootstrap"
EOF

创建 kubelet bootstrapping kubeconfig 配置,对于 node 节点,api server 地址为本地 nginx 监听的 127.0.0.1:6443,如果想把 master 也当做 node 使用,那么 master 上 api server 地址应该为 masterIP:6443,因为在 master 上没必要也无法启动 nginx 来监听 127.0.0.1:6443(6443 已经被 master 上的 api server 占用了)

所以以下配置只适合 node 节点,如果想把 master 也当做 node,那么需要重新生成下面的 kubeconfig 配置,并把 api server 地址修改为当前 master 的 api server 地址

export KUBE_APISERVER="https://127.0.0.1:6443"
# 设置集群参数
kubectl config set-cluster kubernetes 
  --certificate-authority=k8s-root-ca.pem 
  --embed-certs=true 
  --server=${KUBE_APISERVER} 
  --kubeconfig=bootstrap.kubeconfig
# 设置客户端认证参数
kubectl config set-credentials kubelet-bootstrap 
  --token=${BOOTSTRAP_TOKEN} 
  --kubeconfig=bootstrap.kubeconfig
# 设置上下文参数
kubectl config set-context default 
  --cluster=kubernetes 
  --user=kubelet-bootstrap 
  --kubeconfig=bootstrap.kubeconfig
# 设置默认上下文
kubectl config use-context default --kubeconfig=bootstrap.kubeconfig

创建 kube-proxy kubeconfig 配置,同上面一样,如果想要把 master 当 node 使用,需要修改 api server

# 设置集群参数
kubectl config set-cluster kubernetes 
  --certificate-authority=k8s-root-ca.pem 
  --embed-certs=true 
  --server=${KUBE_APISERVER} 
  --kubeconfig=kube-proxy.kubeconfig
# 设置客户端认证参数
kubectl config set-credentials kube-proxy 
  --client-certificate=kube-proxy.pem 
  --client-key=kube-proxy-key.pem 
  --embed-certs=true 
  --kubeconfig=kube-proxy.kubeconfig
# 设置上下文参数
kubectl config set-context default 
  --cluster=kubernetes 
  --user=kube-proxy 
  --kubeconfig=kube-proxy.kubeconfig
# 设置默认上下文
kubectl config use-context default --kubeconfig=kube-proxy.kubeconfig

三、部署 HA ETCD

3.1、安装 Etcd

ETCD 直接采用 rpm 安装,RPM 可以从 Fedora 官方仓库 获取 spec 文件自己 build,或者直接从 rpmFind 网站 搜索

# 下载 rpm 包
wget ftp://195.220.108.108/linux/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/e/etcd-3.1.9-1.fc27.x86_64.rpm
# 分发并安装 rpm
for IP in `seq 1 3`; do
    scp etcd-3.1.9-1.fc27.x86_64.rpm [email protected]$IP:~
    ssh [email protected]$IP rpm -ivh etcd-3.1.9-1.fc27.x86_64.rpm
done

3.2、分发证书

for IP in `seq 1 3`;do
    ssh [email protected]$IP mkdir /etc/etcd/ssl
    scp *.pem [email protected]$IP:/etc/etcd/ssl
    ssh [email protected]$IP chown -R etcd:etcd /etc/etcd/ssl
    ssh [email protected]$IP chmod -R 755 /etc/etcd
done

3.3、修改配置

rpm 安装好以后直接修改 /etc/etcd/etcd.conf 配置文件即可,其中单个节点配置如下(其他节点只是名字和 IP 不同)

# [member]
ETCD_NAME=etcd1
ETCD_DATA_DIR="/var/lib/etcd/etcd1.etcd"
ETCD_WAL_DIR="/var/lib/etcd/wal"
ETCD_SNAPSHOT_COUNT="100"
ETCD_HEARTBEAT_INTERVAL="100"
ETCD_ELECTION_TIMEOUT="1000"
ETCD_LISTEN_PEER_URLS="https://192.168.1.11:2380"
ETCD_LISTEN_CLIENT_URLS="https://192.168.1.11:2379,http://127.0.0.1:2379"
ETCD_MAX_SNAPSHOTS="5"
ETCD_MAX_WALS="5"
#ETCD_CORS=""

# [cluster]
ETCD_INITIAL_ADVERTISE_PEER_URLS="https://192.168.1.11:2380"
# if you use different ETCD_NAME (e.g. test), set ETCD_INITIAL_CLUSTER value for this name, i.e. "test=http://..."
ETCD_INITIAL_CLUSTER="etcd1=https://192.168.1.11:2380,etcd2=https://192.168.1.12:2380,etcd3=https://192.168.1.13:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_ADVERTISE_CLIENT_URLS="https://192.168.1.11:2379"
#ETCD_DISCOVERY=""
#ETCD_DISCOVERY_SRV=""
#ETCD_DISCOVERY_FALLBACK="proxy"
#ETCD_DISCOVERY_PROXY=""
#ETCD_STRICT_RECONFIG_CHECK="false"
#ETCD_AUTO_COMPACTION_RETENTION="0"

# [proxy]
#ETCD_PROXY="off"
#ETCD_PROXY_FAILURE_WAIT="5000"
#ETCD_PROXY_REFRESH_INTERVAL="30000"
#ETCD_PROXY_DIAL_TIMEOUT="1000"
#ETCD_PROXY_WRITE_TIMEOUT="5000"
#ETCD_PROXY_READ_TIMEOUT="0"

# [security]
ETCD_CERT_FILE="/etc/etcd/ssl/etcd.pem"
ETCD_KEY_FILE="/etc/etcd/ssl/etcd-key.pem"
ETCD_CLIENT_CERT_AUTH="true"
ETCD_TRUSTED_CA_FILE="/etc/etcd/ssl/etcd-root-ca.pem"
ETCD_AUTO_TLS="true"
ETCD_PEER_CERT_FILE="/etc/etcd/ssl/etcd.pem"
ETCD_PEER_KEY_FILE="/etc/etcd/ssl/etcd-key.pem"
ETCD_PEER_CLIENT_CERT_AUTH="true"
ETCD_PEER_TRUSTED_CA_FILE="/etc/etcd/ssl/etcd-root-ca.pem"
ETCD_PEER_AUTO_TLS="true"

# [logging]
#ETCD_DEBUG="false"
# examples for -log-package-levels etcdserver=WARNING,security=DEBUG
#ETCD_LOG_PACKAGE_LEVELS=""

3.4、启动及验证

配置修改后在每个节点进行启动即可,注意,Etcd 哥哥节点间必须保证时钟同步,否则会造成启动失败等错误

systemctl daemon-reload
systemctl start etcd
systemctl enable etcd

启动成功后验证节点状态

export ETCDCTL_API=3
etcdctl --cacert=/etc/etcd/ssl/etcd-root-ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem --endpoints=https://192.168.1.11:2379,https://192.168.1.12:2379,https://192.168.1.13:2379 endpoint health

最后截图如下,警告可忽略

未分类

四、部署 HA Master

4.1、HA Master 简述

目前所谓的 Kubernetes HA 其实主要的就是 API Server 的 HA,master 上其他组件比如 controller-manager 等都是可以通过 Etcd 做选举;而 API Server 只是提供一个请求接收服务,所以对于 API Server 一般有两种方式做 HA;一种是对多个 API Server 做 vip,另一种使用 nginx 反向代理,本文采用 nginx 方式,以下为 HA 示意图

未分类

master 之间除 api server 以外其他组件通过 etcd 选举,api server 默认不作处理;在每个 node 上启动一个 nginx,每个 nginx 反向代理所有 api server,node 上 kubelet、kube-proxy 连接本地的 nginx 代理端口,当 nginx 发现无法连接后端时会自动踢掉出问题的 api server,从而实现 api server 的 HA

4.2、部署前预处理

一切以偷懒为主,所以我们仍然采用 rpm 的方式来安装 kubernetes 各个组件,关于 rpm 获取方式可以参考 How to build Kubernetes RPM,以下文章默认认为你已经搞定了 rpm

# 分发 rpm
for IP in `seq 1 3`; do
    scp kubernetes*.rpm [email protected]$IP:~; 
    ssh [email protected]$IP yum install -y conntrack-tools socat
    ssh [email protected]$IP rpm -ivh kubernetes*.rpm
done

rpm 安装好以后还需要进行分发证书配置等

for IP in `seq 1 3`;do
    ssh [email protected]$IP mkdir /etc/kubernetes/ssl
    scp *.pem [email protected]$IP:/etc/kubernetes/ssl
    scp *.kubeconfig [email protected]$IP:/etc/kubernetes
    scp token.csv [email protected]$IP:/etc/kubernetes
    ssh [email protected]$IP chown -R kube:kube /etc/kubernetes/ssl
done

最后由于 api server 会写入一些日志,所以先创建好相关目录,并做好授权,防止因为权限错误导致 api server 无法启动

for IP in `seq 1 3`;do
    ssh [email protected]$IP mkdir /var/log/kube-audit  
    ssh [email protected]$IP chown -R kube:kube /var/log/kube-audit
    ssh [email protected]$IP chmod -R 755 /var/log/kube-audit
done

4.3、修改 master 配置

rpm 安装好以后,默认会生成 /etc/kubernetes 目录,并且该目录中会有很多配置,其中 config 配置文件为通用配置,具体文件如下

➜  kubernetes tree
.
├── apiserver
├── config
├── controller-manager
├── kubelet
├── proxy
└── scheduler

0 directories, 6 files

master 需要编辑 config、apiserver、controller-manager、scheduler这四个文件,具体修改如下

  • config 通用配置
###
# kubernetes system config
#
# The following values are used to configure various aspects of all
# kubernetes services, including
#
#   kube-apiserver.service
#   kube-controller-manager.service
#   kube-scheduler.service
#   kubelet.service
#   kube-proxy.service
# logging to stderr means we get it in the systemd journal
KUBE_LOGTOSTDERR="--logtostderr=true"

# journal message level, 0 is debug
KUBE_LOG_LEVEL="--v=2"

# Should this cluster be allowed to run privileged docker containers
KUBE_ALLOW_PRIV="--allow-privileged=true"

# How the controller-manager, scheduler, and proxy find the apiserver
KUBE_MASTER="--master=http://127.0.0.1:8080"
  • apiserver 配置(其他节点只有 IP 不同)
###
# kubernetes system config
#
# The following values are used to configure the kube-apiserver
#

# The address on the local server to listen to.
KUBE_API_ADDRESS="--advertise-address=192.168.1.11 --insecure-bind-address=127.0.0.1 --bind-address=192.168.1.11"

# The port on the local server to listen on.
KUBE_API_PORT="--insecure-port=8080 --secure-port=6443"

# Port minions listen on
# KUBELET_PORT="--kubelet-port=10250"

# Comma separated list of nodes in the etcd cluster
KUBE_ETCD_SERVERS="--etcd-servers=https://192.168.1.11:2379,https://192.168.1.12:2379,https://192.168.1.13:2379"

# Address range to use for services
KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=10.254.0.0/16"

# default admission control policies
KUBE_ADMISSION_CONTROL="--admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota"

# Add your own!
KUBE_API_ARGS="--authorization-mode=RBAC 
               --runtime-config=rbac.authorization.k8s.io/v1beta1 
               --anonymous-auth=false 
               --kubelet-https=true 
               --experimental-bootstrap-token-auth 
               --token-auth-file=/etc/kubernetes/token.csv 
               --service-node-port-range=30000-50000 
               --tls-cert-file=/etc/kubernetes/ssl/kubernetes.pem 
               --tls-private-key-file=/etc/kubernetes/ssl/kubernetes-key.pem 
               --client-ca-file=/etc/kubernetes/ssl/k8s-root-ca.pem 
               --service-account-key-file=/etc/kubernetes/ssl/k8s-root-ca.pem 
               --etcd-quorum-read=true 
               --storage-backend=etcd3 
               --etcd-cafile=/etc/etcd/ssl/etcd-root-ca.pem 
               --etcd-certfile=/etc/etcd/ssl/etcd.pem 
               --etcd-keyfile=/etc/etcd/ssl/etcd-key.pem 
               --enable-swagger-ui=true 
               --apiserver-count=3 
               --audit-log-maxage=30 
               --audit-log-maxbackup=3 
               --audit-log-maxsize=100 
               --audit-log-path=/var/log/kube-audit/audit.log 
               --event-ttl=1h"
  • controller-manager 配置
###
# The following values are used to configure the kubernetes controller-manager

# defaults from config and apiserver should be adequate

# Add your own!
KUBE_CONTROLLER_MANAGER_ARGS="--address=0.0.0.0 
                              --service-cluster-ip-range=10.254.0.0/16 
                              --cluster-name=kubernetes 
                              --cluster-signing-cert-file=/etc/kubernetes/ssl/k8s-root-ca.pem 
                              --cluster-signing-key-file=/etc/kubernetes/ssl/k8s-root-ca-key.pem 
                              --service-account-private-key-file=/etc/kubernetes/ssl/k8s-root-ca-key.pem 
                              --root-ca-file=/etc/kubernetes/ssl/k8s-root-ca.pem 
                              --leader-elect=true 
                              --node-monitor-grace-period=40s 
                              --node-monitor-period=5s 
                              --pod-eviction-timeout=5m0s"
  • scheduler 配置
###
# kubernetes scheduler config

# default config should be adequate

# Add your own!
KUBE_SCHEDULER_ARGS="--leader-elect=true --address=0.0.0.0"

其他 master 节点配置相同,只需要修改以下 IP 地址即可,修改完成后启动 api server

systemctl daemon-reload
systemctl start kube-apiserver
systemctl start kube-controller-manager
systemctl start kube-scheduler
systemctl enable kube-apiserver
systemctl enable kube-controller-manager
systemctl enable kube-scheduler

各个节点启动成功后,验证组件状态(kubectl 在不做任何配置的情况下默认链接本地 8080 端口)如下,其中 etcd 全部为 Unhealthy 状态,并且提示 remote error: tls: bad certificate 这是个 bug,不影响实际使用,具体可参考 issue

未分类

五、部署 Node

5.1、部署前预处理

部署前分发 rpm 以及证书、token 等配置

# 分发 rpm
for IP in `seq 4 5`;do
    scp kubernetes-node-1.6.7-1.el7.centos.x86_64.rpm kubernetes-client-1.6.7-1.el7.centos.x86_64.rpm [email protected]$IP:~; 
    ssh [email protected]$IP yum install -y conntrack-tools socat
    ssh [email protected]$IP rpm -ivh kubernetes-node-1.6.7-1.el7.centos.x86_64.rpm kubernetes-client-1.6.7-1.el7.centos.x86_64.rpm
done
# 分发证书等配置文件
for IP in `seq 4 5`;do
    ssh [email protected]$IP mkdir /etc/kubernetes/ssl
    scp *.pem [email protected]$IP:/etc/kubernetes/ssl
    scp *.kubeconfig [email protected]$IP:/etc/kubernetes
    scp token.csv [email protected]$IP:/etc/kubernetes
    ssh [email protected]$IP chown -R kube:kube /etc/kubernetes/ssl
done

5.2、修改 node 配置

node 节点上配置文件同样位于 /etc/kubernetes 目录,node 节点只需要修改 config、kubelet、proxy 这三个配置文件,修改如下

  • config 通用配置

注意: config 配置文件(包括下面的 kubelet、proxy)中全部未 定义 API Server 地址,因为 kubelet 和 kube-proxy 组件启动时使用了 –require-kubeconfig 选项,该选项会使其从 *.kubeconfig 中读取 API Server 地址,而忽略配置文件中设置的;所以配置文件中设置的地址其实是无效的

###
# kubernetes system config
#
# The following values are used to configure various aspects of all
# kubernetes services, including
#
#   kube-apiserver.service
#   kube-controller-manager.service
#   kube-scheduler.service
#   kubelet.service
#   kube-proxy.service
# logging to stderr means we get it in the systemd journal
KUBE_LOGTOSTDERR="--logtostderr=true"

# journal message level, 0 is debug
KUBE_LOG_LEVEL="--v=2"

# Should this cluster be allowed to run privileged docker containers
KUBE_ALLOW_PRIV="--allow-privileged=true"

# How the controller-manager, scheduler, and proxy find the apiserver
# KUBE_MASTER="--master=http://127.0.0.1:8080"

kubelet 配置

###
# kubernetes kubelet (minion) config

# The address for the info server to serve on (set to 0.0.0.0 or "" for all interfaces)
KUBELET_ADDRESS="--address=192.168.1.14"

# The port for the info server to serve on
# KUBELET_PORT="--port=10250"

# You may leave this blank to use the actual hostname
KUBELET_HOSTNAME="--hostname-override=docker4.node"

# location of the api-server
# KUBELET_API_SERVER=""

# Add your own!
KUBELET_ARGS="--cgroup-driver=cgroupfs 
              --cluster-dns=10.254.0.2 
              --resolv-conf=/etc/resolv.conf 
              --experimental-bootstrap-kubeconfig=/etc/kubernetes/bootstrap.kubeconfig 
              --kubeconfig=/etc/kubernetes/kubelet.kubeconfig 
              --require-kubeconfig 
              --cert-dir=/etc/kubernetes/ssl 
              --cluster-domain=cluster.local. 
              --hairpin-mode promiscuous-bridge 
              --serialize-image-pulls=false 
              --pod-infra-container-image=gcr.io/google_containers/pause-amd64:3.0"

proxy 配置

###
# kubernetes proxy config

# default config should be adequate

# Add your own!
KUBE_PROXY_ARGS="--bind-address=192.168.1.14 
                 --hostname-override=docker4.node 
                 --kubeconfig=/etc/kubernetes/kube-proxy.kubeconfig 
                 --cluster-cidr=10.254.0.0/16"

5.3、创建 ClusterRoleBinding

由于 kubelet 采用了 TLS Bootstrapping,所有根绝 RBAC 控制策略,kubelet 使用的用户 kubelet-bootstrap 是不具备任何访问 API 权限的,这是需要预先在集群内创建 ClusterRoleBinding 授予其 system:node-bootstrapper Role

# 在任意 master 执行即可
kubectl create clusterrolebinding kubelet-bootstrap 
  --clusterrole=system:node-bootstrapper 
  --user=kubelet-bootstrap

5.4、创建 nginx 代理

根据上面描述的 master HA 架构,此时所有 node 应该连接本地的 nginx 代理,然后 nginx 来负载所有 api server;以下为 nginx 代理相关配置

# 创建配置目录
mkdir -p /etc/nginx

# 写入代理配置
cat << EOF >> /etc/nginx/nginx.conf
error_log stderr notice;

worker_processes auto;
events {
  multi_accept on;
  use epoll;
  worker_connections 1024;
}

stream {
    upstream kube_apiserver {
        least_conn;
        server 192.168.1.11:6443;
        server 192.168.1.12:6443;
        server 192.168.1.13:6443;
    }

    server {
        listen        0.0.0.0:6443;
        proxy_pass    kube_apiserver;
        proxy_timeout 10m;
        proxy_connect_timeout 1s;
    }
}
EOF

# 更新权限
chmod +r /etc/nginx/nginx.conf

为了保证 nginx 的可靠性,综合便捷性考虑,node 节点上的 nginx 使用 docker 启动,同时 使用 systemd 来守护, systemd 配置如下

cat << EOF >> /etc/systemd/system/nginx-proxy.service
[Unit]
Description=kubernetes apiserver docker wrapper
Wants=docker.socket
After=docker.service

[Service]
User=root
PermissionsStartOnly=true
ExecStart=/usr/bin/docker run -p 127.0.0.1:6443:6443 \
                              -v /etc/nginx:/etc/nginx \
                              --name nginx-proxy \
                              --net=host \
                              --restart=on-failure:5 \
                              --memory=512M \
                              nginx:1.13.3-alpine
ExecStartPre=-/usr/bin/docker rm -f nginx-proxy
ExecStop=/usr/bin/docker stop nginx-proxy
Restart=always
RestartSec=15s
TimeoutStartSec=30s

[Install]
WantedBy=multi-user.target
EOF

最后启动 nginx,同时可以使用 kubectl 测试 api server 负载情况

systemctl daemon-reload
systemctl start nginx-proxy
systemctl enable nginx-proxy

启动成功后如下

未分类

kubectl 测试联通性如下

未分类

5.5、添加 Node

一起准备就绪以后就可以启动 node 相关组件了

systemctl daemon-reload
systemctl start kubelet
systemctl enable kubelet

由于采用了 TLS Bootstrapping,所以 kubelet 启动后不会立即加入集群,而是进行证书申请,从日志中可以看到如下输出

Jul 19 14:15:31 docker4.node kubelet[18213]: I0719 14:15:31.810914   18213 feature_gate.go:144] feature gates: map[]
Jul 19 14:15:31 docker4.node kubelet[18213]: I0719 14:15:31.811025   18213 bootstrap.go:58] Using bootstrap kubeconfig to generate TLS client cert, key and kubeconfig file

此时只需要在 master 允许其证书申请即可

# 查看 csr
➜  kubernetes kubectl get csr
NAME        AGE       REQUESTOR           CONDITION
csr-l9d25   2m        kubelet-bootstrap   Pending
# 签发证书
➜  kubernetes kubectl certificate approve csr-l9d25
certificatesigningrequest "csr-l9d25" approved
# 查看 node
➜  kubernetes kubectl get node
NAME           STATUS    AGE       VERSION
docker4.node   Ready     26s       v1.6.7

最后再启动 kube-proxy 组件即可

systemctl start kube-proxy
systemctl enable kube-proxy

5.6、Master 开启 Pod 负载

Master 上部署 Node 与单独 Node 部署大致相同,只需要修改 bootstrap.kubeconfig、kube-proxy.kubeconfig 中的 API Server 地址即可

未分类

然后修改 kubelet、proxy 配置启动即可

systemctl daemon-reload
systemctl start kubelet
systemctl enable kubelet
systemctl start kube-proxy
systemctl enable kube-proxy

最后在 master 签发一下相关证书

kubectl certificate approve csr-z090b

整体部署完成后如下

未分类

六、部署 Calico

网路组件这里采用 Calico,Calico 目前部署也相对比较简单,只需要创建一下 yml 文件即可,具体可参考 Calico 官方文档

Cliaco 官方文档要求 kubelet 启动时要配置使用 cni 插件 –network-plugin=cni,同时 kube-proxy 使用 –masquerade-all 启动,所以需要修改所有 kubelet 和 proxy 配置文件增加这两项,以下默认为这两项已经调整完毕,这里不做演示

# 获取相关 Cliaco.yml
wget http://docs.projectcalico.org/v2.3/getting-started/kubernetes/installation/hosted/calico.yaml

# 修改 Etcd 相关配置,以下列出主要修改部分(etcd 证书内容需要被 base64 转码)

sed -i 's@.*etcd_endpoints:.*@  etcd_endpoints: "https://192.168.1.11:2379,https://192.168.1.12:2379,https://192.168.1.13:2379"@gi' calico.yaml

export ETCD_CERT=`cat /etc/etcd/ssl/etcd.pem | base64 | tr -d 'n'`
export ETCD_KEY=`cat /etc/etcd/ssl/etcd-key.pem | base64 | tr -d 'n'`
export ETCD_CA=`cat /etc/etcd/ssl/etcd-root-ca.pem | base64 | tr -d 'n'`

sed -i "s@.*etcd-cert:.*@  etcd-cert: ${ETCD_CERT}@gi" calico.yaml
sed -i "s@.*etcd-key:.*@  etcd-key: ${ETCD_KEY}@gi" calico.yaml
sed -i "s@.*etcd-ca:.*@  etcd-ca: ${ETCD_CA}@gi" calico.yaml

sed -i 's@.*etcd_ca:.*@  etcd_ca: "/calico-secrets/etcd-ca"@gi' calico.yaml
sed -i 's@.*etcd_cert:.*@  etcd_cert: "/calico-secrets/etcd-cert"@gi' calico.yaml
sed -i 's@.*etcd_key:.*@  etcd_key: "/calico-secrets/etcd-key"@gi' calico.yaml

sed -i '[email protected]/[email protected]/18@gi' calico.yaml

执行部署操作,注意,在开启 RBAC 的情况下需要单独创建 ClusterRole 和 ClusterRoleBinding

kubectl create -f calico.yaml
kubectl apply -f http://docs.projectcalico.org/v2.3/getting-started/kubernetes/installation/rbac.yaml

部署完成后如下

未分类

最后测试一下跨主机通讯

# 创建 deployment
cat << EOF >> demo.deploy.yml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: demo-deployment
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - name: demo
        image: mritd/demo
        ports:
        - containerPort: 80
EOF
kubectl create -f demo.deploy.yml

exec 到一台主机 pod 内 ping 另一个不同 node 上的 pod 如下

未分类

七、部署 DNS

7.1、DNS 组件部署

DNS 部署目前有两种方式,一种是纯手动,另一种是使用 Addon-manager,目前个人感觉 Addon-manager 有点繁琐,所以以下采取纯手动部署 DNS 组件

DNS 组件相关文件位于 kubernetes addons 目录下,把相关文件下载下来然后稍作修改即可

# 获取文件
mkdir dns && cd dns
wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/kubedns-cm.yaml
wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/kubedns-sa.yaml
wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/kubedns-svc.yaml.sed
wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/kubedns-controller.yaml.sed
mv kubedns-controller.yaml.sed kubedns-controller.yaml
mv kubedns-svc.yaml.sed kubedns-svc.yaml
# 修改配置
sed -i 's/$DNS_DOMAIN/cluster.local/gi' kubedns-controller.yaml
sed -i 's/$DNS_SERVER_IP/10.254.0.2/gi' kubedns-svc.yaml
# 创建(我把所有 yml 放到的 dns 目录中)
kubectl create -f ../dns

接下来测试 DNS,测试方法创建两个 deployment 和 svc,通过在 pod 内通过 svc 域名方式访问另一个 deployment 下的 pod,相关测试的 deploy、svc 配置在这里不在展示,基本情况如下图所示

未分类

未分类

7.2、DNS 自动扩容部署

关于 DNS 自动扩容详细可参考https://kubernetes.io/docs/tasks/administer-cluster/dns-horizontal-autoscaling/

以下直接操作

首先获取 Dns horizontal autoscaler 配置文件

wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns-horizontal-autoscaler/dns-horizontal-autoscaler-rbac.yaml
wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns-horizontal-autoscaler/dns-horizontal-autoscaler.yaml

然后直接 kubectl create -f 即可,DNS 自动扩容计算公式为 replicas = max( ceil( cores * 1/coresPerReplica ) , ceil( nodes * 1/nodesPerReplica ) ),如果想调整 DNS 数量(负载因子),只需要调整 ConfigMap 中对应参数即可,具体计算细节参考上面的官方文档

# 编辑 Config Map
kubectl edit cm kube-dns-autoscaler --namespace=kube-system

ubuntu使用kubeadm安装配置k8s(Kubernetes)

安装

翻墙使用root权限执行以下内容或者参考这里

wget https://coding.net/u/scaffrey/p/hosts/git/raw/master/hosts 
cp hosts /etc/hosts

安装

apt-get update && apt-get install -y apt-transport-https
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF > /etc/apt/sources.list.d/kubernetes.list
deb http://apt.kubernetes.io/ kubernetes-xenial main
EOF
apt-get update
apt-get install -y docker.io
apt-get install -y kubelet kubeadm kubectl kubernetes-cni

配置master节点

在master节点执行以下命令

kubeadm init  --pod-network-cidr 10.244.0.0/16

使master node参与工作负载

kubectl taint nodes --all node-role.kubernetes.io/master-

安装网络

kubectl create -f https://github.com/coreos/flannel/raw/master/Documentation/kube-flannel-rbac.yml
kubectl create -f  https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

配置worker节点

执行初始化master节点后打印的最后一行

kubeadm reset
kubeadm join --token 6aa93f.04f7fbce49b7f3bb 222.20.101.106:6443

使用

使用kubectl的run命令创建deployment

kubectl run nginx --image=nginx:1.10.0
kubectl get pods -o wide

使用expose 将端口暴露出来

kubectl expose deployment nginx --port 80 --type LoadBalancer
kubectl get services -o wide

通过scale命令扩展应用

kubectl scale deployments/nginx --replicas=4
kubectl get pods -o wide

创建nginx版本

更新应用镜像,滚动更新应用镜像

kubectl set image deployments/nginx nginx=qihao/nginx

确认更新

kubectl rollout status deployments/nginx 

回滚到之前版本

kubectl rollout undo deployments/nginx 

负载均衡(不停的刷新服务的地址,过段时间会有变化)

kubectl exec -it nginx-2027219757-r1sqm bash
uname -n > /usr/share/nginx/html/index.html 

结束

kubectl delete deployment nginx && kubectl delete service nginx
kubectl get services -o wide
kubectl get pods -o wide
kubectl get nodes

k8s(kubernetes) kube-proxy转发模式及service转发类型介绍

配置方式

kubernetes版本大于或者等于1.2时,外部网络(即非K8S集群内的网络)访问cluster IP的办法是:
修改master的/etc/kubernetes/proxy,把KUBE_PROXY_ARGS=”“改为KUBE_PROXY_ARGS=”–proxy-mode=userspace”
启动kube-proxy服务
在核心路由设备或者源主机上添加一条路由,访问cluster IP段的路由指向到master上。

kubernetes版本小于1.2时,直接添加路由

kube-proxy转发的两种模式

kube-proxy在转发时主要有两种模式Userspace和Iptables。如下图,左侧是Userspace模式,也是kube-proxy默认的方式,所有的转发都是通过kube-proxy软件实现的;右侧是Iptables模式,所有转发都是通过Iptables内核模块实现,而kube-proxy只负责生成相应的Iptables规则。

未分类

使用Userspace模式(k8s版本为1.2之前默认模式),外部网络可以直接访问cluster IP。
使用Iptables模式(k8s版本为1.2之后默认模式),外部网络不能直接访问cluster IP。
从效率上看,Iptables会更高一些,但是需要Iptables version >=1.4.11,Iptables模式在k8s1.2版本放出。

service转发后端服务的三种类型

ClusterIP:提供一个集群内部的虚拟IP以供Pod访问。
NodePort:在每个Node上打开一个端口以供外部访问。
LoadBalancer:通过外部的负载均衡器来访问。

ClusterIP

此类型会提供一个集群内部的虚拟IP(与Pod不在同一网段),以供集群内部的pod之间通信使用。ClusterIP也是Kubernetes service的默认类型。

未分类

为了实现图上的功能主要需要以下几个组件的协同工作:
apiserver:在创建service时,apiserver接收到请求以后将数据存储到etcd中。
kube-proxy:k8s的每个节点中都有该进程,负责实现service功能,这个进程负责感知service,pod的变化,并将变化的信息写入本地的iptables中。
iptables:使用NAT等技术将virtualIP的流量转至endpoint中。

NodePort

NodePort模式除了使用cluster ip外,也将service的port映射到每个node的一个指定内部port上,映射的每个node的内部port都一样。
为每个节点暴露一个端口,通过nodeip + nodeport可以访问这个服务,同时服务依然会有cluster类型的ip+port。内部通过clusterip方式访问,外部通过nodeport方式访问。

loadbalance

只能使用在在gce这样的云环境上。

service的三种端口

port

service暴露在cluster ip上的端口,:port 是提供给集群内部客户访问service的入口。

nodePort

nodePort是k8s提供给集群外部客户访问service入口的一种方式,:nodePort 是提供给集群外部客户访问service的入口。

targetPort
targetPort是pod上的端口,从port和nodePort上到来的数据最终经过kube-proxy流入到后端pod的targetPort上进入容器。

port、nodePort总结

总的来说,port和nodePort都是service的端口,前者暴露给集群内客户访问服务,后者暴露给集群外客户访问服务。从这两个端口到来的数据都需要经过反向代理kube-proxy流入后端pod的targetPod,从而到达pod上的容器内。

k8s(kubernetes)部署三个节点的redis cluster

目的

redis clustor 需要6台服务器才能正常运⾏,由于种种原因,开发或者某些特别的需求,只能在3台服务器上运⾏redis clustor。在不使用哨兵模式情况下,而使⽤最新的clustor模式运行redis。

本文仅作为redis部署方式的研究及理解

准备工作

制作redis docker.latest镜像其中包含以下组件:

  1. redis-cli
  2. ruby
  3. redis-trib

打包到镜像上传到阿里镜像服务器中cluster-redis:latest

创建集群操作

3台服务器上各自运行两个redis容器

使用以下编写好的redis-cluster部署文件,可在一台部署出两个不同端口,不同角⾊的redis容器。

redis-cluster.yaml

apiVersion: extensions/v1beta1
kind: Deployment

metadata:

name: redis-blue

labels:

app: redis

member: redis-blue

spec:

replicas: 3

template:

metadata:

labels:

app: redis

member: redis-blue

spec:

hostNetwork: true

containers:

- name: redis

image: registry.cn-hangzhou.aliyuncs.com/wise2c/cluster-redis:latest

command: ["/bin/sh", "-c"]

args: ["echo 'dir /tmp/data' >> /root/redis.conf && /usr/local/bin/redis-server /root/redis.conf"]

- /usr/local/bin/redis-server

- /root/redis.conf

ports:

- name: redis-port

containerPort: 6379

- name: cluster-port

containerPort: 16379

volumeMounts:

- mountPath: /tmp/data

name: data

volumes:

- name: data

hostPath:

path: /tmp/redis-blue

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

name: redis-green

labels:

app: redis

member: redis-green

spec:

replicas: 3

template:

metadata:

labels:

app: redis

member: redis-green

spec:

hostNetwork: true

containers:

- name: redis

image: registry.cn-hangzhou.aliyuncs.com/wise2c/cluster-redis:latest

command: ["/bin/sh", "-c"]

args: ["sed -i 's/6379/6380/g' /root/redis.conf && echo 'dir /tmp/data' >> /root/redis.conf && /usr/local/ports:

- name: redis-port

containerPort: 6380

- name: cluster-port

containerPort: 16380

volumeMounts:

- mountPath: /tmp/data

name: data

volumes:

- name: data

hostPath:

path: /tmp/redis-green

kubectl create -f redis-cluster.yaml

执行以下脚本,创建出整个redis集群。redis会自动分配哈希槽,使得master与其对应的slave不会出现在同一台服务器上。

create_cluster.sh

!/usr/bin/env bash

redis_count=`kubectl get pod -o wide -l app=redis | grep Running | wc -l`
echo "redis_count:"$redis_count

if [ $redis_count -ne 6 ]; then
echo "the running redis count: ${redis_count} is error"
exit 1
fi
redis_blue_ips=`kubectl get pod -o wide -l app=redis -l member=redis-blue | awk 'NR>1{printf $6":6379 "}'
redis_green_ips=`kubectl get pod -o wide -l app=redis -l member=redis-green | awk 'NR>1{printf $6":6380 "}'
redis_ips=$redis_blue_ips" "$redis_green_ips
echo "redis_ips:"$redis_ips
redis_blue_name=`kubectl get pod -o wide -l app=redis -l member=redis-blue | grep Running | awk '{printf $1" "}'
echo $redis_ips | awk -F' ' '{for( i=1;i<NF; i++ ) print $i}' 
kubectl create -f redis-cluster.yaml

bash create_cluster.sh

关掉其中一台机器

由于master和slave 不在同一台机器上,当我们直接关掉其中⼀一台vm,比如vm2

这时vm3上,由redis cluster自动恢复vm3slave2(6380) —> master2(6380) 提升为master,集群工作正常,业务不中断。

恢复关掉的的机器

当我们重新启动vm2, 这时候集群的状态:

这时集群工作正常,此时vm3有2个master。如果我们关掉vm3,会让集群中2个master同时下线,导致集群无法自动恢复。

重点:执行以下脚本,提升slave2到master2,恢复集群的自动修复功能。

failover.sh

!/usr/bin/env bash

redis_blue_name=`kubectl get pod -o wide -l app=redis -l member=redis-blue | grep Running | awk '{printf $1":6379 redis_names=`kubectl get pod -o wide -l app=redis -l member=redis-blue | grep Running | awk '{printf $1","}'
redis_array=${redis_names//,/ }
for redis_name in $redis_array
do
kubectl exec -it ${redis_name} -- redis-cli cluster failover
done
bash failover.sh

集群自动恢复,变成下面的状态。

集群工作正常,业务不中断。

作者后话

以上的操作,目的是让各台虚拟上不出现有2个master。当其中一台虚拟机出现当机,集群就不能正常工作.。如果是3台master和3台slave分别在不同的机器刚好错开,redis cluster能自动恢复.。最好的解决方案,依然是有6台虚拟机、3台master、3台slave的redis cluster。

To do

我们可以进⼀一步将

1.create-redis.sh
2.failover.sh
3.kubectl

制作为镜像, k8s deployment运行此镜像。实现:

1.创建集群
2.恢复集群的自动修复功能(任意关掉其中一台服务器,业务不中断。每过一秒,检查集群中是否有slave需要提升权限,集群都能正常工作.业务不中断。)

iptables INPUT链使用实例说明

一、简介一下iptables

iptables命令中ACCEPT(允许流量通过)、LOG(记录日志信息)、REJECT(拒绝流量通过)、DROP(拒绝流量通过)。允许动作和记录日志工作都比较好理解,着重需要讲解的是这两条拒绝动作的不同点,其中REJECT和DROP的动作操作都是把数据包拒绝,DROP是直接把数据包抛弃不响应,而REJECT会拒绝后再回复一条“您的信息我已收到,但被扔掉了”,让对方清晰的看到数据被拒绝的响应。下图是iptables的选项。

未分类

二、我们今天分别演示

  • 设置拒绝规则链(默认只能是DROP不能是REJECT);
  • 向规则链中添加icmp包流入允许策略(就是允许别人通过ping命令来查看我们的主机是否在线);
  • 只允许本机指定端口被指定网段访问,其他流量均被拒绝; – 拒绝所有人访问本机指定端口;
  • 拒绝指定主机访问本机指定端口;
  • 拒绝所有人访问本机指定的端口段;
  • 删除指定策略;
  • 最后保存设置的防火墙策略。

1、设置拒绝规则链(默认只能是DROP不能是REJECT)。首先我们输入“iptables -F”清空所有防火墙策略(一定要在实验环境下做,不然会把配置好的策略清除)。然后输入“iptables -P INPUT DROP”。设置好以后我们用“iptables -L”命令查看一下策略列表。然后用ping命令验证一下是否设置成功。可以看到已经拒绝了。成功之后我们为了下面的演示,把INPUT策略设置为允许通过“iptables -P ACCEPT”。

2、向规则链中添加icmp包流入允许策略(就是允许别人通过ping命令来查看我们的主机是否在线)。输入命令:

iptables -I INPUT -p icmp -j ACCEPT

然后我们用ping命令测试一下,可以看到ping通了。

3、只允许本机22端口被192.168.1.0/24网段访问,其他流量均被拒绝。我们输入命令:

iptables -I INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT

和命令:

iptables -A INPUT -p tcp --dport 22 -j REJECT

同学们都知道22端口是ssh服务占用的资源。我们用ssh测试一下,可以看到能够连接到远程主机。

4、拒绝所有人访问本机8888端口,输入命令:

iptables -I INPUT -p tcp --dport 8888 -j REJECT

和命令:

iptables -I INPUT -p udp --dport 8888 -j REJECT

5、拒绝指定主机192.168.1.200访问本机80端口,输入命令:

iptables -I INPUT -s 192.168.1.200 -p tcp --dport 80 -j REJECT

6、拒绝所有人访问本机4444到5555端口,输入命令:

iptables -A INPUT -p tcp --dport 4444:5555 -j REJECT

和命令:

iptables -A INPUT -p udp --dport 4444:5555 -j REJECT

7、删除INPUT链中的第2条策略,首先我们看一下防火墙策略中的第二条策略是什么“iptables -L”,然后输入命令删除第2条策略:

iptables -D INPUT 2

可以看到第2条策略被删除了。

8、设置的防护墙策略在重启系统之后会消失,所有我们要保存设置好的策略,输入命令:

service iptables save

好了,今天的演示就到这里了。

iptables status模块用法介绍

在我们防火墙的配置中,还有一类很重要,就是通过status 来判断是否DROP

下面我们说下status的四种状态:

1. ESTABLISHED

什么样的数据包状态是ESTABLISHED?

未分类

1.1 如果ssh客户端能够在第一次发送请求的时候通过防火墙,到达ssh 服务器,那么之后无论是①还是②状态都是ESTABLISHED

1.2 如果我们再服务器(右侧服务器也是防火墙)上运行firefox 请求DNS服务器,使用的是udp, 如果第一次请求能够通过防护墙,则接下来两个方向的数据包状态都是ESTABLISHED

1.3 如果服务器上的ping命令可以穿过防火墙,icmp,则以后两个方向都是ESTABLISHED状态

2. NEW

TCP,UDP,ICMP对法发送的第一个包的状态都是NEW

3. RELATED

未分类

途中的④这种被动产生的数据就是RELATED状态

上图讲的是tracert工具的原理,如果TTL = 2 ,那么 每经过一个路由器-1 ,当变成0的时候,当前路由器会发送一个信息告诉发送端(不是上一个路由器),你发的包TTL=0了,也就是途中的④

4. INVALID

状态不明的都是INVALID,也就是不属于上边3中的

Centos7系统iptables开放指定端口(80)和配置ftp防火墙规则

查看开放的端口

sudo /etc/init.d/iptables status

开放指定端口

开放80端口,允许数据包从80端口进入,开放其它端口一样改成对应的数字,比如ftp21和ssh的22端口

sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT

保存所做的更改

sudo /etc/rc.d/init.d/iptables save

ftp的设置方法

有时候我们使用ftp的时候明明开放啦21端口但却还是连接不上,原因如下

FTP简介

ftp有 FTP主动模式和被动模式的区别

简单的说,主动模式是从服务器端向客户端发起连接;被动模式是客户端向服务器端发起连接。两者的共同点是都使用 21端口进行用户验证及管理,差别在于传送数据的方式不同,主动(PORT)模式的FTP服务器数据端口固定在20,而被动(PASV)模式则在1025-65535之间 随机。

了解ftp原理后,接下来就去设置iptables规则。我自己服务器上默认INPUT规则是DROP,OUTPUT是ACCEPT。

未分类

如上图所示连接的时候会出现20342这个端口,而这个端口在防火墙里是没有开放的所以就出错啦,而被动模式又是随机端口的我们也不可能都设置开放
我们可以设置一下接受所有状态为ESTABLISHED、RELATED的连接,就可以啦如下

 sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

使用iptables实现端口转发

1. 介绍

传统的端口转发工具portproxy 、rinetd 等, 这些应用工具都通过接收并转发 tcp 数据报文实现转发端口的目的, 但是都存在或多或少的缺陷, 比如不能 tcp/udp 同时支持, 难以修改数据报文的一些路由规则等. 庆幸的是我们可以通过 linux 的 iptables 的数据包过滤规则在 kernel 层面实现端口的转发.

在 iptables 的层面, 端口转发也可以称为端口映射, 是通过NAT(地址转发)的方式来修改数据包目的地址或端口, 再将报文转发到最终的主机(通常在没有公网地址的私有网络中). 通过这种方式用户既可以访问到远端的私有网络的机器(比如运行着 http 服务的主机).

2. 访问结构

我们以如下结构来讲解如何在 public A 主机中进行端口转发, 使得用户可以访问到后端的 private B 主机的 memcached 端口:

note: 所有主机均为 Centos 系统, 1.1.1.1 为任意的公网地址.

   +------+           +----------+             +-----------+
   | user |  -------> | public A | ----------> | private B | 
   +------+           +----------+             +-----------+
   pub: 2.2.2.2     em1: 10.0.21.5             em1: 10.0.21.7
                    em2: 1.1.1.1

图中 public A 主机的 em2 网卡为公网地址, 最终 user 可以通过访问 1.1.1.1:20011 来访问 private B 的 11211 端口. user 用户的主机可能存在于私有网络中, 也可能有独立的公网地址, 后续会介绍两者的不同.

3. iptables 中的数据报文流程

linux 用户可以通过 iptables 及其一系列的规则来高度控制数据报文的传输. 而 iptables 中的表则是其构件块, 描述了功能的大类, iptables 一共有4个表, 分别如下:

filter
nat
mangle
raw

每个表都有自己的一组内置链, 用户基于这些链可以建立一组规则, 常用的有 filter 表中的 INPUT、OUTPUT、和 FORWARD 链等.

下图描述了数据包进入一台主机的 iptables 的工作流程:

                               XXXXXXXXXXXXXXXXXX
                             XXX     Network    XXX
                               XXXXXXXXXXXXXXXXXX
                                       +
                                       |
                                       v
 +-------------+              +------------------+
 |table: filter| <---+        | table: nat       |
 |chain: INPUT |     |        | chain: PREROUTING|
 +-----+-------+     |        +--------+---------+
       |             |                 |
       v             |                 v
 [local process]     |           ****************          +--------------+
       |             +---------+ Routing decision +------> |table: filter |
       v                         ****************          |chain: FORWARD|
****************                                           +------+-------+
Routing decision                                                  |
****************                                                  |
       |                                                          |
       v                        ****************                  |
+-------------+       +------>  Routing decision  <---------------+
|table: nat   |       |         ****************
|chain: OUTPUT|       |               +
+-----+-------+       |               |
      |               |               v
      v               |      +-------------------+
+--------------+      |      | table: nat        |
|table: filter | +----+      | chain: POSTROUTING|
|chain: OUTPUT |             +--------+----------+
+--------------+                      |
                                      v
                               XXXXXXXXXXXXXXXXXX
                             XXX    Network     XXX
                               XXXXXXXXXXXXXXXXXX

本文要介绍的端口转发就是基于 nat 表的 PREROUTING 和 POSTROUTING 链, 所有的数据报文都要先经过 nat 的 PREROUTING 链进行处理, 再根据路由规则选择是进入 filter 的 INPUT 链还是 filter 的 FORWARD 链, 不管进入哪个链, 之后都会进去 nat 表的 POSTROUTING 链, 最后数据报文再转发出去.

4. 设置端口转发

从上面的数据报文的流程来看, 要在 public A 主机中实现端口转发大致有两种方式, 第一种就是文中最开始介绍的 portproxy、rinetd 工具, 这些工具的数据报文在进入 nat 的 PREROUTING 后就进入了 filter 的 INPUT 链. 第二种则是本文要介绍的方法, 数据在进入 nat 的 PREROUTING 链后 直接进入 filter 的 FORWARD 链, 因此要进行以下操作:

4.1 开启内核 ip_forward 转发

redhat/centos 系列系统默认为 0, 或者在 /etc/sysctl.conf 文件进行更改以永久生效;

sysctl -w net.ipv4.ip_forward=1

4.2 设置 PREROUTING 路由规则

用户访问 1.1.1.1:20011 的时候, 通过 DNAT 的方式将数据报文中的目的 ip 信息改为后端的 private B 地址 10.0.21.7:11211.

iptables -t nat -A PREROUTING -d 1.1.1.1/32 -p tcp -m tcp --dport 20011 -j DNAT --to-destination 10.0.21.7:11211

如果 public A 主机的公网地址是固定的静态 ip, 则不用设置下面的参数:

iptables -t nat -A POSTROUTING -o em2 -j MASQUERADE 

4.3 增加 filter 表的 FORWARD 规则

该步骤不是必须的, 如果当前 FORWARD 链的默认规则为 REJECT 则需要添加, 如果是 ACCEPT 就不需要执行下面的操作.

iptables -I FORWARD 1 -d 10.0.21.7/32 -j ACCEPT

4.4 设置 POSTROUTING 路由规则

该步骤也不是必须的, 主要视 public A 主机的路由规则而定, 默认情况下是不需要增加的, 因为 FORWARD 规则会通过 10.0.21.5 内网地址进行转发. 这里的 SNAT 则是将数据报文的源地址改为 10.0.21.5(即 public A 的内网地址), 再发送出去.

iptables -t nat -I POSTROUTING 1 -d 10.0.21.7/32 -p tcp -m tcp --dport 11211 -j SNAT --to-source 10.0.21.5

设置完成后, 用户既可以通过 telnet 1.1.1.1 20011 验证端口转发的有效性.

5. 访问出现的问题.

在上述步骤设置完成后, 笔者也碰到了一个有趣的问题, 如果 user 的主机有独立的公网则可以 telnet 通过, 如果 user 的主机也是存在于私网中, 即也是通过 NAT 的方式访问 public A 主机的话, 就会出现 telnet 超时的问题.

通过 tcpdump 抓包来看看数据报文的走向:

5.1 user 在本地的私网环境中 telnet public A 主机:

telnet 1.1.1.1 20011
Trying 1.1.1.1...
^C

user 本地端抓包:

# tcpdump -S -s0 -nn -i any port 20011
10:09:20.018174 IP 192.168.1.101.51782 > 1.1.1.1.20011: Flags [S], seq 3245571896, win 14600, options [mss 1460,sackOK,TS val 57645414 ecr 0,nop,wscale 7], length 0
10:09:21.017320 IP 192.168.1.101.51782 > 1.1.1.1.20011: Flags [S], seq 3245571896, win 14600, options [mss 1460,sackOK,TS val 57646414 ecr 0,nop,wscale 7], length 0

public A 主机抓包:

# tcpdump -S -nn -i any port 11211 or port 20011
10:09:22.777271 IP 2.2.2.2.57158 > 1.1.1.1.20011: Flags [S], seq 3937785824, win 14600, options [mss 1380,sackOK,TS val 57645414 ecr 0,nop,wscale 7], length 0
10:09:22.777335 IP 10.0.21.5.57158 > 10.0.21.7.11211: Flags [S], seq 3937785824, win 14600, options [mss 1380,sackOK,TS val 57645414 ecr 0,nop,wscale 7], length 0
10:09:23.776389 IP 2.2.2.2.57158 > 1.1.1.1.20011: Flags [S], seq 3937785824, win 14600, options [mss 1380,sackOK,TS val 57646414 ecr 0,nop,wscale 7], length 0
10:09:23.776420 IP 10.0.21.5.57158 > 10.0.21.7.11211: Flags [S], seq 3937785824, win 14600, options [mss 1380,sackOK,TS val 57646414 ecr 0,nop,wscale 7], length 0

private B 主机抓包:

# tcpdump -S -nn -i any port 11211
10:09:23.773626 IP 10.0.21.5.57158 > 10.0.21.7.11211: Flags [S], seq 3937785824, win 14600, options [mss 1380,sackOK,TS val 57646414 ecr 0,nop,wscale 7], length 0
10:09:25.773608 IP 10.0.21.5.57158 > 10.0.21.7.11211: Flags [S], seq 3937785824, win 14600, options [mss 1380,sackOK,TS val 57648414 ecr 0,nop,wscale 7], length 0

从两个 tcpdump 结果可以看出, 数据报文已经正常到了 private B 主机, 也就说已经通过了 public A 主机的 POSTROUTING 处理, 将包转发到了后端的 B 主机, 但是 B 主机没有响应, 正常的三次握手也没有建立完成, 也就是 B 主机直接丢弃了 A 发送过来的报文.

但是如果 user 主机有独立的公网, 则正常验证通过. 这点很让人迷惑, tcpdump 的结果中唯一不同的就是数据报文开头的时间戳信息, 但是 tcp 选项里的 TS val 值是以 user 本地端为准的. 这让笔者想到了 TCP 时间戳的一个问题, 参见 dropping-of-connections-with-tcp-tw-recycle, 而 B 主机上的 tcp_tw_recycle 的参数是开启的. tcp_tw_recycle 内核参数到底有什么用? 下面是内核文档的解释:

kernel-doc-2.6.32/Documentation/networking/ip-sysctl.txt

tcp_tw_recycle - BOOLEAN
        Enable fast recycling TIME-WAIT sockets. Default value is 0.
        It should not be changed without advice/request of technical
        experts.

linux 系统的 TIME_WAIT 状态用来保障连接的正常关系, 实际上并不会消耗过多的资源, 但是在高并发的环境中很多技术人员会将 tcp_tw_recycle 和 tcp_tw_reuse 参数打开用来快速回收和重用 TIME_WAIT 的 socket 连接, 这在一定程度上可以提升机器的性能, 不过也会带来一些难以预料的问题.

当 tcp_tw_recycle 和 tcp_timestamps 参数同时开启的时候, 同一源 ip 的连接, 在 TIME_WAIT 状态下, 系统内核会追踪其最近的时间戳信息, 如果时间戳正常增长就允许重用(re-use)该连接的 socket, 如果时间戳异常变更, 该主机就会丢弃接收到 SYN 报文, 这就会引起上面令人迷惑的问题. 同样再来看看我们的环境, user 如果存在于 NAT 环境, 在连接 public server 的时候, 用户侧的 NAT 只会更改 IP 的源地址信息, 而不会改变时间戳(tcp 报文的时间戳基于系统启动的时间, tcp 报文的 timestamps 选项), rfc文档规定时间戳值必须为单调递增,否则接受到的包可能会被丢掉. 所以对于后端的 private B 主机而言, 其保存着 public A 主机转发时候的连接信息, 这个连接的时间戳也是最新的值, 而 user 本地端的时间戳信息则远远小于该值, 这就会引起 B 主机直接丢弃 user 发送过来的请求. 这种问题实际上在 LVS 环境中也是比较普遍的, 很多人都建议线上的机器只开启 tcp_tw_reuse 选项, 让 tcp_tw_recycle 保持默认, 不要开启.

另外 tcp_timestamps 参数控制时间戳信息, 而在内核代码中 #define tcp_time_stamp ((__u32)(jiffies)) 内核每秒中将 jiffies 变量增加 HZ 次, 对于 HZ 值为 100 的系统, 1 个 jiffy 就等于 1000/100 = 10ms, 对于 1000 的系统, 1 个 jiffy 就是 1ms, 本文中测试的机器的系统的 HZ 为 1000, 如下:

cat /boot/config-2.6.32-573.18.1.el6.x86_64| grep HZ
CONFIG_NO_HZ=y
CONFIG_HZ_1000=y
CONFIG_HZ=1000
CONFIG_MACHZ_WDT=m

我们来看看正常的 telnet 请求的情况:

12:26:41.599122 IP 2.2.2.2.26597 > 1.1.1.1.20011: Flags [S], seq 1403291286, win 14600, options [mss 1380,sackOK,TS val 65884228 ecr 0,nop,wscale 7], length 0
12:26:41.599155 IP 10.0.21.5.26597 > 10.0.21.7.11211: Flags [S], seq 1403291286, win 14600, options [mss 1380,sackOK,TS val 65884228 ecr 0,nop,wscale 7], length 0
12:26:41.599219 IP 10.0.21.7.11211 > 10.0.21.5.26597: Flags [S.], seq 159148930, ack 1403291287, win 14480, options [mss 1460,sackOK,TS val 1681744061 ecr 65884228,nop,wscale 7], length 0
12:26:41.599226 IP 1.1.1.1.20011 > 2.2.2.2.26597: Flags [S.], seq 159148930, ack 1403291287, win 14480, options [mss 1460,sackOK,TS val 1681744061 ecr 65884228,nop,wscale 7], length 0
12:26:41.602296 IP 2.2.2.2.26597 > 1.1.1.1.20011: Flags [.], ack 159148931, win 115, options [nop,nop,TS val 65884232 ecr 1681744061], length 0
12:26:41.602321 IP 10.0.21.5.26597 > 10.0.21.7.11211: Flags [.], ack 159148931, win 115, options [nop,nop,TS val 65884232 ecr 1681744061], length 0
...
...
12:26:44.119060 IP 10.0.21.7.11211 > 10.0.21.5.26597: Flags [.], ack 1403291294, win 114, options [nop,nop,TS val 1681746581 ecr 65886749], length 0
12:26:44.119068 IP 1.1.1.1.20011 > 2.2.2.2.26597: Flags [.], ack 1403291294, win 114, options [nop,nop,TS val 1681746581 ecr 65886749], length 0

这是正常的三次握手的过程, 第三个包为 private B 主机的响应, 倒数第三个包的 TS val 为 65884232, 倒数第二个报的 ecr 为 65886749, 相减为 2.517个 HZ, 即经过了 2517 ms, 刚好对应每行的时间信息. 而最后一个包的 TS val 值 1681746581 会被 private B 主机保存为连接的最新时间戳(如果 tcp_tw_recycle 和 tcp_timestamps 同时开启的话).

6. 总结

总体上 iptables 的端口转发功能是在内核层面实现的, 用户通过 iptables 及一系列规则可以高度控制数据报文的流向, 比起传统的转发工具, 在灵活性方面有了很大的提升, 不过 iptables 方式在安全层面也会有一些隐患, 比如来源地址一定要限制好, 否则用户只要能路由到 public A 主机, 就可以访问转发的端口, 这点不像我们以往了解的只在 filter 表里限制就可以. 最后也需要特别注意 tcp 相关的内核参数设置, selinux 的限制也可能会影响端口转发的可用性.

Ubuntu 16.04系统配置iptables防火墙加强系统安全

这几天使用EasyEngine配置一台VPS主机,安装好之后,发现EasyEngine虽然有安装iptables防火墙程序,但没有配置文件,为了服务器的安全,建议大家启用防火墙设置,所以就需要手动来配置iptables.rules文件。

使用指令查看系统是否安装防火墙:

whereis iptables

输出:

iptables: /sbin/iptables /etc/iptables.rules /usr/share/iptables /usr/share/man/man8/iptables.8.gz #表示已經安裝 iptables

如果 Ubuntu 默认没有安装,请运行此指令安装 iptables 防火墙

apt-get install iptables

查看防火墙配置信息,显示如下:

#iptables -L -n

Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination

开始配置 iptables.rules 文件,因为 VPS 主机是安装 EasyEngine 程序,所以下面的 port 要打开:

22 / TCP (Inbound / Outbound) : Standard SSH port
80 / TCP (Inbound / Outbound) : Standard HTTP port
443 / TCP(Inbound / Outbound) : Standard HTTPS port
22222 / TCP (Inbound) : To access EasyEngine admin tools
11371 / TCP (Outbound) : To connect to GPG Key Server

可将下面的配置 copy 贴上:

# sample configuration for iptables service
# you can edit this manually or use system-config-firewall
# please do not ask us to add additional ports/services to this default configuration
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 465 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 587 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 22222 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 11371 -j ACCEPT
-A INPUT -p icmp -m limit --limit 1/s --limit-burst 10 -j ACCEPT
-A INPUT -p icmp -m limit --limit 100/sec --limit-burst 100 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
COMMIT

我增加了两条规则,作用是每秒钟只允许 100 个数据包,用来防止 DDoS 攻击

-A INPUT -p icmp -m limit --limit 1/sec --limit-burst 10 -j ACCEPT
-A INPUT -f -m limit --limit 100/sec --limit-burst 100 -j ACCEPT

使防火墙规则生效:

iptables-restore < /etc/iptables.rules

CentOS 上可以执行:service iptables save 保存规则,但是需要注意的是 Debian / Ubuntu 上 iptables 是不会保存规则的。需要按如下步骤进行,让网卡关闭保存 iptables 规则,启动时加载 iptables 规则。

创建 /etc/network/if-post-down.d/iptables 文件,添加如下内容:

#!/bin/bash
iptables-save > /etc/iptables.rules

添加执行权限:

chmod +x /etc/network/if-post-down.d/iptables

创建 /etc/network/if-pre-up.d/iptables 文件,添加如下内容:

#!/bin/bash
iptables-restore < /etc/iptables.rules

添加执行权限:

chmod +x /etc/network/if-pre-up.d/iptables

最后查看 iptables 规则是否生效:

iptables -L -n

iptables 的规则其实还有很多,这边只有列出基本常用到的规则,更多的规则说明就要请各位自行上网查询了。

Centos7将firewall防火墙换回iptables防火墙

从Centos7开始默认使用firewall防火墙了

换成iptables防火墙,操作步骤如下:

关闭firewalld防火墙,关闭开机自启

systemctl stop firewalld.service
systemctl disable firewalld.service

安装iptables防火墙,设置开机自启

yum -y install iptables-services net-tools
systemctl enable iptables.service

然后编辑iptables防火墙规则就好了

vim /etc/sysconfig/iptables

来个示例:

*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
COMMIT