しばやん雑記

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

AKS を使って Windows Containers 対応の Kubernetes クラスターを作成する

既にいろんな人が AKS を触っているみたいですが、個人的には Windows や ACI と組み合わせて使いたかったので、まずは Windows 向けクラスターが作成できるのか確認しました。

ACS Engine を使って作成していたクラスターの管理が面倒だったので、これを機に入れ替えます。

クラスターを作成する

サポートされているとは明記されてませんが、公式のドキュメントを見ると osType には Windows を指定できるみたいでしたし、ARM Template が ACS Engine とそっくりだったので試したらうまくいきました。

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

Windows と Linux で 2 つの Agent Pool が作成されていることが確認できますね。

最初は Windows のみでクラスターを作成してみましたが、ACS とは異なり AKS では Kubernetes Master 自身で Pod を稼働させることは出来ないみたいです。なので、Windows だけでは必要な Pod が動作しないので Kubernetes Dashboard は稼働しません。

正しく Kubernetes クラスターを動作させるには Windows と Linux のノードが必要です。

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

ARM Template を見ると、複数の Agent Pool を持つことができるのが一目でわかるので、Windows と Linux のハイブリッドなクラスターを用意することが容易です。

ちなみに ARM Template を使うと、ポータルでは対応していない Dv3 / Ev3 な VM を選べます。

Kubernetes Dashboard にアクセスする

AKS の Kubernetes Dashboard にアクセスするには az aks browse コマンドを使いますが、Azure CLI の不具合で Windows だとうまく動作しないので、互換のあるコマンドを使ってアクセスします。

az aks browse も内部的には kubectl を叩いてるだけなので、以下のコマンドで問題ないです。

# kubernetes dashboard の pod 名を取得
kubectl get pods --namespace kube-system --output name --selector k8s-app=kubernetes-dashboard

# dashboard に対してポートフォワーディングを実行
kubectl --namespace kube-system port-forward kubernetes-dashboard-XXXX 8001:9090

ポートフォワーディングのコマンドは、覚えておいても損はないかもしれません。

これで 127.0.0.1:8001 というアドレスで Kubernetes Dashboard へのアクセスが可能となります。

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

ちゃんと Windows と Linux のノードが表示されていることが確認できます。Kubernetes Master のノードは表示されないようになっているので、Pod の実行も出来ないようになっています。

ASP.NET アプリケーションをデプロイする

クラスターが完成したので、例によって AppVeyor でビルドした ASP.NET アプリケーションのイメージをデプロイしたいところですが、なんと AKS で Windows ノードを作成すると、デフォルトで Version 1709 が使われるみたいです。

まだ扱いにくいので 1709 は使ってこないと思ってましたが、ノードの情報を見て驚きました。

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

Version 1709 が使われているということは、Windows Server Containers がデフォルトになっている環境では、1709 の Docker Image を利用したコンテナーしか動作しないことを意味します。

まだ CI SaaS は 1709 に対応したものが存在しないので、仕方なく開発環境に Docker for Windows をインストールして、Windows 10 FCU 上で Docker Image を作成しました。

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

あとはこれまでと同じようにデプロイすれば、Kubernetes 上で ASP.NET アプリケーションが動作します。Docker Image のサイズが 7GB から 3GB に削減されたので、コールドスタートにかかる時間はかなり短縮されました。実用にはまだかかりそうですが、これくらいなら工夫で何とかなりそうです。

Pod をデプロイすれば、あとは Service を追加すれば外部からアクセス可能になります。

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

地味ですが、普通の ASP.NET MVC アプリケーションが AKS 上でも動作しました。

これまでも ACS や ACS Engine を使うことで Windows Containers に対応した Kubernetes 環境を用意することは出来ましたが、AKS はなんて言ってもマネージドサービスなので、今後 GA に向けていろいろと改善されていくはずです。

最近は VMSS に対して Automatic OS Image Upgrade が提供されましたし、AKS も GA までにはアップデート含め面倒を見てくれるサービスになることを期待しています。

Cosmos DB では Repository パターンを使うのが楽だった話

Cosmos DB の API は地味にくせがあるというか、コレクション毎の違いは UriFactory のパラメータ以外あまりなくて、大体は同じような処理を書く感じだったので Repository を用意して共通化しました。

勿論これが正解とか言うわけではなく、あくまでも一例として捉えて貰えばと思って紹介します。実際のところ Cosmos DB に限らない話ではあるんですが、今回は Cosmos DB を使ったというだけです。

今回のケースについて

実際に仕事で Cosmos DB を使ってたわけですが、コレクション数が 10 ぐらいの中規模ぐらい?なアプリケーションでした。Cosmos DB は非正規化されたデータが入るので、SQL の時みたいにコレクションの数が規模に直結しないのが特徴ですね。

公式ドキュメントが揃っているので、この辺りを読んで事前に知識を入れておきました。

Cosmos DB はコレクション間での JOIN とか出来ないですし、LINQ で書いたり SQL で書いたり、UDF やストアドを使ったりといろいろありそうなので、あまりガッツリとラップしない方が良いなと考えました。

Document を用意

Cosmos DB のドキュメントには必ず id が存在するのと、Optimistic Concurrency 用に Etag も欲しかったので、全てのドキュメントが継承する AbstractDocument クラスを用意しました。

相変わらず名前付けが適当ですね。作成・更新日時が欲しかったので日付型を 2 つ用意しておきました。

public abstract class AbstractDocument
{
    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("createdOn")]
    public DateTime CreatedOn { get; set; }

    [JsonProperty("updatedOn")]
    public DateTime UpdatedOn { get; set; }

    [JsonProperty("_etag")]
    public string Etag { get; set; }
}

Cosmos DB に保存するクラスは AbstractDocument を継承するようにします。Entity Framework でも同じような作りにしていた気がするので、個人的にはこういう設計にするのが好きなのかも知れません。

実際に MemberDocument というクラスを用意しました。名前の通り会員の情報を持つクラスです。

public class MemberDocument : AbstractDocument
{
    [JsonProperty("email")]
    public string Email { get; set; }

    [JsonProperty("lastName")]
    public string LastName { get; set; }

    [JsonProperty("firstName")]
    public string FirstName { get; set; }

    [JsonProperty("prefectureId")]
    public int PrefectureId { get; set; }
}

実装とはあまり関係ないですが、全てのプロパティの定義に JsonProperty 付けるかどうかは好みの問題という気もします。とりあえず今回は camelCase に合わせました。

Repository を作成

Entity Framework の時に割と使った気がする Repository ですが、Cosmos DB の場合は用意した方が良いなと今回実際に書いていて思いました。面倒だったのでインターフェースまでは最初から用意しなかったですが、ASP.NET Core の DI と相性は良い感じがしました。

これも Document と同様に AbstractRepository クラスを用意しました。基本的な CRUD 操作と DocumentDB API の操作に必要な情報を提供します。Upsert の方が良いかも知れません。

public abstract class AbstractRepository<TDocument> where TDocument : AbstractDocument
{
    protected AbstractRepository(DocumentClient documentClient, string databaseId, string collectionId)
    {
        DatabaseId = databaseId;
        CollectionId = collectionId;

        Client = documentClient;
    }

    protected string DatabaseId { get; }
    protected string CollectionId { get; }

    protected DocumentClient Client { get; }

    public async Task CreateAsync(TDocument document)
    {
        document.CreatedOn = DateTime.UtcNow;

        var response = await Client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), document);

        document.Id = response.Resource.Id;
    }

    public async Task<TDocument> GetAsync(string id, string partitionKey)
    {
        try
        {
            var response = await Client.ReadDocumentAsync<TDocument>(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, id), new RequestOptions
            {
                PartitionKey = new PartitionKey(partitionKey)
            });

            return response.Document;
        }
        catch (DocumentClientException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
        {
            return default(TDocument);
        }
    }

    public async Task UpdateAsync(TDocument document)
    {
        try
        {
            document.UpdatedOn = DateTime.UtcNow;

            var condition = new AccessCondition
            {
                Condition = document.Etag,
                Type = AccessConditionType.IfMatch
            };

            await Client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, document.Id), document, new RequestOptions { AccessCondition = condition });
        }
        catch (DocumentClientException ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed)
        {
            // optimistic concurrency に失敗
            throw;
        }
    }

    public async Task DeleteAsync(TDocument document, string partitionKey)
    {
        try
        {
            var condition = new AccessCondition
            {
                Condition = document.Etag,
                Type = AccessConditionType.IfMatch
            };

            await Client.DeleteDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, document.Id), new RequestOptions
            {
                AccessCondition = condition,
                PartitionKey = new PartitionKey(partitionKey)
            });
        }
        catch (DocumentClientException ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed)
        {
            // optimistic concurrency に失敗
            throw;
        }
    }
}

何の変哲もない、基本的な実装のみ提供しています。特に説明も要らない気はしますが、Create 時に生成された Id を呼び出し元に反映するようにしました。この辺りは Entity Framework 的ですね。

更新処理はデフォルトで Optimistic Concurrency を有効にしています。同時実行制御を無効にする必要って正直あまりないかなと考えたので、こういう実装にしました。本来は専用の例外を用意するべきでしょうね。

そして先ほど作成した MemberDocument に対する Repository を作成してみます。

public class MemberRepository : AbstractRepository<MemberDocument>
{
    public MemberRepository(DocumentClient documentClient, string databaseId)
        : base(documentClient, databaseId, "Member")
    {
    }

    public async Task<MemberDocument> GetByEmailAsync(string email)
    {
        var feedOptions = new FeedOptions
        {
            MaxItemCount = 1,
            EnableCrossPartitionQuery = true
        };

        var documentQuery = Client.CreateDocumentQuery<MemberDocument>(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), feedOptions)
                                    .Where(x => x.Email == email)
                                    .AsDocumentQuery();

        return (await documentQuery.ExecuteNextAsync<MemberDocument>()).FirstOrDefault();
    }
}

コンストラクタでは DI で DocumentClient が注入されることを期待していて、databaseId は IOption とかで渡せば良いと思います*1。会員情報というのはメールアドレスで引きたいことが多いと思うので、専用のメソッドを追加しています。

パーティションとかはサンプルに付き検討してないので、とりあえず EnableCrossPartitionQuery = true でお茶を濁しました。この場合はパーティション固定でも良いかも知れません。

使い方など

ASP.NET Core アプリケーションで使う場合には DI を使って簡単に対応できます。ConfigureServices で DocumentClient と作成した Repository を Singleton として追加します。

本当なら接続先やプライマリキーは IConfiguration から取るようにしましょう。

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(provider =>
        {
            var connectionPolicy = new ConnectionPolicy
            {
                ConnectionMode = ConnectionMode.Direct,
                ConnectionProtocol = Protocol.Tcp
            };

            return new DocumentClient(new Uri("https://xxxx.documents.azure.com:443/"), "PRIMARYKEY", connectionPolicy);
        });

    services.AddSingleton<MemberRepository>();

    services.AddMvc();
}

必要なクラスを DI に登録したので、後はコントローラのコンストラクタで Repository を受け取るだけです。

テストなので大したコードではないですが、Repository を使うことでコレクション単位でのアクセスは扱いやすくなりました。裏側が SQL Database に変わっても違いはあまりないでしょう。

public class DemoController : Controller
{
    public DemoController(MemberRepository memberRepository)
    {
        _memberRepository = memberRepository;
    }

    private readonly MemberRepository _memberRepository;

    public async Task<IActionResult> Index()
    {
        var kazuakix = await _memberRepository.GetByEmailAsync("me@kazuakix.jp");

        kazuakix.LastName = "syachiku";

        await _memberRepository.UpdateAsync(kazuakix);

        return View();
    }
}

実際に仕事で Cosmos DB を使った時には Repository の上に Service 層を追加する形で実装しました。最近の仕事の中では上手く実装できた方かなと自分では思ってますが、次はもっと上手く作れそうです。

とまあ、長々と書いてきましたが、こういうのは既にみんな作ってますよね。私の Cosmos DB の初心者っぷりを発揮したところで、今回は終わりにしたいと思います。

*1:CollectionId と同様に定数でも良いのではないかとも思う

App Service on Linux を本番環境で運用する際の注意点

最近は App Service on Linux を使った仕事もしていたので、本番環境で使う場合の注意点を簡単にまとめておきます。地味にはまるポイントが多くて忘れそうなので、ほぼ自分用のメモですね。

2 コア以上のインスタンスを選ぶ

1 core 1.75GB の B1 / S1 インスタンスから選べますが、明らかにコンテナの起動時間が遅いので最低でも B2 / S2 を選ぶべきです。これは本番環境だけではなく、テスト用でも同じです。

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

CPU リソースというよりもメモリが足りていない気もするので、Premium V2 が App Service on Linux でも使えるようになれば幸せになれそうです。

Docker Image のタグを必ず指定する

App Service on Linux はホスト側のメンテナンスなど、様々な事情で実行中のコンテナが再起動されることがあります。なので、タグを指定せずに latest のまま使うと、再起動時に pull が走って意図せずバージョンアップしてしまいます。

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

ACR はタグを指定しないと設定できないので良いですが、Docker Hub を使っている場合には注意です。

当然ながら CI で Docker Image を作る場合にもビルド番号などでタグを指定します。

Always On は有効化する

App Service on Linux の仕組みとして、最初にリクエストがあったタイミングでコンテナが起動されるので、初回アクセス時に割と時間がかかってしまいます。

なので、Always On を有効にして、出来るだけコンテナを常に立ち上げた状態にしておきます。

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

このあたりは Windows と同じですね。設定することで意図しない再起動の影響を最小限に出来ます。

Staging Slot を使って Swap でリリースする

軽量な Alpine ベースの Docker Image を使っている場合には気にならないぐらいの時間でしょうが、ASP.NET Core アプリケーションの Docker Image は 100MB を超えてしまうので、pull で時間がかかります。

Image 自体は App Service on Linux 側にキャッシュされるので、Staging Slot を使って時間のかかる pull を回避しつつ、リリースする方法を取る必要があります。

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

Swap を行うと、今の実装では Production / Staging Slot 共に新しいコンテナが起動されて、起動完了後に入れ替わるという仕組みみたいなので、両方で同じバージョンになるタイミングが発生します。

両方の Slot 設定をキッチリと合わせていても、新しいコンテナが必ず起動されるみたいなので、暖めて出すという使い方が出来ないのが少し残念ではあります。

ACR を利用した CD を使わない

Azure Container Registry の Webhook を使った CD 機能は非常に便利そうに思うのですが、Webhook の実装がコンテナを再起動するだけという、非常に残念なものになっています。

なので、CI では常に同じタグを push せざるを得ず、Swap のタイミングで再起動されるという実装と相まって、Swap を実行すると両方の Slot が最新になるという致命的欠点があります。

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

ACR 連携の CD は Webhook が push された Docker Image とタグの情報を読み込んで、その通りに Docker Image を更新するようになるまで使い物にならないと考えた方が良いでしょう。

Azure CLI を使った更新や、VSTS のタスクでは Docker Image 名を変更するので、こういった問題は発生しません。Docker Image の情報は ARM 側に持っているっぽいので、Webhook での対応は望み薄です。

Visual Studio Team Services と Web App for Container を使用した ASP.NET Core アプリケーションの CI/CD

最近は仕事で Visual Studio Team Services と Web App for Container を使っていましたが、地味にデプロイ周りで苦労をしたので軽くまとめておきます。

デプロイだけでは面白くないので、ASP.NET Core アプリケーションのビルドから行ってみます。Web App for Container には VSTS を使った CD を設定する機能がありますが、実際に試してみたところ中途半端な感じだったので、手動でビルドタスクを組みました。

VSTS で必要なものは Docker でタスクを検索すると出てきます。

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

そして ASP.NET Core アプリケーションのビルドに必要なものは、Visual Studio 2017 で Docker サポートを追加すると全て追加されるので、準備することはあまりありません。

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

VSTS には .NET Core のビルドを行うタスクもありますが、現在のビルドワーカーに入ってるランタイムバージョンなどを調べるのが面倒だったので、Docker Image を使ってビルドを行います。

予めリポジトリを作成して、ASP.NET Core アプリケーションのソースがプッシュされている前提で進めます。あとビルドタスクはテンプレートを使いません。

ASP.NET Core アプリケーションのビルド

Visual Studio 2017 で Docker サポートを有効にしてプロジェクトを作ると、Docker Compose を利用したビルド用の yaml がいくつか追加されます。ちゃんと CI 用の定義も用意されているので、これを使います。

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

Docker Compose がインストールされている環境であれば、CI 用の定義を読み込んで docker-compose up すればアプリケーションのビルドが完了します。非常にシンプルで良いですね。

出力先は /obj/Docker/publish 固定になっていますが、ここは絶対に変更してはいけないです。

version: '3'

services:
  ci-build:
    image: microsoft/aspnetcore-build:1.0-2.0
    volumes:
      - .:/src
    working_dir: /src
    command: /bin/bash -c "dotnet restore ./WebApplication20.sln && dotnet publish ./WebApplication20.sln -c Release -o ./obj/Docker/publish"

ここで分かりにくいのが microsoft/aspnetcore-build:1.0-2.0 というタグのイメージを使わないと Visual Studio の Docker 向け SDK がインストールされておらず、ビルド時にエラーとなってしまうので注意。

素直に Visual Studio 2017 で生成されたまま使うのが正解という話でした。

VSTS では Docker Compose タスクを追加して、Action を "Run service images" に、Docker Compose File を docker-compose.ci.build.yml に変更するだけです。

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

ルートにあるので ** は必要ないですが、あっても害はないので追加したままにしてます。

Docker Image のビルドと ACR へのプッシュ

アプリケーションのビルド後は Docker Image を作成します。デフォルトで生成されている Dockerfile は、ちゃんと Docker Compose を使ってビルドされた前提のパスになっているので簡単です。

FROM microsoft/aspnetcore:2.0
ARG source
WORKDIR /app
EXPOSE 80
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "WebApplication20.dll"]

VSTS では Docker タスクを追加して、Action を "Build an image" にするだけです。Dockerfile のパスは最初から適切な値になっているはずなので、特に弄る必要はありません。

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

Docker Image のビルド後はそれを ACR にプッシュする必要がありますが、これも Docker タスクを追加して Action を "Push an Image" に変えるだけで完了です。GUI での設定なので、あまりやることがないですね。

App Service へのデプロイ

ASP.NET Core アプリケーションが含まれている Docker Image はここまでで ACR までプッシュされているはずなので、最後は Web App for Container にデプロイを行います。

VSTS には非常に便利な Azure App Service Deploy というタスクがあるので、これを利用します。説明文をよく読むと Linux にも対応していると書いてありました。

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

App Service on Linux では Staging スロットを使ってスワップする運用が必須なので、デプロイ先のスロットの選択が簡単に行えるのは非常に良いです

CircleCI で同じようにデプロイした時はサービスプリンシパルを用意したり、Azure CLI をインストールしたりと手間がかかりましたが、VSTS だとポチポチと設定すれば終わるのが楽ですね。

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

今回作成した VSTS のビルド定義は上のようになりました。ビルドからデプロイに必要なのは 4 ステップだけというシンプルさです。App Service へのデプロイタスクのおかげですね。

ビルドを行いデプロイを確認

最後にちゃんとビルドを実行して、Web App for Container にデプロイされることを確認しておきました。

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

VSTS は Docker Image をキャッシュしてくれないようで、アプリケーションのビルドに地味に時間がかかりましたが、それでも 3,4 分で完了しました。aspnetcore-build とかは予め持っておいて欲しいですね。

Azure Portal で staging スロットにデプロイされている Docker Image を確認すると、ちゃんとビルドされたバージョンに切り替わっていました。

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

ここで ACR 扱いになっていない場合は、ACR の Admin User が有効になっていないか、Web App に ACR の情報が設定されていないかのどちらかです。予め設定しておく必要があります。

おまけ:自動でスワップを行って本番リリース

VSTS には App Service のスロットをスワップするタスクもあるので、これを使えば新しい Docker Image をデプロイした後、自動的にスワップを実行してリリースすることも出来ます。

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

Source slot を指定してビルドすれば、Production とスワップされて最新のビルドがリリースされます。

Windows 版の App Service では Auto Swap という機能がありましたが、同じことを Web App for Container でも実現できます。癖のあるデプロイ周りですが、便利に使っていきましょう。

Cosmos DB を利用する上で最初にはまった部分のメモ

Twitter でおーみさんに Cosmos DB についていろいろ聞いたので、忘れないようにメモっておきます。kyrt.in に書いてあればみんな幸せになりそうなんですが、いつ書いてくれるかわからないので。

HTTP GW ではなく TCP Direct を使う

デフォルト設定のまま Cosmos DB を使っていると、どうも遅いような気がしたので聞いたところ、TCP Direct を使うように設定を変えるのが良いと教えて貰いました。

DocumentClient を作るときに ConnectionPolicy を渡してやればよいです。

var connectionPolicy = new ConnectionPolicy
{
    ConnectionMode = ConnectionMode.Direct,
    ConnectionProtocol = Protocol.Tcp
};

var documentClient = new DocumentClient(new Uri("ENDPOINT"), "PRIMARYKEY", connectionPolicy);

これで TCP Direct モードが使われるようになるようです。デフォルトを TCP にしておいて欲しいですね。

DocumentClient はシングルトンに

TCP Direct にしてもまだ遅いような気がしたので、更におーみさんに聞いてみました。DocumentClient は TCP Direct の場合はコネクションをプールするらしいので、シングルトン推奨のようです。

確かに毎回接続を作り直して、初期情報を受信してたら遅いよねという感じです。

幸いにも今回は ASP.NET Core アプリケーションで Cosmos DB を使っていたので、DI を使って簡単にシングルトンを実現出来ました。Factory を使って実現します。

services.AddSingleton(provider =>
{
    var connectionPolicy = new ConnectionPolicy
    {
        ConnectionMode = ConnectionMode.Direct,
        ConnectionProtocol = Protocol.Tcp
    };

    return new DocumentClient(new Uri("ENDPOINT"), "PRIMARYKEY", connectionPolicy);
});

使う場合にはコンストラクタで DocumentClient を受け取るようにするだけです。

public class DemoService
{
    public DemoService(DocumentClient documentClient)
    {
        // documentClient は DI で解決される
    }
}

これでアプリケーションで 1 つの DocumentClient を利用することが出来ます。慣れてくると ASP.NET Core の DI は結構使いやすく感じるようになってきました。

Optimistic Concurrency は ETag を使う

データを扱う上で大体問題になってくるのがロックとかなんですが、Cosmos DB には当然ながらロックとか用意されていないので、必要な場合は Optimistic Concurrency を使って制御を行うことになります。

適当に調べた感じではヒットしなかったので、おーみさんに聞いて ETag を教えて貰いました。

Cosmos DB のドキュメントには全て ETag が含まれていて、Replace 時に AccessCondition に ETag を設定しておくと、一致した時のみ処理を実行するように出来るみたいです。考え方は HTTP 的ですね。

try
{
    var condition = new AccessCondition
    {
        Condition = document.etag,
        Type = AccessConditionType.IfMatch
    };

    await Client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, CollectionId, document.id), document, new RequestOptions { AccessCondition = condition });
}
catch (DocumentClientException ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed)
{
    // optimistic concurrency で失敗
}

処理に失敗した時には PreconditionFailed がステータスコードとして返ってくるので、何かしら処理が必要な場合は行います。新しく追加された catch ~ when は地味に便利でした。

今回のアプリケーションでは Repository Pattern を使って処理をまとめていたので、全ての更新処理を対象に Optimistic Concurrency を入れることが容易でした。

暇になった時にでも、どのように Cosmos DB への処理部分を書いたかまとめたい気がします。

App Service on Linux と Azure Container Registry を使ったデプロイを試したら罠が多かった話

App Service on Linux が GA する少し前に Azure Container Registry を使った自動デプロイが、Azure Portal から簡単に設定できるようになったとブログに上がってました。

Azure Portal から Continuous Deployment を On にすると Webhook の設定をしてくれます。

ドキュメントも用意されてますが、ACR 連携に関しては手動で Webhook を作る必要があると書いてあるので、まだ最新にはなっていないようです。

ACR への Push をトリガーにデプロイが行えると非常に捗りそうなので、いろいろと調べていました。しかし何故か上手くいきませんでしたが、最近いろいろと解決して動作するようになったので書きます。

Webhook はコンテナを再起動するだけ

Docker Hub や Azure Container Registry との連携用に用意されている Webhook は非常に単純な機能しか持っておらず、呼び出されたらコンテナを再起動するだけとなってます。

この挙動は Kudu の実装を見るとすぐにわかります。コンテナの再起動なので、毎回 Docker Image のタグを変更するような運用の場合は正しく動作しません。

タグを変える場合には Azure CLI を使ってコンテナイメージを切り替えるといった、これまで通りの処理が必要になります。CircleCI を使って実現する場合の例は以下の通り。

Docker Image のプッシュ先を ACR に変えるだけですが、サービスプリンシパルを作ったり、Azure CLI をインストールしたりと地味に面倒です。

Docker Image のタグは固定する

Webhook は設定を切り替えてくれることはないので、バージョン管理がそのままでは行えません。

仕方ないので App Service on Linux で Docker Hub や Azure Container Registry を使った CD を行う場合には、バージョン別のタグと latest を含めるようにします。

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

docker tag を使ってやれば良いだけです。バージョン別のタグを持っているので、最悪戻したい時には latest に push し直したり、設定からイメージを指定し直せばよいかなと思います。

latest タグを含めているので、App Service on Linux の設定からは latest を選びます。

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

この状態で Continuous Deployment を On にすると Azure Container Registry に Webhook が作成されます。今は Web App 名にハイフンが含まれていると上手く作成されないらしいので注意。

スコープとして Web App に設定した Image と Tag の組み合わせが設定されます。

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

この状態で latest タグに対してプッシュすると Webhook が実行されて、App Service on Linux 側でコンテナが再起動されます。このときに新しいイメージが pull されます。

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

なのでしばらくすると新しいコンテナが起動してデプロイが完了するという仕組みです。サービスプリンシパルや Azure CLI が必要なくなったので便利ですが、固定のイメージしか使えないのは少し不便だと思います。

Webhook のペイロードにはイメージの情報が含まれているので、それを読み取ってくれると思っていましたが当てが外れました。フィードバックしていきたい部分です。

Azure B-Series VM の挙動と CPU クレジットの仕様を確認する

Azure サポートに B-Series の core 申請を投げていたのが通ったので、前回の続きとして実際に B-Series VM を作成して調べてみることにします。

基本的な B-Series と t2 に関する違いは前回のエントリを見てください。

まずは個人的に AWS でよく使っている t2.small と同じサイズの B1ms の VM を作ります。

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

プレビュー中ですが、1 時間当たりの金額はかなり安いです。コスパが非常に高いです。

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

作成した B-Series VM の CPU クレジットは Azure Monitor から 1 分間隔で確認できるようになっていました。CloudWatch は 5 分だった気がするので、Azure のが少し細かいですね。

CPU クレジットとは

まだ Azure の公式ドキュメントには CPU クレジットについては言及がないので、AWS t2 インスタンスの方を読んだ方がいいかも知れません。考え方はほぼ同じになっているはずです。

CPU クレジットとは何ですか。

1 個の CPU クレジットは、1 台の vCPU を使用率 100% で 1 分間実行することに相当します。たとえば、1 台の vCPU を使用率 50% で 2 分間実行したり、2 台の vCPU を使用率 25% で 2 分間実行したりなど、他の vCPU、使用率、時間の組み合わせでも、1 個の CPU クレジットに相当します。

T2 インスタンス - Amazon Elastic Compute Cloud

t2 と同じように複数の vCPU がある場合には少し計算が分かりにくいかも知れません。

バーストと CPU クレジットの使用

B1ms はベースラインパフォーマンスが 20% ですが、CPU クレジットが残っている場合には 100% まで利用することが出来ます。実際に負荷を掛けてみると 100% まで問題なく使われます。

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

Azure Monitor で CPU クレジットを確認すると、CPU クレジットが消費されていることが分かります。

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

何故か 1 vCPU を 100% 使用しても、CPU クレジットは 0.5 しか消費されませんでした。これは B-Series の不具合っぽいので、質問を投げていますがよくわかりません。追記:直ってました。

ちなみに Azure Monitor 上は CPU 使用率も 50% になっていました。明らかにおかしいですね。

ベースラインパフォーマンスの確認

今は CPU クレジットの消費が何故か半分になっているので、なかなか CPU クレジットを消費しきれませんでしたが、数時間かけて何とかクレジットを使い切ることが出来ました。

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

しかし、CPU クレジットが完全に 0 になることはありませんでした。

そしてベースラインパフォーマンスにまで落とされていても、タスクマネージャー上は 100% のままになります。正しい値を読み取るには Azure Monitor から参照する必要があります。

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

ベースラインパフォーマンスが 20% とはいえ、ハイパーバイザによって 20% きっちりに上限が設定されるわけではないみたいです。平均値が 20% になるように調整されているように見えます。

とはいえ、Azure Monitor の値に不安があるので再検証が必要な気もします。

VM の再起動と CPU クレジット

t2 インスタンスは VM を再起動すると貯まっていた CPU クレジットが初期化されるようになってますが、Azure B-Series でも同様の挙動となっています。

たまたま何故かインスタンスが再起動した結果、Azure Monitor の値にその痕跡がありました。

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

CPU クレジットが 30 を下回っていましたが、再起動後には 30 に戻っていることが分かります。この挙動を知らないと不具合かと思ってしまいそうですが、多分これは意図した動作です。

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

逆に CPU クレジットが初期クレジット以上に貯まっていても、再起動で初期化されています。

インスタンスサイズと初期 CPU クレジット

Azure のドキュメントには初期 CPU クレジットが記載されておらず、Twitter ではいろいろと話が飛び交ってましたが、実際に別のインスタンスサイズで作って確認します。

申請時に 8 cores しか要求しなかったので、仕方なく Standard B4ms を作りました。

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

4 vCPU と 16GB メモリの仮想マシンがプレビュー中は 7000 円程、GA になっても 14000 円程で使えるのは非常に嬉しいですね。本当にもう A-Series VM は要らないです。

Azure Monitor で確認すると、120 CPU クレジットが与えられていました。つまり 1 vCPU あたり 30 CPU クレジットが予め与えられるようです。

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

このあたりの仕様も AWS の t2 インスタンスと同じようなので、ある意味予想通りという感じです。

気になる部分については確認しましたが、バースト周りの仕様については t2 と同じようです。ただし Azure B-Series はストレージパフォーマンスにも言及されてるので、そこは注意したいです。

追記:CPU クレジットの消費が直ってました

起動したまま放置していた 1 vCPU を持った B-Series インスタンスがあったので、久し振りに負荷を目いっぱいかけてみたところ、正しく CPU クレジットが消費されるようになっていました。

以前は CPU 使用率が 100% であっても、CPU クレジットは 0.5 しか消費されていませんでしたが、今は正しくほぼ 1 が消費されていることが確認できます。

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

これまでは 0.5 CPU クレジットしか消費されなかったので、ある意味お得ではあったのですが、ちゃんと Azure Monitor で正しい値が確認出来るようになって良かったです。

バースト可能な Azure B-Series と AWS t2 インスタンスを比較する

前に Burstable なインスタンスが予告されてましたが、プレビューでリリースされたようです。

大体の Web サーバーや開発用インスタンスでは常に高い CPU パフォーマンスが必要なわけではないので、便利に使えるはずです。実際に AWS では t2 を検証などでよく使っています。

まだ利用可能なリージョンは少ない上に、クオータの申請をしないと使えないのでまだ試せてないのですが、明らかに AWS の t2 インスタンスを意識したものだと分かりますね。

実際に起動可能になるまで時間がかかりそうなので、まずはドキュメントレベルで比較してみます。Azure と AWS それぞれのドキュメントは以下の通り。

結論を先に書くと B-Series の仕様は t2 インスタンスとほぼ同じのようです。

CPU クレジットという概念があり、1 時間ごとにインスタンスのサイズによって貯まる量が決まり、クレジット自体の上限値もある。という形です。初期クレジットが Azure は記載されてませんでした。

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

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

Azure の B-Series はストレージのパフォーマンスが、インスタンスサイズに左右されるようですが、AWS 側は特に書かれていません。NIC の帯域の問題だと思うので、実際に計れば同じぐらいなのかもしれませんが。

とりあえず Azure B-Series と AWS t2 インスタンスの対応関係は以下の通りです。t2.nano に相当する B-Series は用意されてないですが、0.5GB メモリは流石にしんどそうなのでまあいいかなと思います。

Azure B-Series AWS t2 インスタンス
1 vCPU / 0.5GB 該当なし t2.nano
1 vCPU / 1GB Standard_B1s t2.micro
1 vCPU / 2GB Standard_B1ms t2.small
2 vCPU / 4GB Standard_B2s t2.medium
2 vCPU / 8GB Standard_B2ms t2.large
4 vCPU / 16GB Standard_B4ms t2.xlarge
8 vCPU / 32GB Standard_B8ms t2.2xlarge

Azure の方は名前が分かりにくいのはこれまで通りという感じですね。きっちり t2 に合わせてきてます。

大きな仕様としては変わりないですが、細かい部分を見ていくと違いが出てきました。気になるのは使われる CPU の種類が Azure は型番まで明記されているところで、Haswell ベースの Xeon が使われるようです。

  • Azure
    • Intel® Haswell 2.4 GHz E5-2673 v3 processors or better
  • AWS
    • 高速な Intel Xeon プロセッサ

AWS は既にリリースされてから期間が経ってるので、リージョンなどでいろんな CPU が選ばれるようになっているみたいです。なので Xeon としか書かれていませんでした。

気になる金額ですが、Azure はプレビュー中なので勿論 50% 引きになってますが、GA した時は Linux / Windows の両方とも t2 インスタンスと同じ価格になるようです。

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

個人的には B-Series が GA になったタイミングで A / Av2 はお役御免という感じがします。

コストパフォーマンスは A / Av2 は B に勝てる気がしないのと、元々 A / Av2 はテスト利用前提みたいな触れ込みなので、B を使った方がコスト圧縮と高パフォーマンスを実現出来そうです。

B-Series が作れるようになったら、もうちょっと詳細に調べてみようと思います。

App Service on Linux と Web App for Containers が GA したらしい

発表も突然なら GA も突然な App Service on Linux です。てっきり Ignite 合わせかと思っていましたが、特に何かあるタイミングではない今日に発表とは驚きました。

とはいえ、何か機能が追加されたわけではなく GA したという発表だけのようです。

同時に謎めく Web App for Containers というサービスが発表されています。

最近追加された App Service on Linux の機能は以前まとめたので、そっちを見てもらえればよいかと思います。内容的にそろそろ GA は近いかなと思っていましたが、予想より早かったです。

App Service on Linux に関してはちょっと前にフォーラムでバグ報告していたので、それが直っているか確認する作業が必要になりました。直っているといいなと思います。

Web App for Containers

突然出てきた Web App for Containers ですが、名前だけ聞くと Windows Containers にも対応したのかなと思いましたが、Linux のみの対応となります。というか、中身は App Service on Linux です。

では何故わざわざ名前を変えてリリースされたかですが、App Service on Linux に対しては大きく分けて 2 つの機能を期待する人が存在するからでしょう。

1 つは Windows の App Service と同じように GitHub や FTP を使ったアプリケーションのデプロイが行えて、Linux で動作する環境。もう 1 つは Docker Image を指定するだけで Web アプリケーションとして実行してくれる環境です。

Azure Portal から作成する時に Web App for Containers は Built-in Runtime Stack を選べなくなっています。その代わり Docker Image は必ず指定する必要があります。

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

Web App for Containers として作成した場合は Deployment options は選べないようになっているので、GitHub や BitBucket からのデプロイは設定できないようになっています。

なので Azure Blog に書いてあった通り、CI/CD に関しては Azure Container Registry や Docker Hub を使った自動デプロイか、Azure CLI からアップデートする方法を選ぶことになります。

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

ちなみに Application settings を見ると面白い設定値が追加されていました。このキーの有無が App Service on Linux と Web App for Containers の切り替えを行っている気がします。

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

このキーを false にすると /home/site が自動でボリュームとしてマウントされなくなりそうなので、SSH で接続して確認してみたところ、予想通りマウントされていなかったです。

個人的には Web App for Containers で Windows が使えるようになって欲しいです。

App Service on Linux に関する最近のアップデートなど

最近は忙しかったのと Windows Containers ばかり弄っていたので遠ざかってましたが、そろそろ App Service on Linux は GA が近くなってきた気がしますね。リージョンは毎週のように増えていますし。

というわけで個人的なまとめを少しだけ。気になる部分は試していくことにしています。

Japan West が追加

7 月には Japan East が追加されましたが、ちょっと前に Japan West でも使えるようになりました。

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

新しめのサブスクリプションでは Japan West に作れない気がしますが、何はともあれ App Service on Linux は Japan East / West の両方で使えるようになっています。

本社からの日本への期待が高いということなのかもしれません。

Node 8.0 / 8.1 がサポート

カスタム Docker Image を使っている場合には関係ないですが、標準のランタイムスタックで Node 8.0 と 8.1 に対応していました。GitHub を使った CI / CD で簡単に使えるはずです。

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

.NET Core 2.0 のサポートはまだのようですが、こっちもそのうち対応するのではと思います。

Docker のログが大幅に改善

これまで App Service on Linux で致命的だと思っていた Docker ログの確認が非常に難しいという問題が、ついに解消していました。ファイル名は日付 + マシン名となっていて、スケールアウト時にも安心。

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

欲を言えばディレクトリを切って欲しいとか、Azure Storage への転送が欲しいとかありますが、そのうち対応してくれればいいかなという気持ちです。

ログにはタイムスタンプが付いているので、トラブルシューティングが格段に行いやすくなりました。

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

何気に docker run のコマンドも出力されているので、挙動を調べるのにも便利です。

そして Docker Container のログ出力にもいつの間にか対応していたみたいです。

Azure Portal を見ると確かに設定が増えているので有効にすると、アプリケーションが標準出力に書き出している情報も、上のログファイルに出力されるようになりました。

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

ただし、現状は一度有効にすると無効に出来ないみたいなので、フォーラムで問合せしています。

Azure Container Registry との連携強化

これまでも Docker Hub からの CD には対応していましたが、Azure Container Registry とも CD 出来るように強化されています。Azure サービスとの連携は強化していってほしいです。

Container Registry を選ぶと、自動的に格納されているイメージやタグがドロップダウンリストで表示されるので、かなり設定が行いやすいです。なお ACR の Admin は有効化必須です。

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

CD を試してみましたが、思ったような挙動にならなかったので、これもまたフォーラムで聞いています。解決策が分かればブログでまたまとめてみようかと思います。

久し振りに触ってみた感じとしてはインフラ周りの挙動は大幅に安定していました。早く仕事で使いたい。