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>

簡単ですね。

22 Jan 2018, 22:00

Azureのリソースグループ限定 共同作成者をいい感じに作る

共同作成者は、ちょっと強い

Azureのリソースグループは、リソースを任意のグループにまとめ、ライフサイクルや権限の管理を一括して行える便利なコンセプトです。

ユースケースのひとつに、”本番とは分離した開発向けリソースグループを作って、アプリ/インフラ開発者に開放したい”、があります。新しい技術は試行錯誤で身につくので、こういった環境は重要です。

なのですが、このようなケースで、権限付与の落とし穴があります。

  • サブスクリプション所有者が開発用リソースグループを作る
  • スコープを開発用リソースグループに限定し、開発者に対し共同作成者ロールを割り当てる
  • 開発者はリソースグループ限定で、のびのび試行錯誤できて幸せ
  • 開発者がスッキリしたくなり、リソースグループごとバッサリ削除 (共同作成者なので可能)
  • 開発者にはサブスクリプションレベルの権限がないため、リソースグループを作成できない
  • 詰む
  • サブスクリプション所有者が、リソースグループ作成と権限付与をやり直し

共同作成者ロールから、リソースグループの削除権限だけを除外できると、いいんですが。そこでカスタムロールの出番です。リソースグループ限定、グループ削除権限なしの共同作成者を作ってみましょう。

いい感じのカスタムロールを作る

Azureのカスタムロールは、個別リソースレベルで粒度の細かい権限設定ができます。ですが、やり過ぎると破綻するため、シンプルなロールを最小限作る、がおすすめです。

シンプルに行きましょう。まずはカスタムロールの定義を作ります。role.jsonとします。

{
    "Name": "Resource Group Contributor",
    "IsCustom": true,
    "Description": "Lets you manage everything except access to resources, but can not delete Resouce Group",
    "Actions": [
        "*"
    ],
    "NotActions": [
        "Microsoft.Authorization/*/Delete",
        "Microsoft.Authorization/*/Write",
        "Microsoft.Authorization/elevateAccess/Action",
        "Microsoft.Resources/subscriptions/resourceGroups/Delete"
    ],
    "AssignableScopes": [
        "/subscriptions/your-subscriotion-id"
    ]
}

組み込みロールの共同作成者をテンプレに、NotActionsでリソースグループの削除権限を除外しました。AssignableScopesでリソースグループを限定してもいいですが、リソースグループの数だけロールを作るのはつらいので、ここでは指定しません。後からロールを割り当てる時にスコープを指定します。

では、カスタムロールを作成します。

$ az role definition create --role-definition ./role.json

出力にカスタムロールのIDが入っていますので、控えておきます。

"id": "/subscriptions/your-subscriotion-id/providers/Microsoft.Authorization/roleDefinitions/your-customrole-id"

カスタムロールをユーザー、グループ、サービスプリンシパルに割り当てる

次に、ユーザー/グループに先ほど作ったカスタムロールを割り当てます。スコープはリソースグループに限定します。

$ az role assignment create --assignee-object-id your-user-or-group-object-id --role your-customrole-id --scope "/subscriptions/your-subscriotion-id/resourceGroups/sample-dev-rg"

サービスプリンシパル作成時に割り当てる場合は、以下のように。

$ az ad sp create-for-rbac -n "rgcontributor" -p "your-password" --role your-customrole-id --scopes "/subscriptions/your-subscriotion-id/resourceGroups/sample-dev-rg"

余談ですが、”az ad sp create-for-rbac”コマンドはAzure ADアプリケーションを同時に作るため、別途アプリを作ってサービスプリンシパルと紐づける、という作業が要りません。

試してみる

ログインして試してみましょう。サービスプリンシパルの例です。

$ az login --service-principal -u "http://rgcontributor" -p "your-password" -t "your-tenant-id"

検証したサブスクリプションには多数のリソースグループがあるのですが、スコープで指定したものだけが見えます。

$ az group list -o table
Name              Location    Status
----------------  ----------  ---------
sample-dev-rg  japaneast   Succeeded

このリソースグループに、VMを作っておきました。リストはしませんが、ストレージやネットワークなど関連リソースもこのグループにあります。

$ az vm list -o table
Name              ResourceGroup     Location
----------------  ----------------  ----------
sampledevvm01     sample-dev-rg  japaneast

試しにリソースグループを作ってみます。サブスクリプションスコープの権限がないため怒られます。

$ az group create -n rgc-poc-rg -l japaneast
The client 'aaaaa-bbbbb-ccccc-ddddd-eeeee' with object id 'aaaaa-bbbbb-ccccc-ddddd-eeeee' does not have authorization to perform action 'Microsoft.Resources/subscriptions/resourcegroups/write' over scope '/subscriptions/your-subscriotion-id/resourcegroups/rgc-poc-rg'.

リソースグループを消してみます。消すかい? -> y -> ダメ、という、持ち上げて落とす怒り方です。

$ az group delete -n sample-dev-rg
Are you sure you want to perform this operation? (y/n): y
The client 'aaaaa-bbbbb-ccccc-ddddd-eeeee' with object id 'aaaaa-bbbbb-ccccc-ddddd-eeeee' does not have authorization to perform action 'Microsoft.Resources/subscriptions/resourcegroups/delete' over scope '/subscriptions/your-subscriotion-id/resourcegroups/sample-dev-rg'.

でもリソースグループのリソースを一括削除したい

でも、リソースグループは消せなくても、リソースをバッサリ消す手段は欲しいですよね。そんな時には空のリソースマネージャーテンプレートを、completeモードでデプロイすると、消せます。

空テンプレートを、empty.jsonとしましょう。

{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "variables": {},
    "resources": [],
    "outputs": {}
}

破壊的空砲を打ちます。

$ az group deployment create --mode complete -g sample-dev-rg --template-file ./empty.json

リソースグループは残ります。

$ az group list -o table
Name              Location    Status
----------------  ----------  ---------
sample-dev-rg  japaneast   Succeeded

VMは消えました。リストしませんが、他の関連リソースもバッサリ消えています。

$ az vm list -o table