オンデマンドにCloudServicesをデプロイしてみたけど大変な目にあったよって話


# 初めに
Azureには様々なPaaSやらIaaSがあります。

我が部署はみんなAzureが好きなもんですから、日々便利に組み合わせて使い倒しています。

そんなたくさんあるサービスの中でもVM並みに古参なのが「CloudService」です。

ロードバランサ―、スケールコントローラー付きで扱いやすくとっても便利なやつです。

VMにも可用性セットが追加されてスケール制御ができるようになりましたが、

まだまだCloudServiceの方が手軽で便利かなと感じます。

今日はそんなCloudServiceを「必要な時にだけデプロイして、不要な時は削除してしまう（で、課金を発生させない）」

というちょっとユニーク（ニッチ？）なことに取り組んだ時のお話です。

# なんでそんなことをやろうと思ったのか
うちの部署は「配信事業本部」という名前の通り、音楽や動画の配信に携わっています。

構築済みのシステムは大体どれもCloudServiceにWorkerRoleをデプロイしてその中でパッケージングやトランスコードをしています。

パッケージングもトランスコードもエンコードに比べると低負荷なので、

CloudServiceのVMサイズは大抵そこまで大きいものにしません。

（D1v2(ディスクは50GB)とか、そんなもん）

が、たまーに「6時間の超大作」とか「4Kの動画」とか「ぶっ続け〇〇24時間」とかをパッケージングすることがあります。

「ぶっ続け〇〇24時」なんて1ファイルしかないのに40GB(!) とか80GBとか(!!)になることもあり、

とてもとてもD1v2じゃディスク足りない、だけどCloudServiceはVMみたいにディスクをアタッチできない。

仕方ないから一時的にでかいサイズのCloudServiceをデプロイしようということに。

でも、これ、いちいちやるのめんどくさい、いやだ…。

# じゃぁ自動化だ！
というわけで作ってみる

作ったのはこんな仕組み

1.処理要求が入る
2.1.が来た時に対象のCloudServiceにインスタンスが1個もなかったら、ストレージに置いてあるcspkgとcscfgを使ってデプロイ

一番大事なデプロイする部分のコードはこんな感じ

```C#
// デプロイパラメータの生成
var parameters = new DeploymentCreateParameters
{
    Label = $"{CloudServiceName} - {DateTime.UtcNow:yyyy/MM/dd hh:mm:ss}", // ラベル適当
    Name = Guid.NewGuid().ToString("N"), // 名前も適当
    PackageUri = new Uri(packageUri), // cspkgのストレージ上のURI
    Configuration = configContent.TrimStart('\uFEFF'),  // cscfgの設定箇所。cscfgファイルを一度Stringにして、BOM付きの場合は削除して設定する
    StartDeployment = true,
};
// デプロイ実行 ComputeManagementClientを以下の様に利用する
var response = managementClient.Deployments.Create(CloudServiceName, deploymentSlot, parameters);
```

※[Compute Management Library](https://www.nuget.org/packages/Microsoft.WindowsAzure.Management.Compute/)が必要なのでNuGetしてきてくださいね。

晴れて自動化…と思ったら、あれ？テーブルストレージにログが出なくなったぞ？

# 原因はDiagnostics
ポータルで見てみるとどうも[Azure Diagnostics の拡張機能](https://blogs.msdn.microsoft.com/windowsazurej/2014/09/08/azure-diagnostics-microsoft-azure-virtual-machines/)がインストールされていない模様。

でもまぁ、拡張診断機能をインストールすればいいだけだもんな。

Visual Studio使って手動でデプロイするときはちゃんとインストールされているんだから、

RESTAPIはあるはずだし、SDKやLibrary使えばインストールできるだろ！

…そんな風に思っていた時期が私にもありました。

# 世の中そんなに甘くない
そもそも、何のクラスを使ったらいいかわからない。

[私の上司](http://qiita.com/ns-teruyama/items/f3cd2a778499efb07917)がComputeManagementClient.HostedServices.AddExtensionが

拡張機能をインストールするメソッドらしいことを突き止めてくださったが、肝心のパラメーターが全く謎。

[RESTAPIのリファレンス](http://msdn.microsoft.com/en-us/library/windowsazure/dn169558.aspx)読んでもﾜｶﾝﾈ

Optionalなのに値を設定しないでリクエストするとバンバン400を突き返される。

…どういうことなの。

散々悪戦苦闘して、ようやく以下(やや長いっす)の通り拡張機能をインストールすることに成功。

```C#
// 拡張機能インストール用に使うサービス証明書をクラウドサービスに設定する
var certificateByteArray = Convert.FromBase64String(deployParameters.DiagnosticsExtensionBase64Certificate); // オレオレでもいいので証明書が必要、あらかじめ用意しておく
var thumbprint = new X509Certificate2(certificateByteArray, deployParameters.DiagnosticsExtensionCertificatePassword).Thumbprint; // 証明書がすでにあるかどうかを確認するにはThumbprintが必要なのでここで取得
try
{
    // サービス証明書がすでにあるかどうかを確認する
    var getCertificateResponse = managementClient.ServiceCertificates.Get(
        new ServiceCertificateGetParameters(
            CloudServiceName, 
            deployParameters.DiagnosticsAddExtensionParameter.ThumbprintAlgorithm, 
            thumbprint));
}
catch (Hyak.Common.CloudException ex)
{
    // サービス証明書がない場合、証明書を登録する
    var addCertificateResponse = managementClient.ServiceCertificates.Create(
        CloudServiceName, 
        new ServiceCertificateCreateParameters()
        {
            CertificateFormat = CertificateFormat.Pfx,
            Data = certificateByteArray,
            Password = deployParameters.DiagnosticsExtensionCertificatePassword
        }
    );
    if (addCertificateResponse == null || addCertificateResponse.Status != OperationStatus.Succeeded)
    {
        throw new DeploymentException("Create", CloudServiceName, ServiceDeploymentSlot, addCertificateResponse);
    }
}
deployParameters.DiagnosticsAddExtensionParameter.Thumbprint = thumbprint;

// PaaSDiagnostics.*.PubConfig.xmlのテキストを取得 要するにWadConfigのこと
var diagnosticsPublicConfigBlob = blobContainer.GetBlockBlobReference(deployParameters.DiagnosticsExtensionPublicConfigurationPath);
var diagnosticsPublicConfigContent = diagnosticsPublicConfigBlob.DownloadText(Encoding.UTF8, null, blobRequestOptions, null);
deployParameters.DiagnosticsAddExtensionParameter.PublicConfiguration = diagnosticsPublicConfigContent.TrimStart('\uFEFF');

// Private Config を生成 ストレージ接続文字列しかない設定ファイル 大した設定値はないのでプログラムで作ってしまう
var xmlns = XNamespace.Get("http://schemas.microsoft.com/ServiceHosting/2010/10/DiagnosticsConfiguration");
var storageAccountElement = new XElement(xmlns + "StorageAccount");
storageAccountElement.SetAttributeValue("name", deployParameters.StorageConnectionString.Split(';')[1].Split('=')[1]);
storageAccountElement.SetAttributeValue("key", deployParameters.StorageConnectionString.Split(';')[2].Split('=')[1]);
var privateConfigElement = new XElement(xmlns + "PrivateConfig", storageAccountElement);
var privateConfig = new XDocument(privateConfigElement);
deployParameters.DiagnosticsAddExtensionParameter.PrivateConfiguration = privateConfig.ToString();

// Diagnostics Extension の設定 ここでインストール
managementClient.HostedServices.AddExtension(CloudServiceName, deployParameters.DiagnosticsAddExtensionParameter);
```

でも、やっぱりログが出ない。

ポータル上で確認するとこんな感じ

![拡張機能インストールおかしい状態.PNG](https://qiita-image-store.s3.amazonaws.com/0/143870/04868010-80b7-30f7-0ea4-d18830465113.png)

なんだよ…「無効」って…

もうだめだぁ…おしまいだぁ…。

# そういえばVisual Studioはどうやってデプロイしてんだ？
そういえば、Visual StudioはどんなRESTAPIをどんな順番呼び出しデプロイしているんだろうとふと思い、

Fiddlerでキャプチャしてみることに。すると…

1.登録済みのサービス証明書を取得もしくは新規登録

ふんふん、ですよね。

2.拡張診断機能のインストール

ふんふん、ですよね。

3.cspkgのデプロイ

ふんふん、ですy…なにこのExtensionConfigurationとかいうパラメーター

```
<CreateDeployment 
    xmlns="http://schemas.microsoft.com/windowsazure" 
    xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <Name>{適当な名前}</Name>
    <PackageUrl>{cspkgのありか}</PackageUrl>
    <Label>UGFja2FnaW5nU2VydmljZS5EZXYgLSAyMDE2LzEyLzA1IDIzOjQzOjE2</Label>
    <Configuration>{省略}</Configuration>
    <TreatWarningsAsError>false</TreatWarningsAsError>
    <ExtensionConfiguration>
        <AllRoles i:nil="true"/>
        <NamedRoles>
            <Role>
                <RoleName>PackagingWorker</RoleName>
                <Extensions>
                    <Extension>
                        <Id>MSVSAZ-PackagingWorker-WAD11-N</Id>
                    </Extension>
                </Extensions>
            </Role>
        </NamedRoles>
    </ExtensionConfiguration>
</CreateDeployment>
```

どうやらデプロイ時に拡張機能とRoleの紐づけを行っている模様

いや、確かに前に[リファレンス](https://msdn.microsoft.com/en-us/library/ee460813.aspx#ExtensionConfiguration)読んだけどこれじゃ分かんねぇって…。

まぁひとまずこうなるようにコード書けばいけるんじゃね？

# 任務…完了…
デプロイ時のコードを以下のような感じにしてみることに。

ExtensionConfigurationで拡張機能とRoleの名前を明示的に紐づけます。

```C#
// デプロイパラメータの生成
// 拡張機能のIDを指定
var extentions = new List<ExtensionConfiguration.Extension>() { new ExtensionConfiguration.Extension() { Id = deployParameters.DiagnosticsAddExtensionParameter.Id } };
var parameters = new DeploymentCreateParameters
{
    Label = $"{CloudServiceName} - {DateTime.UtcNow:yyyy/MM/dd hh:mm:ss}",
    Name = Guid.NewGuid().ToString("N"),
    PackageUri = new Uri(packageUri),
    Configuration = configContent.TrimStart('\uFEFF'),  // BOM付きの場合は削除
    StartDeployment = true, // 上までは前と同じ
    ExtensionConfiguration = new ExtensionConfiguration() // Diagnosticsの拡張機能とロールを紐づける
    {
        NamedRoles = new List<ExtensionConfiguration.NamedRole>()
        {
            new ExtensionConfiguration.NamedRole()
            {
                RoleName = deployParameters.RoleName,
                Extensions = extentions
            }
        }
    }
};
// デプロイ実行 ComputeManagementClientを以下の様に利用する
var response = managementClient.Deployments.Create(CloudServiceName, deploymentSlot, parameters);
```

ちゃんと拡張診断機能がインストールされてログも出るようになりました。

![拡張機能インストール正しい状態.PNG](https://qiita-image-store.s3.amazonaws.com/0/143870/51c0c5fd-0f1d-3604-c0e2-a0f0d24595c7.png)


# 最後に
必要な時だけデプロイすることで、コンピューティングコストを下げられます。

特に大きなサイズのVMを利用している場合は、未使用時間帯はデプロイを削除することでかなりコストを下げられます。

ぜひお試しあれ。