Nomad 系列 -Nomad 网络模式

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
传统型负载均衡 CLB,每月750个小时 15LCU
云数据库 Tair(兼容Redis),内存型 2GB
简介: Nomad 系列 -Nomad 网络模式

概述

Nomad 的网络和 Docker 的也有很大不同, 和 K8s 的有很大不同. 另外, Nomad 不同版本 (Nomad 1.3 版本前后) 或是否集成 Consul 及 CNI 等不同组件也会导致网络模式各不相同. 本文详细梳理一下 Nomad 的主要几种网络模式

在 Nomad 1.3 发布之前,它自身并不支持发现集群中运行的其他应用程序。在集群中调度任务时,这是一个非常基本的要求。Nomad 依赖于 Consul 来发现其他“服务”,并为注册和获取服务记录提供一流的支持,这使得事情变得更容易。Consul 通过各种机制提供记录,例如 REST API,DNS 和 Consul 模板,这些模板在可以注入到应用程序中的 Go 模板中呈现服务的确切 IP/ 端口。

学习 Nomad 的一个难点在于, Nomad 往往和 Consul 一起运行, 那么对于这种情况来说,一个主要的学习曲线是,我们必须首先了解 Consul 是如何工作的,部署一个 Consul 集群, 同时要融会贯通 2 个软件就很难了。Nomad 1.3 解决了这个问题的一部分(即不需要运行 Consul 就可以进行基本的服务发现),非常适合刚刚开始使用基于 Nomad 的网络。

场景一: 在主机上公开应用

Host Dynamic Port

从最简单的用例开始:你有一个 redis 容器,你想把它暴露给主机。 相当于我们想要做的 docker run 是 :

docker run --rm -p=6379 redis
BASH

此命令公开主机上的 动态 端口。要查看端口号到底是什么,您可以执行 docker ps 并在 PORTS 下找到类似于 0.0.0.0:49153->6379/tcp 的输出。

$ redis-cli -p 49153                
127.0.0.1:49153> ping
PONG
SHELL

那么, 在 Nomad 中相同的操作如何实现?

job "redis" {
  type        = "service"
  group "redis" {
    network {
      mode = "host"
      port "redis" {
        to = 6379
      }
    }
    task "redis" {
      driver = "docker"
      config {
        image = "redis"
        ports = ["redis"]
      }
    }
  }
}
HCL

在几行配置中,我们有一个正在运行的 Docker 容器,它公开了一个动态端口 30627:

Nomad Redis Job Map Port

我们可以通过主机上的 redis-cli 连接到它:

$ redis-cli -p 30627
127.0.0.1:30627> ping
PONG
SHELL

🐾Warning

task.config 部分中有 ports 很重要。Nomad 将此信息传递给主机上运行的 docker 守护进程。因此,除非您指定在容器中通告哪些端口,否则它不会知道是否要公开 6379。

暴露静态端口

一种不太常见的情况是将应用程序绑定到主机上的静态端口, 只需在 port 块中添加一个 static 行:

network {
  port "redis" {
    static = 6379
  }
}
HCL

Host Static Port

当我们再次部署相同的文件时,我们可以看到端口分配已经从动态端口更改为我们分配的静态端口。但是注意需要确保没有其他应用程序侦听同一接口和端口,否则必然会导致冲突。

静态端口典型的使用场景就是: Ingress. 比如 Traefik 可以使用静态端口监听 80 和 443.

场景二: 与同一 Group 内的 Redis 通信

对于这个场景,我们假设有一个应用程序需要与 Redis 通信。在这个场景中,Redis 用途是临时缓存,所以可以将它们部署在同一个 Group 中。

一个 Group 可以包含多个 Task。这里需要知道的重要一点是,同一 Group 将始终具有自己的 共享网络命名空间(类似 K8s 中 Pod 中的多个 Container 具有共享网络命名空间)。这意味着,如果您在组中有 2 个 Task,则它们都可以访问相同的网络命名空间。这允许两个 Task 在同一网络接口上相互通信。

job "hello" {
  type        = "service"
  group "app" {
    network {
      mode = "host"
      port "app" {
        static = 8080
      }
      port "redis" {
        static = 6379
      }
    }
    task "redis" {
      driver = "docker"
      config {
        network_mode = "host"
        image        = "redis"
        ports        = ["redis"]
      }
    }
    task "app" {
      driver = "docker"
      env {
        DEMO_REDIS_ADDR = "${NOMAD_ADDR_redis}"
      }
      config {
        network_mode = "host"
        image        = "mrkaran/hello-app:1.0.0"
        ports        = ["app"]
      }
    }
  }
}
HCL

详细说明如下:

  • 您可以看到我们在同一 Group 下定义了 task app 和 task redis 。这意味着 Nomad 将在 同一客户端 上共同定位这两个 Task(因为它们不仅倾向于共享相同的网络命名空间,而且还共享公共分配目录 - 这使得跨任务共享文件变得非常容易)。
  • 我们使用 NOMAD_ADDR_redis 来获取 redis task 的 IP:Port 组合。这在运行时由 Nomad 注入。您可以在 这里 找到运行时环境变量的列表。
  • 这是快速测试 / 开发设置的理想选择,因为您不希望服务发现等问题,并且希望以最小的代价连接到您的应用程序。

如果您要从基于 docker-compose 的环境迁移,以上配置非常适合 (但是实现还是不同, Nomad 利用了主机网络),您可以将此模板用于您的服务。这种方法的最大限制是它使用 主机网络

场景三: 跨不同的 Group 进行通信

如上所述, 如果您有相关的 Task(如init task,您希望在 task 开始前获取文件),同一个 Group 很有用(类似 K8s Pod 的 init container)。但是使用 group 的缺点是您不能独立地扩展 task。在上面的例子中,我们将 Redis 和 App 放在同一个 Group 中,但这意味着如果你增加同一个 Group 的 count 来扩展 app,你最终也会扩展 Redis 容器。这是不可取的,因为 Redis 可能不需要与应用程序成比例地扩展。

创建多个 Group 的方法是将任务拆分到各自的组中:

job "hello" {
  type        = "service"
  group "app" {
    count = 1
    network {
      mode = "host"
      port "app" {
        static = 8080
      }
    }
    task "app" {
      driver = "docker"
      env {
        DEMO_REDIS_ADDR = "localhost:6379"
      }
      config {
        image = "mrkaran/hello-app:1.0.0"
        ports = ["app"]
      }
    }
  }
  group "redis" {
    count = 1
    network {
      mode = "host"
      port "redis" {
        static = 6379
      }
    }
    task "redis" {
      driver = "docker"
      config {
        image = "redis"
        ports = ["redis"]
      }
    }
  }
}
HCL

提交此 Job 后,您将获得 2 个分配 ID(每个 Group 会创建一个 alloc )。这里的关键点是这两个 Group 都有自己的网络命名空间。因此,我们实际上没有任何方法可以访问其他应用程序(我们不能向上面这样依赖主机网络,因为无法保证这两个 Group 都部署在同一个节点上)。

现在由于组是分开的, app 容器不知道 redis (反之亦然):

env | grep NOMAD
NOMAD_REGION=global
NOMAD_CPU_LIMIT=4700
NOMAD_IP_app=127.0.0.1
NOMAD_JOB_ID=hello
NOMAD_TASK_NAME=app
NOMAD_SECRETS_DIR=/secrets
NOMAD_CPU_CORES=1
NOMAD_NAMESPACE=default
NOMAD_ALLOC_INDEX=0
NOMAD_ALLOC_DIR=/alloc
NOMAD_JOB_NAME=hello
NOMAD_HOST_IP_app=127.0.0.1
NOMAD_SHORT_ALLOC_ID=a9da72dc
NOMAD_DC=dc1
NOMAD_ALLOC_NAME=hello.app[0]
NOMAD_PORT_app=8080
NOMAD_GROUP_NAME=app
NOMAD_PARENT_CGROUP=nomad.slice
NOMAD_TASK_DIR=/local
NOMAD_HOST_PORT_app=8080
NOMAD_MEMORY_LIMIT=512
NOMAD_ADDR_app=127.0.0.1:8080
NOMAD_ALLOC_PORT_app=8080
NOMAD_ALLOC_ID=a9da72dc-94fc-6315-bb37-63cbeef153b9
NOMAD_HOST_ADDR_app=127.0.0.1:8080
SHELL

服务发现

app Group 需要在连接到 redis 之前发现它。有多种方法可以做到这一点,但我们将介绍两种更常见的标准方法。

使用 Nomad Native Service Discovery

Nomad Native Service Discovery

这是在 Nomad 1.3 中推出的功能。在这次发布之前,Nomad 不得不依靠 Consul 来完成这一任务。但是有了 Nomad 中内置的原生服务发现,事情就简单多了。让我们对作业文件进行以下更改。在每个 Group 中,我们将添加一个 service 定义:

group "app" {
  count = 1
  network {
    mode = "host"
    port "app" {
      to = 8080
    }
  }
  service {
    name     = "app"
    provider = "nomad"
    port     = "app"
  }
  // task is the same
}
group "redis" {
  count = 1
  network {
    mode = "host"
    port "redis" {
      to = 6379
    }
  }
  service {
    name     = "redis"
    provider = "nomad"
    port     = "redis"
  }
  // task is the same
}
HCL

如上,我们添加了一个新的 service 块,并删除了 static 端口。当我们使用服务发现时,不需要绑定到静态端口

提交作业后,我们可以使用 nomad service list 命令确保服务已注册到 Nomad。

nomad service list    
Service Name  Tags
app           []
redis         []
SHELL

要了解特定服务的详细信息,我们可以使用 nomad service info

$ nomad service info app      
Job ID  Address          Tags  Node ID   Alloc ID
hello   127.0.0.1:29948  []    d92224a5  5f2ac51f
$ nomad service info redis
Job ID  Address          Tags  Node ID   Alloc ID
hello   127.0.0.1:22300  []    d92224a5  8078c9a6
SHELL

如上, 我们可以看到每个服务中的动态端口分配。要在我们的应用程序中使用此配置,我们将其模板化:

task "app" {
      driver = "docker"
      template {
        data = <<EOH
{{ range nomadService "redis" }}
DEMO_REDIS_ADDR={{ .Address }}:{{ .Port }}
{{ end }}
EOH
        destination = "secrets/config.env"
        env         = true
      }
      config {
        image = "mrkaran/hello-app:1.0.0"
        ports = ["app"]
      }
    }
HCL

我们添加了 template 节,它将在容器中插入环境变量。我们遍历 nomadService 并获取 redis 服务的地址和端口。这使得其他节点上的任务可以方便地发现彼此。

使用 Consul 服务发现

Consul Service Discovery

只需调整 service 块中的 provider ,我们就可以使用 Consul 代理进行服务发现。

service {
      name     = "app"
      provider = "consul"
      port     = "app"
    }
    task "app" {
      driver = "docker"
      template {
        data = <<EOH
{{ range service "redis" }}
DEMO_REDIS_ADDR={{ .Address }}:{{ .Port }}
{{ end }}
EOH
HCL

🐾Warning:

注意 range nomadService 也改为了 range service

前提是要确保正在运行 Consul 并已将 Nomad 连接到它。具体请参阅 该文档

其余的事情几乎保持不变。只用 两行代码 就可以在 Nomad/Consul 之间切换来发现服务。

另外, 使用 Consul 会有更多的 优势

  • 可以使用 DNS 查询服务的地址:
doggo redis.service.consul @tcp://127.0.0.1:8600
NAME                    TYPE    CLASS   TTL ADDRESS     NAMESERVER     
redis.service.consul.   A       IN      0s  172.20.10.3 127.0.0.1:8600  
SHELL
  • 可由 Nomad 以外的应用程序访问。如果 consul 被 Nomad 集群外的其他应用程序使用,它们仍然可以获得对应的地址(使用 DNS 或 REST API)

当然,Nomad Native Service Discovery 非常适合本地 / 边缘环境设置,甚至是生产中的较小用例,因为它不再需要 Consul!

场景四: 限制对某些 Namespace 的访问

Consul Service Mesh

在上述所有场景中,我们发现服务会暴露给本地 Nomad 客户端。如果您在集群上运行多个 Namespace,您可能希望根本不公开它们。此外,您可能希望表达应用程序可以访问特定服务的细粒度控制。所有这些都可以通过 服务网格 实现。Nomad 提供了一种通过 Consul Connect 建立“服务网格”的方法。Consul Connect 可以进行 mTLS 和服务授权。在引擎盖下,它是一个与您的应用程序一起运行的 Envoy 代理(或 sidecar)。 Consul 代理为您配置 Envoy 配置,因此这一切都非常无缝。

要做到这一点,我们首先需要的是 bridge 网络模式。此网络模式实际上是一个 CNI 插件,需要在 /opt/cni/bin 中单独安装。按照这里提到的步骤:

network {
  mode = "bridge"
  port "redis" {
    to = 6379
  }
}
HCL

Redis 中的服务被 Consul Connect Ingress 所调用:

service {
  name     = "redis"
  provider = "consul"
  port     = "6379"
  connect {
    sidecar_service {}
  }
}
HCL

这是一个空块,因为我们不需要在这里定义任何上游。其余值将为默认值。

接下来,我们为 app 创建一个服务,这是一个 Consul Connect Egress:

service {
  name     = "app"
  provider = "consul"
  port     = "app"
  connect {
    sidecar_service {
      proxy {
        upstreams {
          destination_name = "redis"
          local_bind_port  = 6379
        }
      }
    }
  }
}
HCL

这里我们为 redis 定义一个上游。在这里,当 app 想要与 redis 通信时,它会与 localhost:6379 对话,这是 Envoy sidecar 正在监听的本地端口。我们可以使用 netstat 来验证:

$ netstat -tulpvn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.2:19001         0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:23237           0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:6379          0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::8080                 :::*                    LISTEN      1/./hello.bin
SHELL

流量从这个端口发送到它通告的端口上的另一个 Envoy 代理(并且 Consul 自动配置)。该 Envoy 代理进一步将流量发送到端口 6379 上的 redis 容器。代理流量通过 mTLS 进行安全加密并授权(通过 Consul Intentions - 本文不做介绍)。

场景五: 向最终用户公开服务

LB + Ingress

在第一个场景中,我们讨论了如何使用静态端口。事实证明,如果你想定义一个 Traffic Ingress 服务,它非常有用。与 K8s 不同的是,Nomad 没有任何 Ingress Controller,所以最好的方法是将这些 Web 代理作为 system job 部署在每个节点上(这意味着它可以确保在每个客户端节点上运行),并将它们绑定到静态端口(比如 443/80)。然后,配置 LB 并将所有 Nomad 节点注册为 Target IP,其端口将是您定义的静态端口。这些 Ingress 代理(比如 Traefik/Nginx)可以通过上面提到的任何模式与您的应用程序通信。

📝Notes:

上一篇文章 中, 我们并没有配置 LB 后面对接所有 Traefik.

相反, 我们直接访问某一个特定节点的 Traefik 的 80/443 端口.

通常,您希望为入口代理使用“基于主机”的路由模式来做出路由决策。

例如,如果您有一个指向 ALB 的 a.example.org DNS 记录。现在,当请求到达 ALB 时,它会转发到任何一个 Traefik/NGINX。为了使 NGINX 正确地将流量路由到a service,您可以使用“Host”报头。

总结

这些是我所知道的一些常见的网络模式。由于其中一些概念并不是非常简单,我希望解释有助于带来一些清晰。

关于这个主题还有很多,比如 Consul Gateway 和多种 CNI,它们可以调整集群中的网络的底层细节,但这些都是一些非常高级的主题,超出了本文的范围。后续有机会可以再做展开.

📚️参考文档

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
2月前
|
存储 安全 JavaScript
云计算浪潮中的网络安全之舵探索Node.js中的异步编程模式
【8月更文挑战第27天】在数字化时代的风帆下,云计算如同一片广阔的海洋,承载着企业与个人的数据梦想。然而,这片海洋并非总是风平浪静。随着网络攻击的波涛汹涌,如何确保航行的安全成为了每一个船员必须面对的挑战。本文将探索云计算环境下的网络安全策略,从云服务的本质出发,深入信息安全的核心,揭示如何在云海中找到安全的灯塔。
|
2月前
|
负载均衡 安全 网络协议
|
26天前
|
Kubernetes 安全 Docker
|
1月前
|
Ubuntu Oracle 关系型数据库
Oracle VM VirtualBox之Ubuntu 22.04LTS双网卡网络模式配置
这篇文章是关于如何在Oracle VM VirtualBox中配置Ubuntu 22.04LTS虚拟机双网卡网络模式的详细指南,包括VirtualBox网络概述、双网卡网络模式的配置步骤以及Ubuntu系统网络配置。
50 3
|
2月前
|
安全 网络性能优化 网络安全
别再让网络瓶颈困扰你!掌握这十种交换机接口模式,提升你的网络布局技能
【8月更文挑战第23天】交换机作为网络核心,其接口模式直接影响网络布局与性能。本文介绍了十大常见接口模式及其配置实例,包括基础接入模式、优化布线的干道模式、动态学习相邻交换机VLAN信息的动态中继协议模式、固定分配VLAN的静态接入模式、确保语音优先传输的语音VLAN模式、指定默认VLAN的native模式、增加带宽与可靠性的链路聚合及EtherChannel模式、保障网络安全的端口安全模式以及确保关键业务流畅传输的QoS模式。理解并掌握这些模式对于构建高效稳定的网络至关重要。
37 1
|
2月前
|
负载均衡 安全 网络虚拟化
CCE集群VPC网络模式下几种访问场景
【8月更文挑战第13天】在CCE(Cloud Container Engine)集群的VPC网络模式下,支持多样化的访问场景:容器应用可直接利用VPC访问外部资源;通过ELB负载均衡或VPN/专线配置,实现外部网络对容器应用的安全访问;容器间通过内部IP或服务发现机制相互通信;跨VPC访问则可通过VPC对等连接或云连接服务实现。这些方案确保了应用内外部通信的安全与高效。
|
2月前
|
安全 网络安全 数据处理
防火墙设置难倒你?这两种组网模式轻松解决网络安全难题!
【8月更文挑战第23天】在网络安全日益重要的今天,防火墙作为关键防护设备扮演着重要角色。本文重点分析两种核心组网模式:三层路由网关模式与二层透明网桥模式。前者通过IP层处理实现内外网隔离及丰富的策略配置,增强安全性;后者以MAC地址转发,部署简便,不影响现有网络结构,适合服务不可中断的情况。通过企业升级安全防护的实际案例,展示了不同模式的应用场景及优势,并提供了三层路由网关模式的配置示例。正确选择和配置防火墙组网模式对于提高网络安全性和保证业务连续性至关重要。
47 0
|
2月前
|
Docker 容器
在Docker中,网络模式有哪些?
在Docker中,网络模式有哪些?
|
2月前
|
存储 Kubernetes API
在K8S中,calico工作原理与网络模式是什么?
在K8S中,calico工作原理与网络模式是什么?
|
3月前
|
前端开发 SDN 网络虚拟化
下一篇
无影云桌面