koudenpaのブログ

趣味のブログです。職業柄IT関連の記事が多いと思います。

ARM (Azure Resource Manager) は PowerShell 以外からも使えます

この記事が何かというと、言いたいことはタイトルで終わっています。

ARM Azure Resource Manager は Azure 上のリソースをいい感じに(宣言的にとか)管理するための仕組みで、ググると大体は Azure PowerShell のコマンドレットを使って操作している例が多くヒットします。

しかしながら、それらのコマンドレットの背後では単に REST な API を呼び出しているだけなので、APIを直叩きすれば、他の実行環境でも余裕でプログラマブルなリソース管理を行うことができます。

今日はそのことに気づかされて感心したので、単にそう主張する記事を書いた次第です。

一応、記事の終わりに C# でのサンプルコードを張ってあるので、多少なりとも参考になりましたら幸いです。

DevOps ハッカソン

気づきは半分仕事、半分遊びで参加した DevOps ハッカソンでのことでした。

devopsjp.connpass.com

twitter.com

もともと DevOps 自体というよりは、その要素であるところの CI の Azure での実際を体感したいがための参加でした。

そんなわけで、ひがな一日 Azure の権限周りと格闘していました。 多少なりとも体感できたのでまぁよかったな、と思っています。

また、同日開催されていた Global Azure Boot Camp 2016 in Japan に飛び込みでLT聴講、懇親会参加させてもらいました。

お騒がせしてありがとうございました。

jazug.doorkeeper.jp

Azure 周りのリソースの権限系

この辺りは今三位理解できていないところで、日々試行錯誤しているのですが、 基本的には Azure 周りのリソースの権限は、Azure Active Directory で管理されています。 そのため、ARMに関する認可も ADアプリ の ClientID で取ることができます。

しかしながら、それはあくまで権限を持っているユーザーの認可を得る必要があるようで、 ADアプリ周りの設定では、アプリが単体でARMしまくることはできなさそうでした。

それをするためには、サービスプリンシパル認証という段取りを踏んでやる必要があるようでした。

azure.microsoft.com

これをマルチテナントで任意のユーザーが手軽に使う感じではないので、 痛しかゆしといったところです。

ハッカソンではこの辺を疎通することができなかったのが残念です。 この辺はいい感じに Azure を使っていくためにお勉強を続けていきたいです。

ADアプリで取る認可

Windows Azure Service Management API で良いようでした。

f:id:koudenpa:20160416231855p:plain

クソコード

サンプルとして「これってクソコードだよなぁ」と書いてる途中から思っていたクソコードを恥ずかしげ万歳に晒します。

抜粋です。

普通に Azure 上のリソースの権限を持っているユーザーから認可を得て、

            // ARM認証:
            // https://msdn.microsoft.com/ja-jp/library/azure/dn790557.aspx
            // https://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal/

            // XXX なんとかしてください
            var tenantId = "76e431d9-cdb8-4fc5-hoge";          // XXX
            var clientId = "1eb2855a-c5e3-479a-9016-hoge";

            var authenticationContext = new AuthenticationContext(string.Format("https://login.windows.net/{0}", tenantId));
            var deviceCode = await authenticationContext.AcquireDeviceCodeAsync("https://management.core.windows.net/", clientId);

            // XXX ここでブレークしてメッセージを見て認証してね → メールなりチャットなり送りたいね
            Console.WriteLine(deviceCode.Message);

            var result = await authenticationContext.AcquireTokenByDeviceCodeAsync(deviceCode);

            if (result == null)
            {
                throw new InvalidOperationException("Failed to obtain the JWT token");
            }

            string token = result.AccessToken;

得た認可(アクセストークン)でAPIをコールしてやればよいです。

            // リソース グループの削除:
            // https://msdn.microsoft.com/ja-jp/library/azure/dn790539.aspx
            // DELETE https://management.azure.com/subscriptions/{subscription-id}/resourcegroups/{resource-group-name}?api-version={api-version}
            // 202 が来る限りコールし続け、200が返ってきたら削除完了
            var deleteUri = string.Format("https://management.azure.com/subscriptions/{0}/resourcegroups/{1}?api-version={2}",
                subscriptionId, resourceGroupName, "2015-01-01");
            HttpStatusCode deleteResultCode;

            Console.WriteLine(string.Format("deleteUri: {0}", deleteUri)); // XXX

            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                do
                {
                    var response = client.DeleteAsync(deleteUri).Result;
                    deleteResultCode = response.StatusCode;
                    Console.WriteLine(string.Format("DeleteAsync: {0}", response)); // XXX
                } while (deleteResultCode == HttpStatusCode.Accepted);
            }

API上は消し終わったら200が返ってくるよ? 的に書いてあるように見えたので、200が返ってくるまでSPAMし続けたのですが、 実際に消えたら404が返ってきました。

タイミングの問題なのか? そもそも、僕のメリケッシュ力の問題なのか?

追記: これを見ないといけないようでした。 DELETE しつづけるとか、ほんとに完全にSPAMじゃん。

The Location response header contains the URI that is used to obtain the status of the process.

以下、全文です。 もうお嫁にいけない。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Net.Http;
using System.Net;
using System.Net.Http.Headers;

namespace DeleteResourceGroupJob
{
    public class Functions
    {
        // This function will get triggered/executed when a new message is written 
        // on an Azure Queue called queue.
        public static void ProcessRemoveResourceQueueMessage([QueueTrigger("removeresourcegroup")] string message, TextWriter log)
        {
            log.WriteLine(message);

            // ARM:
            // https://msdn.microsoft.com/ja-jp/library/azure/dn790568.aspx

            var acceccToken = GetAccessToken().Result;

            // XXX なにもいうまい
            var subscriptionId = "c6d7e47c-bc01-4307-8dab-hoge";    // XXX
            var resourceGroupName = message;
            DeleteResourceGroup(subscriptionId, resourceGroupName, acceccToken);
        }

        private static async Task<string> GetAccessToken()
        {
            // ARM認証:
            // https://msdn.microsoft.com/ja-jp/library/azure/dn790557.aspx
            // https://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal/

            // XXX なんとかしてください
            // ARMにガチ権限があるアプリケーションを作らないといけないようなら色々破たんしている
            var tenantId = "76e431d9-cdb8-4fc5-hoge";          // XXX
            //var clientId = "fba8fe66-c857-4e10-b496-hoge";          // XXX
            //var clientSecret = "hoge=";      // XXX

            var clientId = "1eb2855a-c5e3-479a-9016-hoge";

            var authenticationContext = new AuthenticationContext(string.Format("https://login.windows.net/{0}", tenantId));
            //var credential = new ClientCredential(clientId, clientSecret);
            //var result = authenticationContext.AcquireToken("https://management.core.windows.net/", credential);

            var deviceCode = await authenticationContext.AcquireDeviceCodeAsync("https://management.core.windows.net/", clientId);

            // XXX ここでブレークしてメッセージを見て認証してね → メールなりチャットなり送りたいね
            Console.WriteLine(deviceCode.Message);

            var result = await authenticationContext.AcquireTokenByDeviceCodeAsync(deviceCode);

            if (result == null)
            {
                throw new InvalidOperationException("Failed to obtain the JWT token");
            }

            string token = result.AccessToken;

            Console.WriteLine(string.Format("token: {0}", token)); // XXX

            return token;
        }

        private static void DeleteResourceGroup(string subscriptionId, string resourceGroupName, string accessToken)
        {
            // リソース グループの削除:
            // https://msdn.microsoft.com/ja-jp/library/azure/dn790539.aspx
            // DELETE https://management.azure.com/subscriptions/{subscription-id}/resourcegroups/{resource-group-name}?api-version={api-version}
            // 202 が来る限りコールし続け、200が返ってきたら削除完了
            var deleteUri = string.Format("https://management.azure.com/subscriptions/{0}/resourcegroups/{1}?api-version={2}",
                subscriptionId, resourceGroupName, "2015-01-01");
            HttpStatusCode deleteResultCode;

            Console.WriteLine(string.Format("deleteUri: {0}", deleteUri)); // XXX

            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                do
                {
                    var response = client.DeleteAsync(deleteUri).Result;
                    deleteResultCode = response.StatusCode;
                    Console.WriteLine(string.Format("DeleteAsync: {0}", response)); // XXX
                } while (deleteResultCode == HttpStatusCode.Accepted);
            }
        }
    }
}