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

08 Jan 2018, 16:30

TerraformでAzure サンプル 2018/1版

サンプルのアップデート

年末にリポジトリの大掃除をしていて、2年前に書いたTerraform & Azureの記事に目が止まりました。原則はいいとして、サンプルは2年物で腐りかけです。ということでアップデートします。

インパクトの大きな変更点

Terraformの、ここ2年の重要なアップデートは以下でしょうか。Azure視点で。

  1. BackendにAzure Blobを使えるようになった
  2. Workspaceで同一コード・複数環境管理ができるようになった
  3. 対応リソースが増えた
  4. Terraform Module Registryが公開された

更新版サンプルの方針

重要アップデートをふまえ、以下の方針で新サンプルを作りました。

チーム、複数端末での運用

BackendにAzure Blobがサポートされたので、チーム、複数端末でstateの共有がしやすくなりました。ひとつのプロジェクトや環境を、チームメンバーがどこからでも、だけでなく、複数プロジェクトでのstate共有もできます。

Workspaceの導入

従来は /dev /stage /prodなど、環境別にコードを分けて管理していました。ゆえに環境間のコード同期が課題でしたが、TerraformのWorkspace機能で解決しやすくなりました。リソース定義で ${terraform.workspace} 変数を参照するように書けば、ひとつのコードで複数環境を扱えます。

要件によっては、従来通り環境別にコードを分けた方がいいこともあるでしょう。環境間の差分が大きい、開発とデプロイのタイミングやライフサイクルが異なるなど、Workspaceが使いづらいケースもあるでしょう。その場合は無理せず従来のやり方で。今回のサンプルは「Workspaceを使ったら何ができるか?」を考えるネタにしてください。

Module、Terraform Module Registryの活用

TerraformのModuleはとても強力な機能なのですが、あーでもないこーでもないと、こだわり過ぎるとキリがありません。「うまいやり方」を見てから使いたいのが人情です。そこでTerraform Module Registryを活かします。お墨付きのVerifiedモジュールが公開されていますので、そのまま使うもよし、ライセンスを確認の上フォークするのもよし、です。

リソースグループは環境ごとに準備し、管理をTerraformから分離

AzureのリソースをプロビジョニングするTerraformコードの多くは、Azureのリソースグループを管理下に入れている印象です。すなわちdestroyするとリソースグループごとバッサリ消える。わかりやすいけど破壊的。

TerraformはApp ServiceやACIなどPaaS、アプリ寄りのリソースも作成できるようになってきたので、アプリ開発者にTerraformを開放したいケースが増えてきています。dev環境をアプリ開発者とインフラ技術者がコラボして育て、そのコードをstageやprodにデプロイする、など。

ところで。TerraformのWorkspaceは、こんな感じで簡単に切り替えられます。

terraform workspace select prod

みなまで言わなくても分かりますね。悲劇はプラットフォーム側で回避しましょう。今回のサンプルではリソースグループをTerraform管理下に置かず、別途作成します。Terraformからはdata resourcesとしてRead Onlyで参照する実装です。環境別のリソースグループを作成し、dev環境のみアプリ開発者へ権限を付与します。

サンプル解説

サンプルはGitHubに置きました。合わせてご確認ください。

このコードをapplyすると、以下のリソースが出来上がります。

  • NGINX on Ubuntu Webサーバー VMスケールセット
  • VMスケールセット向けロードバランサー
  • 踏み台サーバー
  • 上記を配置するネットワーク (仮想ネットワーク、サブネット、NSG)

リポジトリ構造

サンプルのリポジトリ構造です。

├── modules
│   ├── computegroup
│   │   ├── main.tf
│   │   ├── os
│   │   │   ├── outputs.tf
│   │   │   └── variables.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── loadbalancer
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── network
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
└── projects
    ├── project_a
    │   ├── backend.tf
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    └── shared
        ├── backend.tf
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

/modulesにはTerraform Module RegistryでVerifiedされているモジュールをフォークしたコードを入れました。フォークした理由は、リソースグループをdata resource化して参照のみにしたかったためです。

そして、/projectsに2つのプロジェクトを作りました。プロジェクトでリソースとTerraformの実行単位、stateを分割します。sharedで土台となる仮想ネットワークと踏み台サーバー関連リソース、project_aでVMスケールセットとロードバランサーを管理します。

このボリュームだとプロジェクトを分割する必然性は低いのですが、以下のケースにも対応できるように分けました。

  • アプリ開発者がproject_a下でアプリ関連リソースに集中したい
  • 性能観点で分割したい (Terraformはリソース量につれて重くなりがち)
  • 有事を考慮し影響範囲を分割したい

プロジェクト間では、stateをremote_stateを使って共有します。サンプルではsharedで作成した仮想ネットワークのサブネットIDをoutputし、project_aで参照できるよう定義しています。

使い方

前提

  • Linux、WSL、macOSなどbash環境の実行例です
  • SSHの公開鍵をTerraform実行環境の ~/.ssh/id_rsa.pub として準備してください

管理者向けのサービスプリンシパルを用意する

インフラのプロビジョニングの主体者、管理者向けのサービスプリンシパルを用意します。リソースグループを作成できる権限が必要です。

もしなければ作成します。組み込みロールでは、サブスクリプションに対するContributorが妥当でしょう。Terraformのドキュメントも参考に。

az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/SUBSCRIPTION_ID"

出力されるappId、password、tenantを控えます。既存のサービスプリンシパルを使うのであれば、同情報を確認してください。

なお参考までに。Azure Cloud ShellなどAzure CLIが導入されている環境では、特に認証情報の指定なしでterraform planやapply時にAzureのリソースにアクセスできます。TerraformがCLIの認証トークンを使うからです。

そしてBackendをAzure Blobとする場合、Blobにアクセスするためのキーが別途必要です。ですが、残念ながらBackendロジックでキーを得る際に、このトークンが使われません。キーを明示することもできますが、Blobのアクセスキーは漏洩時のリカバリーが大変です。できれば直に扱いたくありません。

サービスプリンシパル認証であれば、Azureリソースへのプロビジョニング、Backendアクセスどちらも対応できます。これがこのサンプルでサービスプリンシパル認証を選んだ理由です。

管理者の環境変数を設定する

Terraformが認証関連で必要な情報を環境変数で設定します。先ほど控えた情報を使います。

export ARM_SUBSCRIPTION_ID="<your subscription id>"
export ARM_CLIENT_ID="<your servicce principal appid>"
export ARM_CLIENT_SECRET="<your service principal password>"
export ARM_TENANT_ID="<your service principal tenant>"

Workspaceを作る

開発(dev)/ステージング(stage)/本番(prod)、3つのWorkspaceを作る例です。

terraform workspace new dev
terraform workspace new stage
terraform workspace new prod

リソースグループを作る

まずWorkspace別にリソースグループを作ります。

az group create -n tf-sample-dev-rg -l japaneast
az group create -n tf-sample-stage-rg -l japaneast
az group create -n tf-sample-prod-rg -l japaneast

リソースグループ名にはルールがあります。Workspace別にリソースグループを分離するため、Terraformのコードで ${terraform.workspace} 変数を使っているためです。この変数は実行時に評価されます。

data "azurerm_resource_group" "resource_group" {
  name = "${var.resource_group_name}-${terraform.workspace}-rg"
}

${var.resource_group_name} は接頭辞です。サンプルではvariables.tfで”tf-sample”と指定しています。

次にBackend、state共有向けリソースグループを作ります。

az group create -n tf-sample-state-rg -l japaneast

このリソースグループは、各projectのbackend.tfで指定しています。

terraform {
  backend "azurerm" {
    resource_group_name  = "tf-sample-state-rg"
    storage_account_name = "<your storage account name>"
    container_name       = "tfstate-project-a"
    key                  = "terraform.tfstate"
  }
}

最後にアプリ開発者がリソースグループtf-sample-dev-rg、tf-sample-state-rgへアクセスできるよう、アプリ開発者向けサービスプリンシパルを作成します。

az ad sp create-for-rbac --role="Contributor" --scopes "/subscriptions/<your subscription id>/resourceGroups/tf-sample-dev-rg" "/subscriptions/<your subscription id>/resourceGroups/tf-sample-state-rg"

出力されるappId、password、tenantは、アプリ開発者向けに控えておきます。

Backendを準備する

project別にストレージアカウントとコンテナーを作ります。tf-sample-state-rgに

  • ストレージアカウント (名前は任意)
  • コンテナー *2 (tfstate-project-a, tfstate-shared)

を作ってください。GUIでもCLIでも、お好きなやり方で。

その後、project_a/backend.tf.sample、shared/backend.tf.sampleをそれぞれbackend.tfにリネームし、先ほど作ったストレージアカウント名を指定します。以下はproject_a/backend.tf.sampleの例。

terraform {
  backend "azurerm" {
    resource_group_name  = "tf-sample-state-rg"
    storage_account_name = "<your storage account name>"
    container_name       = "tfstate-project-a"
    key                  = "terraform.tfstate"
  }
}

data "terraform_remote_state" "shared" {
  backend = "azurerm"

  config {
    resource_group_name  = "tf-sample-state-rg"
    storage_account_name = "<your storage account name>"
    container_name       = "tfstate-shared"
    key                  = "terraform.tfstateenv:${terraform.workspace}"
  }
}

これで準備完了です。

実行

Workspaceをdevに切り替えます。

terraform workspace select dev

まずは土台となるリソースを作成するsharedから。

cd shared
terraform init
terraform plan
terraform apply

土台となるリソースが作成されたら、次はproject_aを。

cd ../project_a
terraform init
terraform plan
terraform apply

ここでは割愛しますが、dev向けサービスプリンシパルで認証しても、dev Workspaceではplan、apply可能です。

dev Workspaceでコードが育ったら、stage/prod Workspaceに切り替えて実行します。

terraform workspace select stage
[以下devと同様の操作]

当然、dev向けサービスプリンシパルで認証している場合は、stage/prodでのplan、apply、もちろんdestroyも失敗します。stage/prod リソースグループにアクセスする権限がないからです。

参考情報

04 Dec 2017, 22:00

Windows上でLinux向けGoバイナリをDockerでビルドする

小ネタです

Goはクロスプラットフォーム開発しやすい言語なのですが、Windows上でLinux向けバイナリーをビルドするなら、gccが要ります。正直なところ入れたくありません。なのでDockerでやります。

条件

  • Docker for Windows
    • Linuxモード
    • ドライブ共有

PowerShell窓で実行

ビルドしたいGoのソースがあるディレクトリで以下のコマンドを実行します。Linux向けバイナリーが同じディレクトリに出来ます。

docker run --rm -it -e GOPATH=/go --mount type=bind,source=${env:GOPATH},target=/go --mount type=bind,source=${PWD},target=/work -w /work golang:1.9.2-alpine go build -a -tags netgo -installsuffix netgo -o yourapp_linux
  • golang:1.9.2-alpine DockerイメージはGOPATHに/goを設定してビルドされていますが、念のため実行時にも設定
  • -v オプションでのマウントは非推奨になったので –mount で
  • スタティックリンク