为存量Hadoop集群注入Consul Connect实现透明化mTLS加密


我们团队维护着一个规模不小的、基于物理机部署的Hadoop集群。安全审计提出的新要求是:所有核心组件之间的数据传输必须强制加密,尤其是NameNode与DataNode之间的RPC调用,以及YARN ResourceManager与NodeManager之间的通信。传统的解决方案是启用Hadoop自身的Kerberos + RPC SASL加密,但任何经历过Hadoop Kerberos配置与运维的人都清楚,那是一场关于principal、keytab和复杂配置文件的噩梦,尤其是在一个已经运行了多年的庞大集群上进行改造,风险和工作量都难以估量。

团队内部开始探讨,能否用云原生的思路来解决这个“遗留系统”的问题。服务网格技术,特别是其透明化的mTLS加密能力,进入了我们的视野。多数服务网格方案(如Istio)与Kubernetes深度绑定,但我们的Hadoop集群是纯粹的VM/物理机环境。HashiCorp的Consul,凭借其平台无关的Agent模式,成为了最可行的候选者。

目标很明确:利用Consul Connect作为旁路组件,为Hadoop核心服务(HDFS, YARN)提供服务发现和自动mTLS加密,同时对Hadoop应用程序做到最小化侵入,彻底绕开Kerberos。

初步构想与架构设计

核心思路是为每个Hadoop守护进程(Daemon)部署一个Consul Agent,并为其启动一个Envoy Sidecar代理。Hadoop进程本身不直接相互通信,而是将所有出站流量发送到本地的Sidecar,由Sidecar负责建立mTLS连接并将流量转发到目标服务的Sidecar,最终再由目标Sidecar将解密后的流量发送给目标Hadoop进程。

这将形成如下的数据流:

DataNode进程 -> 本地Envoy Proxy -> mTLS加密隧道 -> NameNode节点的Envoy Proxy -> NameNode进程

这个方案的成败关键在于两点:

  1. Hadoop组件能否被“欺骗”,将流量指向本地代理而不是远程节点。
  2. 如何为非云原生的、多端口的Hadoop服务正确配置Consul服务定义和代理。

我们的实验环境由五台虚拟机组成:

  • consul-server-1, consul-server-2, consul-server-3: Consul集群服务端。
  • hadoop-master: 运行HDFS NameNode和YARN ResourceManager。
  • hadoop-worker-1: 运行HDFS DataNode和YARN NodeManager。
graph TD
    subgraph VM: hadoop-worker-1
        DN[DataNode Process] -- localhost:16001 --> DNP[Envoy Proxy];
        NM[NodeManager Process] -- localhost:16002 --> NMP[Envoy Proxy];
        CAW[Consul Agent];
        DNP -- Manages --> DN;
        NMP -- Manages --> NM;
        CAW -.-> DNP;
        CAW -.-> NMP;
    end

    subgraph VM: hadoop-master
        NN[NameNode Process];
        RM[ResourceManager Process];
        NNP[Envoy Proxy] -- localhost:8020 --> NN;
        RMP[Envoy Proxy] -- localhost:8132 --> RM;
        CAM[Consul Agent];
        NNP -- Manages --> NN;
        RMP -- Manages --> RM;
        CAM -.-> NNP;
        CAM -.-> RMP;
    end

    DNP <== mTLS over Wire ==> NNP;
    NMP <== mTLS over Wire ==> RMP;

    style DN fill:#f9f,stroke:#333,stroke-width:2px;
    style NN fill:#f9f,stroke:#333,stroke-width:2px;
    style NM fill:#ccf,stroke:#333,stroke-width:2px;
    style RM fill:#ccf,stroke:#333,stroke-width:2px;

步骤一:部署Consul集群

这一步相对直接。在三台consul-server节点上,我们创建配置文件 consul.hcl

// /etc/consul.d/consul.hcl on consul-server-1
// 生产环境中,IP地址应为内网固定IP
server = true
bootstrap_expect = 3
client_addr = "0.0.0.0"
bind_addr = "192.168.10.11" // 当前服务器的私有IP
ui_config {
  enabled = true
}
data_dir = "/opt/consul"
enable_local_script_checks = true // 允许本地脚本健康检查

// 用于自动加入集群
retry_join = ["192.168.10.11", "192.168.10.12", "192.168.10.13"]

在三台服务器上分别修改bind_addr后启动Consul:

# 在所有consul-server节点上执行
sudo consul agent -config-file=/etc/consul.d/consul.hcl

通过 consul members 确认集群状态正常。

接下来,在 hadoop-masterhadoop-worker-1 上安装并配置Consul Agent作为客户端。

// /etc/consul.d/consul.hcl on hadoop nodes
client_addr = "0.0.0.0"
bind_addr = "192.168.10.21" // 当前hadoop-master的私有IP
data_dir = "/opt/consul"
enable_local_script_checks = true

// 加入Consul Server集群
retry_join = ["192.168.10.11", "192.168.10.12", "192.168.10.13"]

启动客户端Agent:

sudo consul agent -config-file=/etc/consul.d/consul.hcl

步骤二:为NameNode和ResourceManager注册服务

这是第一个真正的挑战。Hadoop的组件通常监听多个端口,我们需要将这些端口都代理起来。我们选择为每个核心服务(如HDFS-NN, YARN-RM)定义一个独立的Consul服务,而不是将所有服务塞进一个定义。

hadoop-master 节点上,创建服务定义文件 /etc/consul.d/hadoop-master-services.hcl

// /etc/consul.d/hadoop-master-services.hcl
service {
  name = "namenode-rpc"
  id   = "namenode-rpc-1"
  port = 8020 // NameNode RPC的实际监听端口

  // 健康检查至关重要,这里用一个简单的脚本检查进程是否存在
  check {
    id       = "namenode-process-check"
    name     = "Check NameNode Process"
    script   = "pgrep -f 'proc_namenode' > /dev/null"
    interval = "10s"
    timeout  = "2s"
  }

  // 这是Consul Connect的核心配置
  connect {
    sidecar_service {
      // Sidecar代理将监听在18020端口,并将流量转发给本地的8020端口
      port = 18020
      proxy {
        // 配置上游依赖。NameNode需要与DataNode通信
        upstreams {
          destination_name = "datanode-ipc"
          local_bind_port  = 25001 // DataNode IPC服务的本地监听端口
        }
        upstreams {
          destination_name = "datanode-http"
          local_bind_port = 25002 // DataNode HTTP服务的本地监听端口
        }
      }
    }
  }
}

service {
  name = "resourcemanager-scheduler"
  id   = "resourcemanager-scheduler-1"
  port = 8030 // RM Scheduler的实际端口

  check {
    id = "resourcemanager-process-check"
    name = "Check ResourceManager Process"
    script = "pgrep -f 'proc_resourcemanager' > /dev/null"
    interval = "10s"
    timeout = "2s"
  }
  
  connect {
    sidecar_service {
      port = 18030
      // RM不需要主动连接其他服务,所以upstreams为空
    }
  }
}

这里的坑在于:

  1. port 字段必须是Hadoop进程实际监听的端口。
  2. sidecar_service 内部的 port 字段是Envoy代理为该服务监听的入站端口,其他服务的Sidecar将连接到这个端口。
  3. upstreams 定义了当前服务需要访问哪些其他服务。destination_name 是目标Consul服务名,local_bind_port 是在本地为这个上游连接创建的监听端口。Hadoop进程将通过连接 localhost:local_bind_port 来访问目标服务。

重载Consul配置以注册服务:

consul reload

现在,为这两个服务启动Sidecar代理:

# 为NameNode启动代理
consul connect envoy -sidecar-for namenode-rpc-1 -admin-bind localhost:19001 &

# 为ResourceManager启动代理
consul connect envoy -sidecar-for resourcemanager-scheduler-1 -admin-bind localhost:19002 &

-sidecar-for 参数告诉Envoy它在为哪个Consul服务ID工作,Consul会据此动态生成Envoy的配置。

步骤三:为DataNode和NodeManager注册服务

hadoop-worker-1 节点上,创建 /etc/consul.d/hadoop-worker-services.hcl。DataNode是配置中最复杂的部分,因为它既是其他服务(如NameNode)的上游,也需要连接到其他服务(NameNode)。

// /etc/consul.d/hadoop-worker-services.hcl
service {
  // DataNode的IPC服务,供NameNode连接
  name = "datanode-ipc"
  id   = "datanode-ipc-worker-1"
  port = 9867

  check {
    id = "datanode-process-check"
    name = "Check DataNode Process"
    script = "pgrep -f 'proc_datanode' > /dev/null"
    interval = "10s"
    timeout = "2s"
  }
  
  connect {
    sidecar_service {
      port = 19867 // 其他服务连接此代理端口
    }
  }
}

service {
  // DataNode的HTTP服务
  name = "datanode-http"
  id   = "datanode-http-worker-1"
  port = 9864

  check {
    id = "datanode-http-check"
    name = "Check DataNode HTTP Port"
    tcp = "localhost:9864"
    interval = "10s"
    timeout = "2s"
  }
  
  connect {
    sidecar_service {
      port = 19864
    }
  }
}


service {
  // 这是一个"虚拟"服务,仅用于定义DataNode进程的出站连接
  // 它不监听任何端口,只是为了让Consul Connect为DataNode进程管理上游
  name = "datanode-client"
  id   = "datanode-client-worker-1"

  connect {
    sidecar_service {
      proxy {
        upstreams {
          destination_name = "namenode-rpc"
          local_bind_port  = 28020 // DataNode将通过localhost:28020连接NameNode
        }
      }
    }
  }
}

service {
  name = "nodemanager"
  id = "nodemanager-worker-1"
  port = 8041 // NodeManager的实际端口
  
  check {
    id = "nodemanager-process-check"
    name = "Check NodeManager Process"
    script = "pgrep -f 'proc_nodemanager' > /dev/null"
    interval = "10s"
    timeout = "2s"
  }

  connect {
    sidecar_service {
      port = 18041
      proxy {
        upstreams {
          destination_name = "resourcemanager-scheduler"
          local_bind_port = 28030 // NodeManager通过此端口连接RM
        }
      }
    }
  }
}

设计抉择:
我们为DataNode创建了一个名为 datanode-client 的虚拟服务。这是因为DataNode进程本身需要发起对NameNode的出站连接,但它本身没有一个单一的服务身份来承载这个upstreams配置。通过这个虚拟服务,我们可以启动一个专门管理DataNode出站流量的Sidecar。

在worker节点上启动所有代理:

consul reload
consul connect envoy -sidecar-for datanode-ipc-worker-1 &
consul connect envoy -sidecar-for datanode-http-worker-1 &
consul connect envoy -sidecar-for datanode-client-worker-1 &
consul connect envoy -sidecar-for nodemanager-worker-1 &

步骤四:修改Hadoop配置,将流量重定向至Sidecar

这是整个方案中最具侵入性但也是最关键的一步。我们需要修改 core-site.xmlyarn-site.xml,让Hadoop组件连接本地的Sidecar代理端口,而不是直接连接远程服务。

修改 hadoop-master 上的 /opt/hadoop/etc/hadoop/core-site.xml:

<configuration>
    <property>
        <name>fs.defaultFS</name>
        <!-- 
          重要变更: 客户端和组件现在连接到NameNode RPC的本地代理端口。
          注意:这里的地址是NameNode节点自己为namenode-rpc上游配置的本地监听端口。
          然而,由于NameNode本身就是namenode-rpc服务,它不需要通过upstream访问自己。
          这里的配置主要是给集群中的其他客户端使用,例如HDFS CLI。
          为了统一,DataNode将连接它自己本地的代理端口。
        -->
        <value>hdfs://localhost:28020</value> 
        <!-- 假设DataNode上的namenode-rpc上游监听在28020 -->
    </property>
</configuration>

修改 hadoop-worker-1 上的 /opt/hadoop/etc/hadoop/core-site.xml:

<configuration>
    <property>
        <name>fs.defaultFS</name>
        <!-- 
          关键: DataNode通过连接它本地的Sidecar代理来访问NameNode。
          这个端口号(28020)是在datanode-client服务的upstreams中定义的。
        -->
        <value>hdfs://localhost:28020</value>
    </property>
</configuration>

修改 hadoop-worker-1 上的 /opt/hadoop/etc/hadoop/yarn-site.xml:

<configuration>
    <property>
        <name>yarn.resourcemanager.hostname</name>
        <!--
          关键: NodeManager不再直接连接RM的主机名,
          而是连接到为resourcemanager-scheduler上游配置的本地代理端口。
          这个端口号(28030)是在nodemanager服务的upstreams中定义的。
        -->
        <value>localhost</value>
    </property>
    <property>
        <name>yarn.resourcemanager.scheduler.address</name>
        <value>${yarn.resourcemanager.hostname}:28030</value>
    </property>
</configuration>

在修改完所有相关节点的配置后,需要重启HDFS和YARN服务,使新配置生效。

步骤五:配置Consul Intentions并验证

默认情况下,Consul Connect会拒绝所有服务间的通信(零信任网络)。我们需要明确定义哪些服务可以相互通信。

# 允许datanode-client(代表DataNode进程)访问namenode-rpc
consul intention create -allow datanode-client namenode-rpc

# 允许NameNode访问DataNode的IPC和HTTP服务
consul intention create -allow namenode-rpc datanode-ipc
consul intention create -allow namenode-rpc datanode-http

# 允许nodemanager访问resourcemanager-scheduler
consul intention create -allow nodemanager resourcemanager-scheduler

此时,如果一切顺利,Hadoop集群应该能正常工作。在 hadoop-worker-1 上执行 hdfs dfs -ls /,命令应该成功返回。

验证加密:
hadoop-worker-1 上使用 tcpdump 抓取物理网卡上的包:

sudo tcpdump -i eth0 -A host hadoop-master and port 18020

你会看到输出全是加密的TLS流量,没有任何明文的Hadoop RPC协议内容。
同时,使用 lsof 可以验证进程连接:

# 在hadoop-worker-1上
sudo lsof -i -P -n | grep java

输出会显示,DataNode的Java进程正在连接 localhost:28020,而不是 hadoop-master:8020。这证明流量确实经过了本地Sidecar。

现在,如果我们删除一条intention规则:

consul intention delete datanode-client namenode-rpc

稍等片刻,再次执行 hdfs dfs -ls /,命令会卡住然后超时失败。这有力地证明了Consul Connect的mTLS策略正在生效。

遗留问题与未来展望

这个方案成功地为存量的、非容器化的Hadoop集群提供了一层透明的mTLS加密,极大地简化了安全配置。在真实项目中,这种方式避免了对现有Kerberos体系的依赖和改造,显著降低了运维复杂性。

但它并非银弹,还存在一些局限性和需要进一步考虑的问题:

  1. 性能开销:引入两层Envoy代理(源端和目的端)必然会带来额外的网络延迟和CPU消耗。对于RPC密集型和数据密集型的Hadoop作业,需要进行详尽的性能压测来评估其影响。
  2. 生命周期管理:当前我们是手动启动和管理Sidecar进程。在生产环境中,需要使用Systemd或其他进程管理工具来确保Sidecar与对应的Hadoop守护进程同生命周期,并在Hadoop进程启动前确保Sidecar已就绪。
  3. 配置复杂性:虽然避免了Kerberos,但引入了Consul服务定义和Intention配置。对于非常庞大的集群,管理这些HCL文件和Sidecar也需要自动化工具支持。
  4. 覆盖范围:此方案主要覆盖了Hadoop核心组件间的通信。Hadoop生态系统中的其他组件,如Hive Server2, Presto, HBase等,也需要进行类似的改造,工作量不容小觑。
  5. 高可用性:对于HDFS NameNode HA场景,需要更复杂的Consul服务定义和上游配置来处理主备切换,这需要结合Consul的健康检查和DNS功能来实现动态路由。

  目录