nabeo がピーしているブログ (仮)

どーも、nabeop です

terraform で for_each でリソースを作りつつ、リソース内に Optional な引数を作りたい時

terraform の Google Cloud プロバイダーの google_compute_subnetwork リソースを定義している module で、

  • 1つのサブネットで1つ以上のセカンダリ IP レンジを持つことができる
  • Production 環境と Staging 環境など似た構成で複数のサブネットを作りたい
  • ただし、あるネットワークではセカンダリ IP レンジを持たないサブネットもある

というようなことが想定される場合はこんなふうに書いておくと良さそう。

# modules/foo/variables.tf
variable "subnets" {
  type = map(object({
    ip_cidr_range = string
  }))
}
variable "secondary_ip_ranges" {
  type = map(list(object({
    range_name    = string
    ip_cidr_range = string
  })))
  default = {}
}

サブネットそのもの情報と、サブネット内でオプショナルとなるセカンダリ IP レンジの情報を別の変数として定義しておく。ミソは map として作っているところ。また、secondary_ip_ranges は存在しない場合があるので default = {} として空の map をデフォルト値としている。

# moduels/foo/main.tf
resource "google_compute_subnetwork" "ubnetwork" {
  for_each = var.subnets

...

  ip_cidr_range = each.value.ip_cidr_range

  dynamic secondary_ip_range {
    for_each = contains(keys(var.secondary_ip_ranges), each.key) ? var.secondary_ip_ranges[each.key] : []
    content {
      range_name    = lookup(secondary_ip_range.value, "range_name", null)
      ip_cidr_range = lookup(secondary_ip_range.value, "ip_cidr_range", null)
    }
  }
}

dynamic secodary_ip_range の中で var.secondary_ip_ranges のキーと var.subnet のキーを突き合わせて存在する場合は secondary_ip_range を設定するようにしている。

実際に使う時は以下のようにする。

# main.tf
# セカンダリ IP レンジが存在するサブネット
module "hoge" {
  source = "./modules/foo"

...

  subnets = {
    "production" = {
      ip_cidr_range = "10.244.0.0/20"
    }
    "staging" = {
      ip_cidr_range = "10.244.16.0/20"
    }
  }

  secondary_ip_ranges = {
    "production" = [
      {
        range_name    = "subnet-01"
        ip_cidr_range = "10.244.192.0/20"
      }
      {
        range_name    = "subnet-02"
        ip_cidr_range = "10.244.208.0/20"
      }
    ]
    "staging" = [
      {
        range_name    = "subnet-01"
        ip_cidr_range = "10.244.224.0/20"
      }
      {
        range_name    = "subnet-02"
        ip_cidr_range = "10.244.240.0/20"
      }
    ]
  }
}

# セカンダリ IP レンジが存在しないサブネット
module "fuga" {
  source = "./modules/foo"

...

  subnets = {
    "production" = {
      ip_cidr_range = "10.245.0.0/20"
    }
    "staging" = {
      ip_cidr_range = "10.245.16.0/20"
    }
  }
} 

cdktf diff の内容を詳しく調べる

最近は Terraform を触っているんだけど、AWS-CDK に慣れた身としては HCL を直接書くのは厳しいので、cdktf を試している。cdktf とは AWS-CDK のように Terraform の HCL を Typescript などで記述できるツールです。

github.com

cdktf を使うにあたり厳しさを感じていたのが cdktf diff で変更されるリソースの内容がわかりにくいという問題があった。cdktf の使い方とかは別エントリで書こうと思っていたんだけど、terraform と cdktf を行き来するなかで現状における解決策を見つけたのでメモしておく。

まず、cdktf diff したら cdktf.out ディレクトリが作成される。このディレクトリは terraform init して terraform plan -out plan した内容が出力されている。cdktf.out の中身は以下のようになっている。

cdktf.out
├── .terraform
│   └── plugins
│       ├── registry.terraform.io
│       │   └── hashicorp
│       │       └── google
│       │           └── 3.42.0
│       │               └── darwin_amd64
│       └── selections.json
├── cdk.tf.json
└── plan

cdktf.out/cdk.tf.json は謎だけど、 cdktf.out/plan が今回の本丸。

cdktf.out ディレクトリを working ディレクトリとして、terraform show plan すると terraform plan したときの出力が得られる。

$ cd cdktf.out
$ terraform show plan

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:

Terraform will perform the following actions:

Plan: 0 to add, 0 to change, 0 to destroy.
$

本当は AWS-CDK 並にいい感じに cdktf diff が出力して欲しいところだけど、正式リリース前なのでこんなもんでしょという感じではある。

Terraform で Google Beta API をつかう

2行でまとめると

  • プロバイダは googlegoogle-beta の両方を定義する
  • 必要な時だけ google-beta プロバイダをつかうと宣言する

という感じ。

モノによっては Google Beta API をつかう必要がある。Terraform の Google Cloud 向けプロバイダでは API の種別ごとに googlegoogle-beta というプロバイダが用意されている。googlegoogle-beta については Google Provider Versions - Terraform by HashiCorp で説明されている。

具体的な例は以下のとおり。

2つのプロバイダを定義する。

provider "google" {
  credentials = file("account.json")
  project     = "sample"
  region      = "asia-northeast1"
}

provider "google-beta" {
  credentials = file("account.json")
  project     = "sample"
  region      = "asia-northeast1"
}

shared-vpc-host-projectというプロジェクトの定義では google プロバイダを使用するが、shared-vpc-host-project を共有 VPC として有効にするときは google-beta プロバイダを利用する。

resource "google_project" "shared_vpc_host_project" {
  name       = "shared VPC Host Project"
  project_id = "shared-vpc-host-project"
}

resource "google_compute_shared_vpc_host_project" "shared_vpc_host_project" {
 # フォルダ単位でロールをつけているので beta API をつかう必要がある
  provider = google-beta
  project  = google_project.shared_vpc_host_project.project_id
}

GoBGP で受け取った経路を Quagga (zebra) でカーネル経路に落とし込む

vagrant で k8s クラスタを作った記録では以下のように書いていました。

gobgpd と quagga を使った BGP ルータの構築も https://github.com/nabeo/vagrant-k8s-metallb で構築していますが、ここでは詳しくは触れません。

ということで、今回は gobgpd で受けた BGP 経路を Quagga で kernel の経路表に落とし込んで別サブネットに存在しているクライアントが k8 で構築しているサービスにアクセスできるようにしている部分の解説をしようと思います。Quagga でも BGP を受けれることは承知していますが、GoBGP を使ってみたかったのでこんな構成にしています。

続きを読む

k8s クラスタを気軽に遊べる環境を作っている

k8s のマネージドサービスとして GKE や EKS などで出揃った感はあるけど、マネージドサービスでは k8s クラスタの全ての機能を使うことはできないという認識です。特にネットワークなどインフラよりな低レイヤーな部分はマネージドサービス側でいい感じに隠蔽して、利用者は意識する機会は少ないです。最近は仕事でも k8s など docker コンテナ基盤を扱う機会が増えてきていますが、インフラ屋さんとしてはこのような低レイヤーな部分が気になることがあるので、k8s クラスタを手軽に作れる環境が欲しくなりました。

というわけで、Vagrant を使って手元の仮想環境の管理をしつつ、ansible で k8s クラスタの構築する環境を作りました。ただし、ansible で k8s クラスタを構築する、といっても ansible で完結しておらず、一手間が必要です。というわけで、以下のレポジトリ でどのように k8s クラスタを作っているかを解説してみます。

構築する k8s クラスタ環境

レポジトリの README.md にありますが、構築する環境は以下のようなネットワークになっています。

             client (172.17.1.0/24 / ASN 64512)
-------+--------------+------------
       |              |
   eth1|.1      enp0s8|.10
  +----+----+   +-----+-----+
  |  rt-01  |   | client-01 |
  +----+----+   +-----------+
   eth2|.1
       |                                          k8s-cluster (172.17.0.0/24 / ASN 64522)
-------+-----+------------------+-----------------+-----------------+--------
             |                  |                 |                 |
       enp0s8|.10         enp0s8|.21        enp0s8|.22        enp0s8|.23
     +-------+-------+   +------+------+   +------+------+   +------+------+
     | k8s-master-01 |   | k8s-node-01 |   | k8s-node-02 |   | k8s-node-03 |
     +---------------+   +-------------+   +-------------+   +-------------+

レポジトリ名から予想がつきますが、k8s の LoadBalancer として metallb を使うため、BGP ルーター(rt-01) も構築しています。また、metallb で使用する AS 番号として 64522 を定義しています。また、k8s の CNI プラグインとして flannel を採用しています。各ネットワークのアドレスブロックは以下のようにしています。

  • docker0 : 172.17.255.0/24
  • k8s
    • Flannel Pod Network CIDR : 10.244.0.0/16
    • Service CIDR : 10.244.0.0/16
    • BGP network : 172.17.254.0/24
  • ASNs
    • client 64512
    • k8s-cluster 64522

また、k8s クラスタの管理には kubeadm を全面的に採用しています。

k8s クラスタの構築

k8s 環境の構築

https://github.com/nabeo/vagrant-k8s-metallbgit clone してきたディレクトリで make up を実行します。内部的には各 VM 定義に対して vagrant up が実行され、Vagrantansible_local によるプロビジョニングが実施されます。ansible_local のプロビジョニングでは以下を実施していないため、後の手順で手動で実施する必要があります。

  • k8s-node の k8s クラスタへの参加と worker ノードとしての登録
  • metallb の構築

k8s-node の k8s クラスタへの参加と worker ノードとしての登録

まず、make k8s-master-01-sshk8s-master-01 ノードに ssh ログインします。

k8s-master-01kubeadm token create --print-join-command を実行して、worker ノードが k8s クラスタに参加するためのトークン発行と k8s クラスタに参加する worker ノードで実行するべき kubeadm join コマンドを表示させます。

次に、各 worker ノードで k8s-master-01 ノードで取得した kubeadm join コマンドを実行して、k8s クラスタに参加させます。

この時点では worker ノードは k8s クラスタに参加していますが、worker ノードとしては認識されていないので、k8s-master-01 で以下を実行して worker ノードとして登録します。

kubectl label nodes <hostname> node-role.kubernetes.io/node=
kubectl label nodes <hostname> type=worker

全ての worker ノードの準備が整ったら、 kubectl get nodes では以下のように表示されていると思います。

NAME            STATUS   ROLES    AGE     VERSION
k8s-master-01   Ready    master   42d     v1.15.1
k8s-node-01     Ready    node     42d     v1.15.1
k8s-node-02     Ready    node     5d18h   v1.15.1
k8s-node-03     Ready    node     5d18h   v1.15.1

metallb の構築

Vagrantfile では vm との synced_folder として ansible プレイブックを配置している ansible ディレクトリの他に雑多なファイルを格納するための shared ディレクトリを登録しています。k8s クラスタで使用するマニフェストファイルも shared ディレクトリに配置しています。

ということで k8s-master-01shared/k8s-configs にある以下のファイルを kubectl apply で読み込ませます。

  • metallb.yaml
  • metallb-bgp.yaml

metallb.yaml は metallb の v0.7.3 をベースにして、Vagrant 環境で使えるように調整しています。metallb-bgp.yaml は metallb を BGP モードで使用し、rt-01 に BGP ピアを貼るようにしています。

metallb は k8s クラスタmetallb-system という名前空間を使用しています。1つの controller pod と worker ノード数分の speaker pod が立ち上がっているはずです。

k8s クラスタで遊ぶ

試しに nginx をデプロイしてみます。以下のマニフェストkubectl apply で読み込ませます

---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 6
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx-container
        image: nginx:alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 80
  selector:
    app: nginx
  type: LoadBalancer

ここでは nginx サービスの typeLoadBalancer とすることで metallb を使うようにしています。kubectl apply によって環境がデプロイされると、rt-01 では以下のように nginx サービスの EXTERNAL-IP である 172.17.254.1 の next hop が worker ノードに向いているような経路が BGP で広報されたことがわかります。

vagrant@rt-01:~$ gobgp monitor adj-in
2019-07-27T07:32:17Z [ROUTE] 172.17.254.1/32 via 172.17.0.23 aspath [64522] attrs [{Origin: ?}]
2019-07-27T07:32:17Z [ROUTE] 172.17.254.1/32 via 172.17.0.21 aspath [64522] attrs [{Origin: ?}]
2019-07-27T07:32:17Z [ROUTE] 172.17.254.1/32 via 172.17.0.22 aspath [64522] attrs [{Origin: ?}]

また、rt-01 では gobgpd で受け取った経路を quaggazebraカーネル経路に落とし込んでいるので以下のように ip route でも確認できます。

172.17.254.1 proto zebra metric 20
        nexthop via 172.17.0.22 dev enp0s8 weight 1
        nexthop via 172.17.0.21 dev enp0s8 weight 1
        nexthop via 172.17.0.23 dev enp0s8 weight 1

gobgpdquagga を使った BGP ルータの構築も https://github.com/nabeo/vagrant-k8s-metallb で構築していますが、ここでは詳しくは触れません。