27 Mar 2018, 00:10

Azure DNS Private Zonesの動きを確認する

プライベートゾーンのパブリックプレビュー開始

Azure DNSのプライベートゾーン対応が、全リージョンでパブリックプレビューとなりました。ゾーンとプレビューのプライベートとパブリックが入り混じって、なにやら紛らわしいですが。

さて、このプライベートゾーン対応ですが、名前のとおりAzure DNSをプライベートな仮想ネットワーク(VNet)で使えるようになります。加えて、しみじみと嬉しい便利機能がついています。

  • Split-Horizonに対応します。VNet内からの問い合わせにはプライベートゾーン、それ以外からはパブリックゾーンのレコードを返します。
  • 仮想マシンの作成時、プライベートゾーンへ自動でホスト名を追加します。
  • プライベートゾーンとVNetをリンクして利用します。複数のVNetをリンクすることが可能です。
  • リンクの種類として、仮想マシンホスト名の自動登録が行われるVNetをRegistration VNet、名前解決(正引き)のみ可能なResolution VNetがあります。
  • プライベートゾーンあたり、Registration VNetの現時点の上限数は1、Resolution VNetは10です。

公式ドキュメントはこちら。現時点の制約もまとまっているので、目を通しておきましょう。

動きを見てみよう

公式ドキュメントには想定シナリオがあり、これを読めばできることがだいたい分かります。ですが、名前解決は呼吸のようなもの、体に叩き込みたいお気持ちです。手を動かして確認します。

事前に準備する環境

下記リソースを先に作っておきます。手順は割愛。ドメイン名はexample.comとしましたが、適宜読み替えてください。

  • VNet *2
    • vnet01
    • subnet01
      • subnet01-nsg (allow ssh)
    • vnet02
    • subnet01
      • subnet01-nsg (allow ssh)
  • Azure DNS Public Zone
    • example.com

Azure CLIへDNS拡張を導入

プレビュー機能をCLIに導入します。いずれ要らなくなるかもしれませんので、要否は公式ドキュメントで確認してください。

$ az extension add --name dns

プライベートゾーンの作成

既存のゾーンを確認します。パブリックゾーンがあります。

$ az network dns zone list -o table
ZoneName      ResourceGroup             RecordSets    MaxRecordSets
------------  ----------------------  ------------  ---------------
example.com   common-global-rg                   2             5000

プライベートゾーンを作成します。Registration VNetとしてvnet01をリンクします。現時点の制約で、リンク時にはVNet上にVMが無い状態にする必要があります。

$ az network dns zone create -g private-dns-poc-ejp-rg -n example.com --zone-type Private --registration-vnets vnet01

同じ名前のゾーンが2つになりました。

$ az network dns zone list -o table
ZoneName      ResourceGroup             RecordSets    MaxRecordSets
------------  ----------------------  ------------  ---------------
example.com   common-global-rg                   2             5000
example.com   private-dns-poc-ejp-rg             1             5000

Registration VNetへVMを作成

VMを2つ作ります。1つにはインターネット経由でsshするので、パブリックIPを割り当てます。

$ BASE_NAME="private-dns-poc-ejp"
$ az network public-ip create -n vm01-pip -g ${BASE_NAME}-rg
$ az network nic create -g ${BASE_NAME}-rg -n vm01-nic --public-ip-address vm01-pip --vnet vnet01 --subnet subnet01
$ az vm create -g ${BASE_NAME}-rg -n vm01 --image Canonical:UbuntuServer:16.04.0-LTS:latest --size Standard_B1s --nics vm01-nic
$ az network nic create -g ${BASE_NAME}-rg -n vm02-nic --vnet vnet01 --subnet subnet01
$ az vm create -g ${BASE_NAME}-rg -n vm02 --image Canonical:UbuntuServer:16.04.0-LTS:latest --size Standard_B1s --nics vm02-nic

パブリックIPをパブリックゾーンへ登録

Split-Horizonの動きを確認したいので、パブリックIPをパブリックゾーンへ登録します。

$ az network public-ip show -g private-dns-poc-ejp-rg -n vm01-pip --query ipAddress
"13.78.84.84"
$ az network dns record-set a add-record -g common-global-rg -z example.com -n vm01 -a 13.78.84.84

パブリックゾーンで名前解決できることを確認します。

$ nslookup vm01.example.com
Server:         103.5.140.1
Address:        103.5.140.1#53

Non-authoritative answer:
Name:   vm01.example.com
Address: 13.78.84.84

Registration VNetの動きを確認

vnet01のvm01へ、パブリックIP経由でsshします。

$ ssh vm01.example.com

同じRegistration VNet上のvm02を正引きします。ドメイン名無し、ホスト名だけでnslookupすると、VNetの内部ドメイン名がSuffixになります。

vm01:~$ nslookup vm02
Server:         168.63.129.16
Address:        168.63.129.16#53

Non-authoritative answer:
Name:   vm02.aioh0amlfdze5drhlpb1ktqwxd.lx.internal.cloudapp.net
Address: 10.0.0.5

ドメイン名をつけてみましょう。Nameはvnet01にリンクしたプライベートゾーンのドメイン名になりました。

vm01:~$ nslookup vm02.example.com
Server:         168.63.129.16
Address:        168.63.129.16#53

Non-authoritative answer:
Name:   vm02.example.com
Address: 10.0.0.5

逆引きもできます。

vm01:~$ nslookup 10.0.0.5
Server:         168.63.129.16
Address:        168.63.129.16#53

Non-authoritative answer:
5.0.0.10.in-addr.arpa   name = vm02.example.com.

Authoritative answers can be found from:

Split-Horizonの動きを確認

さて、いま作業をしているvm01には、インターネット経由でパブリックゾーンで名前解決してsshしたわけですが、プライベートなVNet内でnslookupするとどうなるでしょう。

vm01:~$ nslookup vm01.example.com
Server:         168.63.129.16
Address:        168.63.129.16#53

Non-authoritative answer:
Name:   vm01.example.com
Address: 10.0.0.4

プライベートゾーンで解決されました。Split-Horizonが機能していることが分かります。

あ、どうでもいいことですが、Split-Horizonって戦隊モノの必殺技みたいなネーミングですね。叫びながら地面に拳を叩きつけたい感じ。

Resolution VNetの動きを確認

vnet02を作成し、Resolution VNetとしてプライベートゾーンとリンクします。そして、vnet02にvm03を作ります。vm03へのsshまで一気に進めます。

$ BASE_NAME="private-dns-poc-ejp"
$ az network vnet create -g ${BASE_NAME}-rg -n vnet02 --address-prefix 10.1.0.0/16 --subnet-name subnet01
$ az network vnet subnet update -g ${BASE_NAME}-rg -n subnet01 --vnet-name vnet02 --network-security-group subnet01-nsg
$ az network public-ip create -n vm03-pip -g ${BASE_NAME}-rg
$ az network dns zone update -g private-dns-poc-ejp-rg -n example.com --resolution-vnets vnet02
$ az network nic create -g ${BASE_NAME}-rg -n vm03-nic --public-ip-address vm03-pip --vnet vnet02 --subnet subnet01
$ az vm create -g ${BASE_NAME}-rg -n vm03 --image Canonical:UbuntuServer:16.04.0-LTS:latest --size Standard_B1s --nics vm03-nic
$ az network public-ip show -g private-dns-poc-ejp-rg -n vm03-pip --query ipAddress
"13.78.54.133"
$ ssh 13.78.54.133

名前解決の確認が目的なので、vnet01/02間はPeeringしません。

では、vnet01上のvm01を正引きします。ドメイン名を指定しないと、解決できません。vnet02上にvm01がある、と指定されたと判断するからです。

vm03:~$ nslookup vm01
Server:         168.63.129.16
Address:        168.63.129.16#53

** server can't find vm01: SERVFAIL

ではプライベートゾーンのドメイン名をつけてみます。解決できました。

vm03:~$ nslookup vm01.example.com
Server:         168.63.129.16
Address:        168.63.129.16#53

Non-authoritative answer:
Name:   vm01.example.com
Address: 10.0.0.4

Resolution VNetからは、逆引きできません。

vm03:~$ nslookup 10.0.0.4
Server:         168.63.129.16
Address:        168.63.129.16#53

** server can't find 4.0.0.10.in-addr.arpa: NXDOMAIN

ところでRegistration VNetからResolution VNetのホスト名をnslookupするとどうなるでしょう。

$ ssh vm01.example.com
vm01:~$ nslookup vm03
Server:         168.63.129.16
Address:        168.63.129.16#53

** server can't find vm03: SERVFAIL

vm01:~$ nslookup vm03.example.com
Server:         168.63.129.16
Address:        168.63.129.16#53

** server can't find vm03.example.com: NXDOMAIN

ドメイン名あり、なしに関わらず、名前解決できません。VNetが別なのでVNetの内部DNSで解決できない、また、Resolution VNetのVMはレコードがプライベートゾーンに自動登録されないことが分かります。

26 Mar 2018, 00:08

AzureのAvailability Zonesへ分散するVMSSをTerraformで作る

動機

Terraform Azure Provider 1.3.0で、VMSSを作る際にAvailability Zonesを指定できるようになりました。Availability Zonesはインフラの根っこの仕組みなので、現在(20183)限定されたリージョンで長めのプレビュー期間がとられています。ですが、GAやグローバル展開を見据え、素振りしておきましょう。

前提条件

  • Availability Zones対応リージョンを選びます。現在は5リージョンです。この記事ではEast US 2とします。
  • Availability Zonesのプレビューにサインアップ済みとします。
  • bashでsshの公開鍵が~/.ssh/id_rsa.pubにあると想定します。
  • 動作確認した環境は以下です。
    • Terraform 0.11.2
    • Terraform Azure Provider 1.3.0
    • WSL (ubuntu 16.04)
    • macos (High Sierra 10.13.3)

コード

以下のファイルを同じディレクトリに作成します。

Terraform メインコード

VMSSと周辺リソースを作ります。

  • 最終行近くの “zones = [1, 2, 3]” がポイントです。これだけで、インスタンスを散らす先のゾーンを指定できます。
  • クロスゾーン負荷分散、冗長化するため、Load BalancerとパブリックIPのSKUをStandardにします。

[main.tf]

resource "azurerm_resource_group" "poc" {
  name     = "${var.resource_group_name}"
  location = "East US 2"
}

resource "azurerm_virtual_network" "poc" {
  name                = "vnet01"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  location            = "${azurerm_resource_group.poc.location}"
  address_space       = ["10.0.0.0/16"]
}

resource "azurerm_subnet" "poc" {
  name                      = "subnet01"
  resource_group_name       = "${azurerm_resource_group.poc.name}"
  virtual_network_name      = "${azurerm_virtual_network.poc.name}"
  address_prefix            = "10.0.2.0/24"
  network_security_group_id = "${azurerm_network_security_group.poc.id}"
}

resource "azurerm_network_security_group" "poc" {
  name                = "nsg01"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  location            = "${azurerm_resource_group.poc.location}"

  security_rule = [
    {
      name                       = "allow_http"
      priority                   = 100
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "80"
      source_address_prefix      = "*"
      destination_address_prefix = "*"
    },
    {
      name                       = "allow_ssh"
      priority                   = 101
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "22"
      source_address_prefix      = "*"
      destination_address_prefix = "*"
    },
  ]
}

resource "azurerm_public_ip" "poc" {
  name                         = "pip01"
  resource_group_name          = "${azurerm_resource_group.poc.name}"
  location                     = "${azurerm_resource_group.poc.location}"
  public_ip_address_allocation = "static"
  domain_name_label            = "${var.scaleset_name}"

  sku = "Standard"
}

resource "azurerm_lb" "poc" {
  name                = "lb01"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  location            = "${azurerm_resource_group.poc.location}"

  frontend_ip_configuration {
    name                 = "fipConf01"
    public_ip_address_id = "${azurerm_public_ip.poc.id}"
  }

  sku = "Standard"
}

resource "azurerm_lb_backend_address_pool" "poc" {
  name                = "bePool01"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  loadbalancer_id     = "${azurerm_lb.poc.id}"
}

resource "azurerm_lb_rule" "poc" {
  name                           = "lbRule"
  resource_group_name            = "${azurerm_resource_group.poc.name}"
  loadbalancer_id                = "${azurerm_lb.poc.id}"
  protocol                       = "Tcp"
  frontend_port                  = 80
  backend_port                   = 80
  frontend_ip_configuration_name = "fipConf01"
  backend_address_pool_id        = "${azurerm_lb_backend_address_pool.poc.id}"
  probe_id                       = "${azurerm_lb_probe.poc.id}"
}

resource "azurerm_lb_probe" "poc" {
  name                = "http-probe"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  loadbalancer_id     = "${azurerm_lb.poc.id}"
  port                = 80
}

resource "azurerm_lb_nat_pool" "poc" {
  count                          = 3
  name                           = "ssh"
  resource_group_name            = "${azurerm_resource_group.poc.name}"
  loadbalancer_id                = "${azurerm_lb.poc.id}"
  protocol                       = "Tcp"
  frontend_port_start            = 50000
  frontend_port_end              = 50119
  backend_port                   = 22
  frontend_ip_configuration_name = "fipConf01"
}

data "template_cloudinit_config" "poc" {
  gzip          = true
  base64_encode = true

  part {
    content_type = "text/cloud-config"
    content      = "${file("${path.module}/cloud-config.yaml")}"
  }
}

resource "azurerm_virtual_machine_scale_set" "poc" {
  name                = "${var.scaleset_name}"
  resource_group_name = "${azurerm_resource_group.poc.name}"
  location            = "${azurerm_resource_group.poc.location}"
  upgrade_policy_mode = "Manual"

  sku {
    name     = "Standard_B1s"
    tier     = "Standard"
    capacity = 3
  }

  storage_profile_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "16.04-LTS"
    version   = "latest"
  }

  storage_profile_os_disk {
    name              = ""
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "Standard_LRS"
  }

  os_profile {
    computer_name_prefix = "pocvmss"
    admin_username       = "${var.admin_username}"
    admin_password       = ""
    custom_data          = "${data.template_cloudinit_config.poc.rendered}"
  }

  os_profile_linux_config {
    disable_password_authentication = true

    ssh_keys {
      path     = "/home/${var.admin_username}/.ssh/authorized_keys"
      key_data = "${file("~/.ssh/id_rsa.pub")}"
    }
  }

  network_profile {
    name    = "terraformnetworkprofile"
    primary = true

    ip_configuration {
      name                                   = "PoCIPConfiguration"
      subnet_id                              = "${azurerm_subnet.poc.id}"
      load_balancer_backend_address_pool_ids = ["${azurerm_lb_backend_address_pool.poc.id}"]
      load_balancer_inbound_nat_rules_ids    = ["${element(azurerm_lb_nat_pool.poc.*.id, count.index)}"]
    }
  }

  zones = [1, 2, 3]
}

cloud-init configファイル

各インスタンスがどのゾーンで動いているか確認したいので、インスタンス作成時にcloud-initでWebサーバーを仕込みます。メタデータからインスタンス名と実行ゾーンを引っ張り、nginxのドキュメントルートに書きます。

[cloud-config.yaml]

#cloud-config
package_upgrade: true
packages:
  - nginx
runcmd:
  - 'echo "[Instance Name]: `curl -H Metadata:true "http://169.254.169.254/metadata/instance/compute/name?api-version=2017-12-01&format=text"`    [Zone]: `curl -H Metadata:true "http://169.254.169.254/metadata/instance/compute/zone?api-version=2017-12-01&format=text"`" > /var/www/html/index.nginx-debian.html'

インスタンス作成時、パッケージの導入やアップデートに時間をかけたくない場合は、Packerなどで前もってカスタムイメージを作っておくのも手です。

Terraform 変数ファイル

変数は別ファイルへ。

[variables.tf]

variable "resource_group_name" {
  default = "your-rg"
}

variable "scaleset_name" {
  default = "yourvmss01"
}

variable "admin_username" {
  default = "yourname"
}

実行

では実行。

$ terraform init
$ terraform plan
$ terraform apply

5分くらいで完了しました。このサンプルでは、この後のcloud-initのパッケージ処理に時間がかかります。待てない場合は前述の通り、カスタムイメージを使いましょう。

インスタンスへのsshを通すよう、Load BalancerにNATを設定していますので、cloud-initの進捗は確認できます。

$ ssh -p 50000 yourname@yourvmss01.eastus2.cloudapp.azure.com
$ tail -f /var/log/cloud-init-output.log
Cloud-init v. 17.1 finished at Sun, 25 Mar 2018 10:41:40 +0000. Datasource DataSourceAzure [seed=/dev/sr0].  Up 611.51 seconds

ではWebサーバーにアクセスしてみましょう。

$ while true; do curl yourvmss01.eastus2.cloudapp.azure.com; sleep 1; done;
[Instance Name]: yourvmss01_2    [Zone]: 3
[Instance Name]: yourvmss01_0    [Zone]: 1
[Instance Name]: yourvmss01_2    [Zone]: 3
[Instance Name]: yourvmss01_1    [Zone]: 2

VMSSのインスタンスがゾーンに分散されたことが分かります。

では、このままスケールアウトしてみましょう。main.tfのazurerm_virtual_machine_scale_set.poc.sku.capacityを3から4にし、再度applyします。

[Instance Name]: yourvmss01_1    [Zone]: 2
[Instance Name]: yourvmss01_3    [Zone]: 1
[Instance Name]: yourvmss01_3    [Zone]: 1
[Instance Name]: yourvmss01_1    [Zone]: 2
[Instance Name]: yourvmss01_3    [Zone]: 1

ダウンタイムなしに、yourvmss01_3が追加されました。すこぶる簡単。

12 Mar 2018, 00:21

AKSのService作成時にホスト名を付ける

2つのやり口

Azure Container Service(AKS)はServiceを公開する際、パブリックIPを割り当てられます。でもIPだけじゃなく、ホスト名も同時に差し出して欲しいケースがありますよね。

わたしの知る限り、2つの方法があります。

以下、AKS 1.9.2での実現手順です。

DNSラベル名付与機能

簡単です。Serviceのannotationsに定義するだけ。試しにnginxをServiceとして公開し、確認してみましょう。

[nginx-label.yaml]

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: hogeginx
  annotations:
    service.beta.kubernetes.io/azure-dns-label-name: hogeginx
spec:
  selector:
    app: nginx
  type: LoadBalancer
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

デプロイ。

$ kubectl create -f nginx-label.yaml

パブリックIP(EXTERNAL-IP)が割り当てられた後、ラベル名が使えます。ルールは [ラベル名].[リージョン].cloudapp.azure.com です。

$ curl hogeginx.eastus.cloudapp.azure.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
[snip]

ドメイン名は指定しなくていいから、Service毎にホスト名を固定したいんじゃ、という場合にはこれでOK。

Kubenetes ExternalDNS

任意のドメイン名を使いたい場合は、Incubatorプロジェクトのひとつ、Kubenetes ExternalDNSを使ってAzure DNSへAレコードを追加する手があります。本家の説明はこちら

Kubenetes ExternalDNSは、Azure DNSなどAPIを持つDNSサービスを操作するアプリです。k8sのDeploymentとして動かせます。Route 53などにも対応。

さて動かしてみましょう。前提として、すでにAzure DNSにゾーンがあるものとします。

ExternalDNSがDNSゾーンを操作できるよう、サービスプリンシパルを作成しましょう。スコープはDNSゾーンが置かれているリソースグループ、ロールはContributorとします。

$ az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/your-subscription-id/resourceGroups/hoge-dns-rg" -n hogeExtDnsSp

appId、password、tenantを控えておいてください。次でsecretに使います。

ではExteralDNSに渡すsecretを作ります。まずJSONファイルに書きます。

[azure.json]

{
    "tenantId": "your-tenant",
    "subscriptionId": "your-subscription-id",
    "aadClientId": "your-appId",
    "aadClientSecret": "your-password",
    "resourceGroup": "hoge-dns-rg"
}

JSONファイルを元に、secretを作ります。

$ kubectl create secret generic azure-config-file --from-file=azure.json

ExteralDNSのマニフェストを作ります。ドメイン名はexmaple.comとしていますが、使うDNSゾーンに合わせてください。以下はRBACを使っていない環境での書き方です。

[extdns.yaml]

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: external-dns
spec:
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      containers:
      - name: external-dns
        image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
        args:
        - --source=service
        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
        - --provider=azure
        - --azure-resource-group=hoge-dns-rg # (optional) use the DNS zones from the tutorial's resource group
        volumeMounts:
        - name: azure-config-file
          mountPath: /etc/kubernetes
          readOnly: true
      volumes:
      - name: azure-config-file
        secret:
          secretName: azure-config-file

ExternalDNSをデプロイします。

$ kubectl create -f extdns.yaml

ではホスト名を付与するServiceのマニフェストを作りましょう。先ほどのDNSラベル名付与機能と同様、annotationsへ定義します。

[nginx-extdns.yaml]

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx-extdns
spec:
  template:
    metadata:
      labels:
        app: nginx-extdns
    spec:
      containers:
      - image: nginx
        name: nginx
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: hogeginx-extdns
  annotations:
    external-dns.alpha.kubernetes.io/hostname: hogeginx.example.com
spec:
  selector:
    app: nginx-extdns
  type: LoadBalancer
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

デプローイ。

$ kubectl create -f nginx-extdns.yaml

パブリックIP(EXTERNAL-IP)が割り当てられた後、Aレコードが登録されます。確認してみましょう。

$ az network dns record-set a list -g hoge-dns-rg -z example.com -o table
Name      ResourceGroup       Ttl  Type    Metadata
--------  ----------------  -----  ------  ----------
hogeginx  hoge-dns-rg         300  A

ゲッツ。

$ curl hogeginx.example.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
[snip]

Incubatorプロジェクトなので今後大きく変化する可能性がありますが、ご参考になれば。

11 Feb 2018, 00:20

AKSのIngress TLS証明書を自動更新する

カジュアルな証明書管理方式が欲しい

ChromeがHTTPサイトに対する警告を強化するそうです。非HTTPSサイトには、生きづらい世の中になりました。

さてそうなると、TLS証明書の入手と更新、めんどくさいですね。ガチなサイトでは証明書の維持管理を計画的に行うべきですが、検証とかちょっとした用途で立てるサイトでは、とにかくめんどくさい。カジュアルな方式が望まれます。

そこで、Azure Container Service(AKS)で使える気軽な方法をご紹介します。

  • TLSはIngress(NGINX Ingress Controller)でまとめて終端
  • Let’s Encyptから証明書を入手
  • Kubenetesのアドオンであるcert-managerで証明書の入手、更新とIngressへの適用を自動化
    • ACME(Automatic Certificate Management Environment)対応
    • cert-managerはまだ歴史の浅いプロジェクトだが、kube-legoの後継として期待

なおKubernetes/AKSは開発ペースやエコシステムの変化が速いので要注意。この記事は2018/2/10に書いています。

使い方

AKSクラスターと、Azure DNS上に利用可能なゾーンがあることを前提にします。ない場合、それぞれ公式ドキュメントを参考にしてください。

まずAKSにNGINX Ingress Controllerを導入します。helmで入れるのが楽でしょう。この記事も参考に。

$ helm install stable/nginx-ingress --name my-nginx

サービスの状況を確認します。NGINX Ingress ControllerにEXTERNAL-IPが割り当てられるまで、待ちます。

$ kubectl get svc
NAME                                     TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)                     AGE
kubernetes                               ClusterIP      10.0.0.1       <none>           443/TCP                     79d
my-nginx-nginx-ingress-controller        LoadBalancer   10.0.2.105     52.234.148.138   80:30613/TCP,443:30186/TCP   6m
my-nginx-nginx-ingress-default-backend   ClusterIP      10.0.102.246   <none>           80/TCP                     6m

EXTERNAL-IPが割り当てられたら、Azure DNSで名前解決できるようにします。Azure CLIを使います。Ingressのホスト名をwww.example.comとする例です。このホスト名で、後ほどLet’s Encryptから証明書を取得します。

$ az network dns record-set a add-record -z example.com -g your-dnszone-rg -n www -a 52.234.148.138

cert-managerのソースをGitHubから取得し、contribからhelm installします。いずれstableを使えるようになるでしょう。なお、このAKSクラスターはまだRBACを使っていないので、”–set rbac.create=false”オプションを指定しています。

$ git clone https://github.com/jetstack/cert-manager
$ cd cert-manager/
$ helm install --name cert-manager --namespace kube-system contrib/charts/cert-manager --set rbac.create=false

では任意の作業ディレクトリに移動し、以下の内容でマニフェストを作ります。cm-issuer-le-staging-sample.yamlとします。

apiVersion: certmanager.k8s.io/v1alpha1
kind: Issuer
metadata:
  name: letsencrypt-staging
  namespace: default
spec:
  acme:
    # The ACME server URL
    server: https://acme-staging.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: hoge@example.com
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-staging
    # Enable the HTTP-01 challenge provider
    http01: {}

証明書を発行してもらうLet’s EncryptをIssuerとして登録するわけですが、まずはステージングのAPIエンドポイントを指定しています。Let’s EncryptにはRate Limitがあり、失敗した時に痛いからです。Let’s EncryptのステージングAPIを使うとフェイクな証明書(Fake LE Intermediate X1)が発行されますが、流れの確認やマニフェストの検証は、できます。

なお、Let’s Encryptとのチャレンジには今回、HTTPを使います。DNSチャレンジもいずれ対応する見込みです。

では、Issuerを登録します。

$ kubectl apply -f cm-issuer-le-staging-sample.yaml

次は証明書の設定です。マニフェストはcm-cert-le-staging-sample.yamlとします。acme節にACME構成を書きます。チャレンジはHTTP、ingressClassはnginxです。

apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: example-com
  namespace: default
spec:
  secretName: example-com-tls
  issuerRef:
    name: letsencrypt-staging
  commonName: www.example.com
  dnsNames:
  - www.example.com
  acme:
    config:
    - http01:
        ingressClass: nginx
      domains:
      - www.example.com

証明書設定をデプロイします。

$ kubectl apply -f cm-cert-le-staging-sample.yaml

証明書の発行状況を確認します。

$ kubectl describe certificate example-com
Name:         example-com
Namespace:    default
[snip]
Events:
  Type     Reason                 Age              From                     Message
  ----     ------                 ----             ----                     -------
  Warning  ErrorCheckCertificate  8m               cert-manager-controller  Error checking existing TLS certificate: secret "example-com-tls" not found
  Normal   PrepareCertificate     8m               cert-manager-controller  Preparing certificate with issuer
  Normal   PresentChallenge       8m               cert-manager-controller  Presenting http-01 challenge for domain www.example.com
  Normal   SelfCheck              8m               cert-manager-controller  Performing self-check for domain www.example.com
  Normal   ObtainAuthorization    7m               cert-manager-controller  Obtained authorization for domain www.example.com
  Normal   IssueCertificate       7m               cert-manager-controller  Issuing certificate...
  Normal   CeritifcateIssued      7m               cert-manager-controller  Certificated issuedsuccessfully
  Normal   RenewalScheduled       7m (x2 over 7m)  cert-manager-controller  Certificate scheduled for renewal in 1438 hours

無事に証明書が発行され、更新もスケジュールされました。手順やマニフェストの書きっぷりは問題なさそうです。これをもってステージング完了としましょう。

ではLet’s EncryptのAPIエンドポイントをProduction向けに変更し、新たにIssuer登録します。cm-issuer-le-prod-sample.yamlとします。

apiVersion: certmanager.k8s.io/v1alpha1
kind: Issuer
metadata:
  name: letsencrypt-prod
  namespace: default
spec:
  acme:
    # The ACME server URL
    server: https://acme-v01.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: hoge@example.com
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-prod
    # Enable the HTTP-01 challenge provider
    http01: {}

デプロイします。

$ kubectl apply -f cm-issuer-le-prod-sample.yaml

同様に、Production向けの証明書設定をします。cm-cert-le-prod-sample.yamlとします。

apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: prod-example-com
  namespace: default
spec:
  secretName: prod-example-com-tls
  issuerRef:
    name: letsencrypt-prod
  commonName: www.example.com
  dnsNames:
  - www.example.com
  acme:
    config:
    - http01:
        ingressClass: nginx
      domains:
      - www.example.com

デプロイします。

$ kubectl apply -f cm-cert-le-prod-sample.yaml

発行状況を確認します。

$ kubectl describe certificate prod-example-com
Name:         prod-example-com
Namespace:    default
[snip]
Events:
  Type     Reason                 Age              From                     Message
  ----     ------                 ----             ----                     -------
  Warning  ErrorCheckCertificate  27s              cert-manager-controller  Error checking existing TLS certificate: secret "prod-example-com-tls" not found
  Normal   PrepareCertificate     27s              cert-manager-controller  Preparing certificate with issuer
  Normal   PresentChallenge       26s              cert-manager-controller  Presenting http-01 challenge for domain www.example.com
  Normal   SelfCheck              26s              cert-manager-controller  Performing self-check for domain www.example.com
  Normal   IssueCertificate       7s               cert-manager-controller  Issuing certificate...
  Normal   ObtainAuthorization    7s               cert-manager-controller  Obtained authorization for domain www.example.com
  Normal   RenewalScheduled       6s (x3 over 5m)  cert-manager-controller  Certificate scheduled for renewal in 1438 hours
  Normal   CeritifcateIssued      6s               cert-manager-controller  Certificated issuedsuccessfully

証明書が発行され、1438時間(約60日)内の更新がスケジュールされました。

ではバックエンドを設定して確認してみましょう。バックエンドにNGINXを立て、exposeします。

$ kubectl run nginx --image nginx --port 80
$ kubectl expose deployment nginx --type NodePort

Ingressを設定します。ファイル名はingress-nginx-sample.yamlとします。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /
  name: ingress-nginx-sample
spec:
  rules:
    - host: www.example.com
      http:
        paths:
          - path: /
            backend:
              serviceName: nginx
              servicePort: 80
  tls:
    - hosts:
      - www.example.com
      secretName: prod-example-com-tls

デプロイします。

$ kubectl apply -f ingress-nginx-sample.yaml

いざ確認。

$ curl https://www.example.com/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
[snip]

便利ですね。Let’s Encryptをはじめ、関連プロジェクトに感謝です。

10 Feb 2018, 11:00

AKSのNGINX Ingress Controllerのデプロイで悩んだら

楽したいならhelmで入れましょう

AKSに限った話ではありませんが、Kubernetesにぶら下げるアプリの数が多くなってくると、URLマッピングやTLS終端がしたくなります。方法は色々あるのですが、シンプルな選択肢はNGINX Ingress Controllerでしょう。

さて、そのNGINX Ingress ControllerのデプロイはGitHubのドキュメント通りに淡々とやればいいのですが、helmを使えばコマンド一発です。そのようにドキュメントにも書いてあるのですが、最後の方で出てくるので「それ早く言ってよ」な感じです。

せっかくなので、Azure(AKS)での使い方をまとめておきます。開発ペースやエコシステムの変化が速いので要注意。この記事は2018/2/10に書いています。

使い方

AKSクラスターと、Azure DNS上に利用可能なゾーンがあることを前提にします。ない場合、それぞれ公式ドキュメントを参考にしてください。

ではhelmでNGINX Ingress Controllerを導入します。helmを使っていなければ、入れておいてください。デプロイはこれだけ。Chartはここ

$ helm install stable/nginx-ingress --name my-nginx

バックエンドへのつなぎが機能するか、Webアプリを作ってテストします。NGINXとApacheを選びました。

$ kubectl run nginx --image nginx --port 80
$ kubectl run apache --image httpd --port 80

サービスとしてexposeします。

$ kubectl expose deployment nginx --type NodePort
$ kubectl expose deployment apache --type NodePort

現時点のサービスたちを確認します。

$ kubectl get svc
NAME                                     TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                  AGE
apache                                   NodePort       10.0.244.167   <none>          80:30928/TCP                 14h
kubernetes                               ClusterIP      10.0.0.1       <none>          443/TCP                  79d
my-nginx-nginx-ingress-controller        LoadBalancer   10.0.91.78     13.72.108.187   80:32448/TCP,443:31991/TCP   14h
my-nginx-nginx-ingress-default-backend   ClusterIP      10.0.74.104    <none>          80/TCP                  14h
nginx                                    NodePort       10.0.191.16    <none>          80:30752/TCP                 14h

AKSの場合はパブリックIPがNGINX Ingress Controllerに割り当てられます。EXTERNAL-IPがpendingの場合は割り当て中なので、しばし待ちます。

割り当てられたら、EXTERNAL-IPをAzure DNSで名前解決できるようにしましょう。Azure CLIを使います。dev.example.comの例です。

$ az network dns record-set a add-record -z example.com -g your-dnszone-rg -n dev -a 13.72.108.187

TLSが終端できるかも検証したいので、Secretを作ります。証明書とキーはLet’s Encryptで作っておきました。

$ kubectl create secret tls example-tls --key privkey.pem --cert fullchain.pem

ではIngressを構成しましょう。以下をファイル名ingress-nginx-sample.yamlとして保存します。IngressでTLSを終端し、/へのアクセスは先ほどexposeしたNGINXのサービスへ、/apacheへのアクセスはApacheへ流します。rewrite-targetをannotaionsで指定するのを、忘れずに。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /
  name: ingress-nginx-sample
spec:
  rules:
    - host: dev.example.com
      http:
        paths:
          - path: /
            backend:
              serviceName: nginx
              servicePort: 80
          - path: /apache
            backend:
              serviceName: apache
              servicePort: 80
  tls:
    - hosts:
      - dev.example.com
      secretName: example-tls

あとは反映するだけ。

$ kubectl apply -f ingress-nginx-sample.yaml

curlで確認します。

$ curl https://dev.example.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
[snip]

/apacheへのパスも確認します。

$ curl https://dev.example.com/apache
<html><body><h1>It works!</h1></body></html>

簡単ですね。