読者です 読者をやめる 読者になる 読者になる

しばやん雑記

ASP.NET とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Container Service で作成した Kubernetes で Cron Jobs を使ってみる

Jobs はいまいち使いどころが分からなかったですが、Cron Jobs はいくらでも思いつくので実際に試しておきました。デフォルトでは有効化されていないので、ドキュメントに従って設定を追加する必要があります。

ちなみに Azure Container Service で作られる Kubernetes は 1.5 系です。

You need a working Kubernetes cluster at version >= 1.4 (for ScheduledJob), >= 1.5 (for CronJob), with batch/v2alpha1 API turned on by passing --runtime-config=batch/v2alpha1=true while bringing up the API server (see Turn on or off an API version for your cluster for more). You cannot use Cron Jobs on a hosted Kubernetes provider that has disabled alpha resources.

Cron Jobs | Kubernetes

Kubernetes の API server を起動する時のパラメータを追加すれば良いみたいですが、Azure Container Service で作成した Kubernetes の場合はマスターに接続して手動で変えるしか方法がありませんでした。

SSH で接続して /etc/kubernetes/manifests/kube-apiserver.yaml というファイルを直接編集します。

f:id:shiba-yan:20170508012308p:plain

kube-apiserver というコンテナの設定にコマンドを追加出来るようになっているので、最後に必要な設定を追加しておきました。反映させるのが面倒だったので、今回はマスターを再起動させました。

再起動後には kubectl やダッシュボードから Cron Job の追加が行えるようになっています。今回はテストなので専用のイメージを用意せずに試してみることにしました。

apiVersion: batch/v2alpha1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: microsoft/windowsservercore
            args:
            - cmd
            - /c
            - echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure
          nodeSelector:
            beta.kubernetes.io/os: windows

公式ドキュメントの YAML を Windows Server Core を使うように少し書き換えただけのものです。例によって nodeSelector を使って Windows を選ぶようにしておかないと辛いことになります。

作成した YAML をダッシュボードからアップロードして Cron Job を作成しました。前回は検証エラーが発生して作成できませんでしたが、今回は問題なく終了しました。

f:id:shiba-yan:20170508012642p:plain

Cron Jobs はダッシュボードから確認できないので、ここだけは kubectl を使って確認します。

f:id:shiba-yan:20170508011520p:plain

公式の YAML は 1 分間隔で起動するようになっていたのですが、Windows Server Core のダウンロードに時間がかかるため、Job と Pod が次々に作られてしまいました。この辺りは相変わらず罠ですね。

仕方ないので一度 Cron Job / Job / Pod を削除して、Windows Server Core をダウンロードさせることにしました。本来なら concurrencyPolicy で Forbit を選択するべきでした。

f:id:shiba-yan:20170508011635p:plain

適当に kubectl から Windows Server Core を使うデプロイメントを作成して、ダウンロードさせた後に再度 Cron Job を作成すると、今度は 1 分以内にコンテナが終了するようになりました。

Pod のステータスが完了になっているので、正常にコンテナが終了してることが分かります。

f:id:shiba-yan:20170508012731p:plain

各 Pod のログを確認すると、YAML に書いたメッセージがちゃんと出力されているので、Job が動作したことも確認できました。コンテナをステートレスにしておくと並列実行も容易ですね。

f:id:shiba-yan:20170508012754p:plain

定期的に実行される Job を扱うのは可用性やスケーラビリティの面で結構大変ですが、Kubernetes を使うことで特定のノードに依存することなく実行させることが出来るようになりそうです。

少しコンテナの起動に時間がかかっているのが気になりますが、これは VM サイズの問題な気もします。Windows Server Containers を使う場合には Premium Storage は必須でしょう。

ASP.NET の App Settings と Connection Strings の値を環境変数からオーバーライド可能にする

App Service は Azure Portal から設定した App Settings と Connection Strings の値を、いい感じにオーバーライドしてくれるのが非常に便利だったので、同じように環境変数からオーバーライド出来るようにするライブラリを書きました。主に Docker 向けとして考えてます。

リポジトリは用意しましたが、NuGet とかにはしてないです。今後考えます。

やってることは App Service と同じプリフィックスが付いた環境変数を読み取って、ConfigurationManager の値を書き換えているだけです。App Service がどう実装してるのかは謎です。

Application_Start より早く処理しておきたいので、PreApplicationStartMethod を使いました。

using System;
using System.Collections;
using System.Configuration;
using System.Linq;
using System.Web;

[assembly: PreApplicationStartMethod(typeof(OverrideConfig.OverrideBootstrapper), "Start")]

namespace OverrideConfig
{
    public class OverrideBootstrapper
    {
        public static void Start()
        {
            var environmentVariables = Environment.GetEnvironmentVariables()
                                                  .Cast<DictionaryEntry>()
                                                  .ToDictionary(x => (string)x.Key, x => (string)x.Value);

            foreach (var appSetting in environmentVariables.Where(x => x.Key.StartsWith(AppSettingsPrefix, StringComparison.OrdinalIgnoreCase)))
            {
                ConfigurationManager.AppSettings[appSetting.Key.Substring(AppSettingsPrefix.Length)] = appSetting.Value;
            }

            var connectionStrings = new ConnectionStringSettingsCollectionWrapper(ConfigurationManager.ConnectionStrings);

            foreach (var connectionString in environmentVariables.Where(x => x.Key.StartsWith(SqlServerConnStrPrefix, StringComparison.OrdinalIgnoreCase)))
            {
                connectionStrings.AddOrUpdate(connectionString.Key.Substring(SqlServerConnStrPrefix.Length), connectionString.Value, SqlServerProviderName);
            }
        }

        private const string AppSettingsPrefix = "APPSETTINGS_";
        private const string SqlServerConnStrPrefix = "SQLSERVERCONNSTR_";

        private const string SqlServerProviderName = "System.Data.SqlClient";
    }
}

ループを 2 つ回してるのがちょっとイケてない感じしますが、Connection Strings は後付けだったので暇なときに何とかする方向にします。

AppSettings は読み書きが自由なので問題なかったのですが、ConnectionStrings は読み取り専用になってしまっていて、そのままだとコレクションの操作が出来なかったです。少し調べてみると、リフレクションを使って内部のフラグを書き換えろとありました。

ちょっと無理やりな気もしますが、それ以外は Web.config を物理的に書き換える方法しか見つからなかったので、リフレクションを使って書き換える方法を採用することにしました。

最近は Reference Source のおかげで、こういった内部のフラグを調べるのが楽になりましたね。

https://referencesource.microsoft.com/#System.Configuration/System/Configuration/ConfigurationElement.cs,58
https://referencesource.microsoft.com/#System.Configuration/System/Configuration/ConfigurationElementCollection.cs,29

AppSettings とインターフェースを出来るだけ合わせるために、Wrapper を用意して内部でリフレクションを使ってフラグを書き換えるようにしました。

この時 Collection と Element の両方で書き換え処理が必要なので注意です。

internal class ConnectionStringSettingsCollectionWrapper
{
    public ConnectionStringSettingsCollectionWrapper(ConnectionStringSettingsCollection connectionStrings)
    {
        _connectionStrings = connectionStrings;

        typeof(ConfigurationElementCollection).GetField("bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic)
                                                .SetValue(_connectionStrings, false);
    }

    private readonly ConnectionStringSettingsCollection _connectionStrings;

    public void AddOrUpdate(string name, string connectionString, string providerName)
    {
        var settings = _connectionStrings[name];

        if (settings == null)
        {
            _connectionStrings.Add(new ConnectionStringSettings
            {
                Name = name,
                ConnectionString = connectionString,
                ProviderName = providerName
            });
        }
        else
        {
            typeof(ConfigurationElement).GetField("_bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic)
                                        .SetValue(settings, false);

            settings.ConnectionString = connectionString;
            settings.ProviderName = providerName;
        }
    }
}

既に同名のキーが存在する場合には上書き、存在しない場合には新規追加という風にしました。

最後に簡単に動作確認だけしておきます。今回は以下のような設定を Web.config に追加しました。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="TEST" value="11111"/>
  </appSettings>
  <connectionStrings>
    <add name="DefaultConnection" connectionString="Server=.\sqlexpress;Database=default;Trusted_Connection=True;" providerName="System.Data.SqlClient"/>
  </connectionStrings>
</configuration>

今回、環境変数として登録した値は以下の通りです。プリフィックスは省略しますが TEST / TEST2 / DefaultConnection をオーバーライドするように環境変数を追加してあります。

f:id:shiba-yan:20170506230052p:plain

そして上で作成したクラスライブラリを読み込むようにした ASP.NET アプリケーションを作成して、AppSettings と ConnectionStrings の値を一覧表示したものが以下になります。

f:id:shiba-yan:20170506230116p:plain

正しく環境変数に追加した値が反映されていることが確認できました。面倒だったので載せてはいないですが、Application_Start の時点でもオーバーライドされた値が取れるようになっているので安心です。

これで Windows Server Containers で ASP.NET アプリケーションを動かす時に、環境変数から接続文字列などを渡すことが出来るようになりました。アプリケーション側の改修が要らないのがメリットです。

Azure Container Service Engine を使って Windows と Linux が共存する Kubernetes クラスタを作る

Azure Container Service で Managed Disk を使いたいと思って調べていた時に、Azure Container Service Engine のサンプルに引っかかったので、いろいろと調べてました。

ちょっと前にオープンソース化された ACS Engine ですが、いい感じに ARM Template を作ってくれるだけのコンポーネントで、ECS Agent とは全く異なるものでした。

サンプルの中に Kubernetes のエージェントとして Windows と Linux を同時に扱うものがあったので、面白そうだったので実際にデプロイしてみました。

ACS Engine の準備

まず ACS Engine のリポジトリをクローンしてきます。ドキュメントに書いてあるように Docker を使った方が楽なのでおすすめです。

acs-engine/acsengine.md at master · Azure/acs-engine · GitHub

Docker の環境が出来ていれば devenv.sh を実行して、初回のみ make build を行います。

./script/devenv.sh
make build

これでボリュームがマウントされた ACS Engine の Docker 環境にログインされるので、後は ARM Template を生成すれば終わりです。今回使うのは kubernetes-hybrid.json というファイルです。

acs-engine/kubernetes-hybrid.json at master · Azure/acs-engine · GitHub

dnsPrefix やサービスプリンシパルなどの情報が抜けてるので、acs-engine の実行前に埋めておきます。デフォルトでは Windows と Linux で 2 つずつインスタンスが作られるので注意。

JSON の修正が終われば、そのファイルを acs-engine に食わせるだけです。

./acs-engine ./example/windows/kubernetes-hybrid.json

実行すると Kubernetes のデプロイに必要な証明書など、いろいろと作成してくれます。

f:id:shiba-yan:20170504231858p:plain

Azure へのデプロイに必要な azuredeploy.json と azuredeploy.parameters.json も作成されているので、次はこのファイルと Azure CLI を使ってデプロイを行います。

Azure にデプロイ

デプロイには Azure CLI 2.0 を使いました。手順としては Resource Group を作成して、その Group に対して ARM Template を指定してデプロイを行う形です。

Resource Group や Location は適宜読み替えてほしいですが、大体 2 行で処理は終わります。

az group create --name "Container-RG" --location "Japan West"

az group deployment create --name "kubernetes" --resource-group "Container-RG" \
    --template-file "./azuredeploy.json" --parameters "@./azuredeploy.parameters.json"

parameters に指定するパスの前に付いている @ は必要なので忘れないように。地味にはまりました。

デプロイを実行すると、大量のリソースが作成されます。ACS 自体はリソースとして作成されないです。

f:id:shiba-yan:20170504232226p:plain

これまでと同じように kubectl を使って Kubernetes ダッシュボードからノードを確認しておきます。

f:id:shiba-yan:20170504231512p:plain

名前の付け方やラベルに多少の違いはありますが、1 つの Kubernetes 内で Windows と Linux のノードが動作しています。実は ACS で作ったものより、Kubernetes のバージョンが新しいです。

クラスタが完成したので、実際にアプリケーションをデプロイして確認してみます。

アプリケーションをデプロイ

Windows ノードにデプロイするのは IIS なので、Azure の Kubernetes ドキュメントから拾ってきた YAML をほぼそのまま利用します。変更点としてはイメージを microsoft/iis に変えただけです。

apiVersion: v1
kind: Service
metadata:
  name: win-webserver
  labels:
    app: win-webserver
spec:
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: win-webserver
  type: LoadBalancer
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: win-webserver
  name: win-webserver
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: win-webserver
      name: win-webserver
    spec:
      containers:
      - name: windowswebserver
        image: microsoft/iis
      nodeSelector:
        beta.kubernetes.io/os: windows

これとほぼ同じ内容でイメージを nginx に、nodeSelector を linux に変更したものを Linux ノードにデプロイするために用意しました。

YAML をアップロードしてデプロイを行うと、あっさりと Windows と Linux のデプロイが作成されます。

f:id:shiba-yan:20170504234125p:plain

ちょっと不思議な光景ですが、上手く扱えば開発の自由度向上と運用の手間を削減できそうです。

それぞれのポッドが作成完了になった後に、外部エンドポイントにアクセスするとデフォルトのページが表示されます。イメージのサイズが違うので仕方ないのですが、IIS の方が数倍時間がかかりました。*1

f:id:shiba-yan:20170504234612p:plain

同じネットワーク内にいるので、コンテナ間での通信も問題ないです。サービス名だけで繋がります。

f:id:shiba-yan:20170505000315p:plain

kubectl exec を使うことで、Windows と Linux のポッドに接続できるので、それを使って試しました。

最後に何となく、それぞれのポッドのレプリカを増やして試してみました。

f:id:shiba-yan:20170504234519p:plain

普通の感覚だと 2 ノードでレプリカを 2 つに増やすと、それぞれのノードにデプロイされるはずですが、nodeSelector で OS を指定しているので、ちゃんと同一ノード上にデプロイされました。

実行環境は全て 1 つの Kubernetes にまとめてしまって、ネームスペースで Production / Staging など区別し、アプリケーションは OS 問わず同じクラスタに乗せてしまうというのもありかもしれません。

*1:しかも Windows は Premium Storage を使っているというのに