HashiCorp Nomadで学ぶ軽量コンテナオーケストレーション


はじめに

コンテナオーケストレーションといえばKubernetesが第一選択肢として挙がるが、全てのプロジェクトにKubernetesが最適とは限らない。学習コストの高さ、運用の複雑さ、リソースオーバーヘッドの大きさに悩むチームは少なくない。

HashiCorp Nomadは、そうした課題に対する有力な代替手段だ。シンプルなアーキテクチャと単一バイナリでのデプロイ、HCL(HashiCorp Configuration Language)による直感的なジョブ定義が特徴で、小〜中規模のインフラにおいて圧倒的な導入しやすさを誇る。

この記事では、Nomadの基本概念からインストール、ジョブ定義、サービスディスカバリ、デプロイ戦略まで、実践的なコード付きで解説する。


Nomadとは何か

Nomadは、HashiCorpが開発するワークロードオーケストレーターだ。Docker、Podman、Java、バイナリ実行ファイルなど、多様なタスクドライバーに対応しており、コンテナに限らず幅広いワークロードをスケジュール・管理できる。

Nomadの主要コンポーネント

Nomadのアーキテクチャは以下の3つのコンポーネントで構成される。

コンポーネント役割
Serverクラスタの制御プレーン。ジョブのスケジューリングと状態管理を担う
Client(Agent)実際にワークロードを実行するノード。Serverからの指示を受けてタスクを起動する
Driverタスクの実行エンジン。Docker、exec、Java、Podmanなどが利用可能

ServerはRaftプロトコルによる合意形成で高可用性を実現する。本番環境では3台または5台のServerノードを推奨する。

Nomadの設計思想

Nomadの設計は「一つのことをうまくやる」Unix哲学に基づいている。オーケストレーション機能に特化し、サービスディスカバリはConsul、シークレット管理はVaultという形で、HashiCorpエコシステムの各ツールと連携する構成を想定している。


KubernetesとNomadの比較

Nomadを検討する上で、Kubernetesとの違いを理解しておくことは重要だ。以下に主要な比較項目をまとめる。

比較項目KubernetesNomad
アーキテクチャ複数コンポーネント(etcd, API Server, Controller Manager, Scheduler, kubelet)単一バイナリ(Server/Clientモード)
学習コスト高い(CRD, Operator, RBAC等の概念が多い)低い(HCLベースのシンプルな定義)
セットアップ時間数時間〜数日(マネージドサービス利用時は短縮可能)数分〜数十分
対応ワークロードコンテナ中心コンテナ、VM、バイナリ、Java等
スケーラビリティ数千ノード数万ノード(公式ベンチマーク)
サービスメッシュIstio, Linkerd等Consul Connect(組み込み)
シークレット管理Kubernetes SecretsVault連携
コミュニティ規模非常に大きい中規模だが成長中
マネージドサービスEKS, GKE, AKS等HCP Nomad
リソース消費比較的大きい軽量

Nomadが適しているケース

以下のようなケースでは、Nomadの採用が有効だ。

  • チームが小規模でKubernetesの運用負荷を許容できない
  • コンテナ以外のワークロード(バッチ処理、レガシーアプリ)も管理したい
  • HashiCorpエコシステム(Consul, Vault, Terraform)を既に利用している
  • 段階的にオーケストレーションを導入したい
  • オンプレミスとクラウドのハイブリッド環境で統一的に管理したい

Kubernetesが適しているケース

一方で、以下の場合はKubernetesの方が適している。

  • 大規模なマイクロサービスアーキテクチャを構築している
  • クラウドベンダーのマネージドサービスを前提にできる
  • Kubernetesエコシステムの豊富なツール群を活用したい
  • チームにKubernetes運用の経験者がいる

Nomadのインストール

Nomadは単一バイナリで提供されるため、インストールは非常に簡単だ。

Linux(Ubuntu/Debian)の場合

# HashiCorpのGPGキーとリポジトリを追加
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

# インストール
sudo apt-get update && sudo apt-get install nomad

# バージョン確認
nomad version

macOS(Homebrew)の場合

brew tap hashicorp/tap
brew install hashicorp/tap/nomad

nomad version

開発モードでの起動

ローカル開発やテストには、開発モード(dev mode)が便利だ。永続化なしのシングルノードクラスタが即座に起動する。

# 開発モードで起動
sudo nomad agent -dev

# 別ターミナルでノード状態を確認
nomad node status
ID        DC   Name       Class   Drain  Eligibility  Status
a1b2c3d4  dc1  localhost  <none>  false  eligible     ready

ジョブ定義の基本

Nomadのジョブ定義はHCL(HashiCorp Configuration Language)で記述する。ジョブ定義は階層構造を持ち、以下のような構成になっている。

Job
 └── Group(タスクグループ)
      └── Task(個別のタスク)

最初のジョブ定義

Nginxを動かすシンプルなジョブ定義を作成する。

# nginx.nomad.hcl
job "nginx-web" {
  datacenters = ["dc1"]
  type        = "service"

  group "web" {
    count = 3

    network {
      port "http" {
        static = 0
        to     = 80
      }
    }

    task "nginx" {
      driver = "docker"

      config {
        image = "nginx:1.27-alpine"
        ports = ["http"]
      }

      resources {
        cpu    = 200
        memory = 128
      }

      service {
        name = "nginx-web"
        port = "http"

        check {
          type     = "http"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }
    }
  }
}

ジョブの実行と管理

# ジョブの実行計画を確認(ドライラン)
nomad job plan nginx.nomad.hcl

# ジョブを実行
nomad job run nginx.nomad.hcl

# ジョブの状態を確認
nomad job status nginx-web

# アロケーション(配置)の一覧
nomad job allocs nginx-web

# 特定のアロケーションのログを確認
nomad alloc logs <alloc-id>

# ジョブの停止
nomad job stop nginx-web

ジョブタイプの理解

Nomadには3つのジョブタイプがあり、ワークロードの性質に応じて使い分ける。

serviceタイプ

長時間稼働するサービスに使用する。Webアプリケーション、APIサーバー、データベースなどが該当する。失敗時には自動的に再スケジュールされる。

job "api-server" {
  datacenters = ["dc1"]
  type        = "service"

  group "api" {
    count = 3

    restart {
      attempts = 5
      interval = "30m"
      delay    = "15s"
      mode     = "fail"
    }

    reschedule {
      attempts       = 10
      interval       = "24h"
      delay          = "30s"
      delay_function = "exponential"
      max_delay      = "1h"
      unlimited      = false
    }

    network {
      port "http" {
        to = 8080
      }
    }

    task "api" {
      driver = "docker"

      config {
        image = "myapp/api:v1.2.0"
        ports = ["http"]
      }

      env {
        APP_ENV  = "production"
        LOG_LEVEL = "info"
      }

      resources {
        cpu    = 500
        memory = 256
      }
    }
  }
}

batchタイプ

1回限りまたは定期的なバッチ処理に使用する。タスクが正常終了すれば完了とみなされる。

job "data-migration" {
  datacenters = ["dc1"]
  type        = "batch"

  group "migrate" {
    task "run-migration" {
      driver = "docker"

      config {
        image   = "myapp/migrator:v1.0.0"
        command = "/bin/sh"
        args    = ["-c", "python migrate.py --target production"]
      }

      resources {
        cpu    = 1000
        memory = 512
      }
    }
  }
}

定期実行バッチ(Periodic)

cronライクなスケジュール定義で定期実行するバッチジョブも定義できる。

job "nightly-report" {
  datacenters = ["dc1"]
  type        = "batch"

  periodic {
    cron             = "0 2 * * *"
    prohibit_overlap = true
    time_zone        = "Asia/Tokyo"
  }

  group "report" {
    task "generate" {
      driver = "docker"

      config {
        image = "myapp/reporter:v2.1.0"
        args  = ["--format", "pdf", "--output", "/data/reports/"]
      }

      resources {
        cpu    = 500
        memory = 256
      }

      volume_mount {
        volume      = "reports"
        destination = "/data/reports"
      }
    }

    volume "reports" {
      type   = "host"
      source = "reports-volume"
    }
  }
}

systemタイプ

クラスタの全ノード(または条件に合致するノード)で1つずつ実行するデーモン型のジョブに使用する。ログ収集エージェントや監視エージェントの配置に適している。

job "log-collector" {
  datacenters = ["dc1"]
  type        = "system"

  group "logging" {
    task "fluentbit" {
      driver = "docker"

      config {
        image        = "fluent/fluent-bit:3.0"
        network_mode = "host"
        volumes      = [
          "/var/log:/var/log:ro",
          "local/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf"
        ]
      }

      template {
        data = <<-EOF
          [SERVICE]
              Flush        5
              Daemon       Off
              Log_Level    info

          [INPUT]
              Name         tail
              Path         /var/log/nomad/*.log
              Tag          nomad.*

          [OUTPUT]
              Name         forward
              Match        *
              Host         log-aggregator.service.consul
              Port         24224
        EOF

        destination = "local/fluent-bit.conf"
      }

      resources {
        cpu    = 100
        memory = 64
      }
    }
  }
}

テンプレート機能とConsul連携

Nomadのtemplate stanzaは、Consul KVストアやVaultからの値をタスク内のファイルに動的に注入する強力な機能だ。consul-templateの構文をそのまま利用できる。

Consul KVからの設定注入

job "webapp" {
  datacenters = ["dc1"]
  type        = "service"

  group "app" {
    count = 2

    network {
      port "http" {
        to = 3000
      }
    }

    task "node-app" {
      driver = "docker"

      config {
        image = "myapp/webapp:v3.0.0"
        ports = ["http"]
      }

      template {
        data = <<-EOF
          DATABASE_URL={{ key "config/webapp/database_url" }}
          REDIS_URL={{ key "config/webapp/redis_url" }}
          {{ range service "postgres" }}
          DB_HOST={{ .Address }}
          DB_PORT={{ .Port }}
          {{ end }}
        EOF

        destination = "secrets/app.env"
        env         = true
        change_mode = "restart"
      }

      resources {
        cpu    = 300
        memory = 256
      }
    }
  }
}

Vault連携によるシークレット管理

job "secure-app" {
  datacenters = ["dc1"]
  type        = "service"

  group "app" {
    task "api" {
      driver = "docker"

      config {
        image = "myapp/secure-api:v1.0.0"
      }

      vault {
        policies = ["app-secrets"]
      }

      template {
        data = <<-EOF
          {{ with secret "secret/data/app/database" }}
          DB_USERNAME={{ .Data.data.username }}
          DB_PASSWORD={{ .Data.data.password }}
          {{ end }}
          {{ with secret "secret/data/app/api-keys" }}
          STRIPE_KEY={{ .Data.data.stripe_key }}
          {{ end }}
        EOF

        destination = "secrets/credentials.env"
        env         = true
        change_mode = "restart"
      }

      resources {
        cpu    = 500
        memory = 256
      }
    }
  }
}

サービスディスカバリ

Nomadは単体でもシンプルなサービスディスカバリ機能を持つが、Consulと連携することでより強力なサービスディスカバリを実現できる。

Nomad Native Service Discovery

Nomad 1.3以降、Consul不要のネイティブサービスディスカバリが利用可能になった。

job "frontend" {
  datacenters = ["dc1"]
  type        = "service"

  group "web" {
    count = 2

    network {
      port "http" {
        to = 80
      }
    }

    service {
      name     = "frontend"
      provider = "nomad"
      port     = "http"

      check {
        type     = "http"
        path     = "/health"
        interval = "10s"
        timeout  = "2s"
      }
    }

    task "nginx" {
      driver = "docker"

      config {
        image = "myapp/frontend:v2.0.0"
        ports = ["http"]
      }

      template {
        data = <<-EOF
          upstream backend {
            {{ range nomadService "backend-api" }}
            server {{ .Address }}:{{ .Port }};
            {{ end }}
          }

          server {
            listen 80;
            location /api/ {
              proxy_pass http://backend;
            }
          }
        EOF

        destination = "local/nginx.conf"
        change_mode = "signal"
        change_signal = "SIGHUP"
      }

      resources {
        cpu    = 200
        memory = 128
      }
    }
  }
}

Consul連携によるサービスディスカバリ

ConsulをプロバイダーとしたDNSベースのサービスディスカバリを利用する場合は以下のように定義する。

service {
  name     = "backend-api"
  provider = "consul"
  port     = "http"
  tags     = ["v2", "production"]

  check {
    type     = "http"
    path     = "/health"
    interval = "10s"
    timeout  = "3s"
  }

  connect {
    sidecar_service {
      proxy {
        upstreams {
          destination_name = "postgres"
          local_bind_port  = 5432
        }
        upstreams {
          destination_name = "redis"
          local_bind_port  = 6379
        }
      }
    }
  }
}

この構成では、Consul Connectによるサービスメッシュが有効になり、mTLSによるサービス間通信の暗号化と、Intentionベースのアクセス制御が自動的に適用される。


ネットワーキング

Nomadのネットワーキングは柔軟で、用途に応じた構成が可能だ。

ポートマッピング

group "multi-port" {
  network {
    # 動的ポート割り当て
    port "http" {
      to = 8080
    }
    # 静的ポート割り当て
    port "metrics" {
      static = 9090
      to     = 9090
    }
  }

  task "app" {
    driver = "docker"

    config {
      image = "myapp/server:latest"
      ports = ["http", "metrics"]
    }

    # 環境変数でポート番号を参照
    env {
      HTTP_PORT    = "${NOMAD_PORT_http}"
      METRICS_PORT = "${NOMAD_PORT_metrics}"
      HOST_IP      = "${NOMAD_IP_http}"
    }
  }
}

Consul Connectによるサービスメッシュ

group "mesh-app" {
  network {
    mode = "bridge"

    port "http" {
      to = 8080
    }
  }

  service {
    name = "mesh-app"
    port = "8080"

    connect {
      sidecar_service {
        proxy {
          upstreams {
            destination_name = "database"
            local_bind_port  = 5432
          }
        }
      }
    }
  }

  task "app" {
    driver = "docker"

    config {
      image = "myapp/mesh-app:v1.0.0"
    }

    env {
      DB_HOST = "127.0.0.1"
      DB_PORT = "5432"
    }

    resources {
      cpu    = 300
      memory = 256
    }
  }
}

bridgeモードを使用すると、各タスクグループに専用のネットワーク名前空間が作成され、Envoyサイドカープロキシを経由した安全な通信が実現する。


オートスケーリング

Nomad Autoscalerを使用すると、メトリクスに基づいたジョブのオートスケーリングが可能になる。

Autoscalerのインストールと設定

# Nomad Autoscalerのダウンロード
wget https://releases.hashicorp.com/nomad-autoscaler/0.4.5/nomad-autoscaler_0.4.5_linux_amd64.zip
unzip nomad-autoscaler_0.4.5_linux_amd64.zip
sudo mv nomad-autoscaler /usr/local/bin/

Autoscaler設定ファイル

# autoscaler.hcl
nomad {
  address = "http://127.0.0.1:4646"
}

apm "prometheus" {
  driver = "prometheus"
  config = {
    address = "http://prometheus.service.consul:9090"
  }
}

strategy "target-value" {
  driver = "target-value"
}

スケーリングポリシー付きジョブ

job "scalable-api" {
  datacenters = ["dc1"]
  type        = "service"

  group "api" {
    count = 2

    scaling {
      enabled = true
      min     = 2
      max     = 10

      policy {
        evaluation_interval = "30s"
        cooldown            = "2m"

        check "cpu_utilization" {
          source = "prometheus"
          query  = "avg(nomad_client_allocs_cpu_total_percent{task='api-server'})"

          strategy "target-value" {
            target = 70
          }
        }

        check "request_rate" {
          source = "prometheus"
          query  = "sum(rate(http_requests_total{job='scalable-api'}[5m]))"

          strategy "target-value" {
            target = 1000
          }
        }
      }
    }

    network {
      port "http" {
        to = 8080
      }
    }

    task "api-server" {
      driver = "docker"

      config {
        image = "myapp/api:v2.0.0"
        ports = ["http"]
      }

      resources {
        cpu    = 500
        memory = 256
      }
    }
  }
}

デプロイ戦略

Nomadはupdateスタンザでデプロイ戦略を細かく制御できる。

ローリングアップデート

job "rolling-app" {
  datacenters = ["dc1"]
  type        = "service"

  update {
    max_parallel     = 1
    min_healthy_time = "30s"
    healthy_deadline = "5m"
    auto_revert      = true
    canary           = 0
  }

  group "app" {
    count = 6

    task "web" {
      driver = "docker"

      config {
        image = "myapp/web:v2.1.0"
      }

      resources {
        cpu    = 300
        memory = 256
      }
    }
  }
}

この設定では、一度に1つのアロケーションずつ更新し、新しいアロケーションが30秒以上健全な状態を維持してから次に進む。5分以内に健全にならない場合はデプロイが失敗し、auto_revertによって自動的にロールバックされる。

カナリアデプロイ

job "canary-app" {
  datacenters = ["dc1"]
  type        = "service"

  update {
    max_parallel     = 1
    min_healthy_time = "30s"
    healthy_deadline = "5m"
    auto_revert      = true
    canary           = 2
    auto_promote     = false
  }

  group "app" {
    count = 6

    task "web" {
      driver = "docker"

      config {
        image = "myapp/web:v3.0.0"
      }

      resources {
        cpu    = 300
        memory = 256
      }
    }
  }
}

カナリアデプロイでは、まず2つのカナリアアロケーションが作成される。手動でプロモートするまで残りのアロケーションは更新されない。

# カナリアの状態を確認
nomad job status canary-app

# 問題がなければプロモート
nomad deployment promote <deployment-id>

# 問題があればロールバック
nomad deployment fail <deployment-id>

Blue/Greenデプロイ

Nomadには組み込みのBlue/Greenデプロイはないが、カナリア数をグループのcountと同数に設定することで実質的なBlue/Greenデプロイを実現できる。

update {
  max_parallel     = 6
  canary           = 6
  min_healthy_time = "30s"
  healthy_deadline = "5m"
  auto_revert      = true
  auto_promote     = false
}

group "app" {
  count = 6
  # ...
}

この構成では、新バージョンの6インスタンス(Green)が既存の6インスタンス(Blue)と並行して起動する。プロモート後にBlue側が停止される。


マルチリージョン構成

Nomadはマルチリージョン・マルチデータセンターに対応しており、地理的に分散した環境でも統一的な管理が可能だ。

job "global-api" {
  datacenters = ["dc1", "dc2"]
  type        = "service"

  multiregion {
    strategy {
      max_parallel = 1
      on_failure   = "fail_all"
    }

    region "asia" {
      count       = 3
      datacenters = ["tokyo-dc1", "osaka-dc1"]
    }

    region "us" {
      count       = 3
      datacenters = ["us-east-1", "us-west-2"]
    }
  }

  group "api" {
    count = 3

    network {
      port "http" {
        to = 8080
      }
    }

    task "server" {
      driver = "docker"

      config {
        image = "myapp/global-api:v1.0.0"
        ports = ["http"]
      }

      resources {
        cpu    = 500
        memory = 512
      }
    }
  }
}

制約とアフィニティ

Nomadでは、ジョブを特定のノードに配置するための制約(constraint)と、配置優先度を指定するアフィニティ(affinity)を定義できる。

job "gpu-training" {
  datacenters = ["dc1"]
  type        = "batch"

  # ハード制約: GPUを持つノードにのみ配置
  constraint {
    attribute = "${attr.unique.gpu.model}"
    operator  = "regexp"
    value     = "A100|H100"
  }

  # ソフト制約: メモリ64GB以上のノードを優先
  affinity {
    attribute = "${attr.memory.totalbytes}"
    operator  = ">="
    value     = "68719476736"
    weight    = 80
  }

  group "training" {
    task "train-model" {
      driver = "docker"

      config {
        image = "myapp/ml-trainer:v1.0.0"
      }

      resources {
        cpu    = 4000
        memory = 32768

        device "nvidia/gpu" {
          count = 1
        }
      }
    }
  }
}

本番運用のベストプラクティス

Nomadサーバー設定(本番用)

# /etc/nomad.d/server.hcl
datacenter = "dc1"
data_dir   = "/opt/nomad/data"

server {
  enabled          = true
  bootstrap_expect = 3

  server_join {
    retry_join = [
      "nomad-server-1.internal:4648",
      "nomad-server-2.internal:4648",
      "nomad-server-3.internal:4648"
    ]
  }

  encrypt = "YOUR_GOSSIP_KEY_HERE"
}

tls {
  http = true
  rpc  = true

  ca_file   = "/etc/nomad.d/tls/ca.pem"
  cert_file = "/etc/nomad.d/tls/server.pem"
  key_file  = "/etc/nomad.d/tls/server-key.pem"

  verify_server_hostname = true
}

acl {
  enabled = true
}

telemetry {
  prometheus_metrics         = true
  publish_allocation_metrics = true
  publish_node_metrics       = true
}

ACLポリシーの設定

# deploy-policy.hcl
namespace "production" {
  policy = "read"

  capabilities = [
    "submit-job",
    "read-job",
    "list-jobs",
    "read-logs",
    "read-fs"
  ]
}

namespace "staging" {
  policy = "write"
}
# ACLブートストラップ
nomad acl bootstrap

# ポリシーの作成
nomad acl policy apply deploy-policy deploy-policy.hcl

# トークンの作成
nomad acl token create -name="deploy-token" -policy="deploy-policy"

監視とアラート

# Prometheusスクレイプ設定
# prometheus.yml
scrape_configs:
  - job_name: 'nomad'
    consul_sd_configs:
      - server: 'consul.service.consul:8500'
        services: ['nomad-client', 'nomad']
    metrics_path: '/v1/metrics'
    params:
      format: ['prometheus']
    relabel_configs:
      - source_labels: ['__meta_consul_tags']
        regex: '(.*)http(.*)'
        action: keep

Terraformとの連携

NomadジョブをTerraformで管理することで、インフラとアプリケーションのデプロイを統一的にコード管理できる。

# main.tf
terraform {
  required_providers {
    nomad = {
      source  = "hashicorp/nomad"
      version = "~> 2.0"
    }
  }
}

provider "nomad" {
  address = "https://nomad.example.com:4646"
}

resource "nomad_job" "webapp" {
  jobspec = file("${path.module}/jobs/webapp.nomad.hcl")

  hcl2 {
    enabled = true
    vars = {
      image_tag = var.webapp_version
      replicas  = var.webapp_replicas
    }
  }
}

resource "nomad_namespace" "production" {
  name        = "production"
  description = "Production workloads"
}

variable "webapp_version" {
  type    = string
  default = "v1.0.0"
}

variable "webapp_replicas" {
  type    = number
  default = 3
}

まとめ

HashiCorp Nomadは、Kubernetesの複雑さを許容できない、あるいは必要としないチームにとって優れた選択肢だ。単一バイナリでの導入、HCLによる直感的なジョブ定義、コンテナ以外のワークロードへの対応、そしてHashiCorpエコシステムとのシームレスな連携が大きな強みとなる。

本記事で紹介した内容を振り返る。

  • 基本概念: Server/Client/Driverの3コンポーネントアーキテクチャ
  • ジョブタイプ: service、batch、systemの3種類を用途に応じて使い分ける
  • サービスディスカバリ: Nomadネイティブまたは Consul連携で実現
  • デプロイ戦略: ローリング、カナリア、Blue/Greenを標準機能で実現
  • オートスケーリング: Nomad Autoscalerでメトリクスベースのスケーリング
  • 本番運用: TLS、ACL、Telemetryの設定が重要

まずは開発モードでNomadを起動し、シンプルなジョブ定義から試してみることを推奨する。Kubernetesでは過剰だと感じるプロジェクトにこそ、Nomadの真価が発揮される。