normalian blog

Let's talk about Microsoft Azure, ASP.NET and Java!

Eclipse で Maven Project を使ってる際にはまるかもしれない TIPS

EclipseMaven Project を使っている場合、Eclipse の設定でしばしばはまるので備忘録として書いておく。

Unbound Classpath Variable: 'M2_REPO/... が発生する

Eclipse のクラスパス変数に対して M2_REPO が設定できていないケース。以下のコマンドで対応できる。

>mvn -Declipse.workspace=c:\opt\workspace eclipse:configure-workspace

調べ方によっては eclipse:add-maven-repo を実行する方法が出てくるが、Maven 2.1 辺りからコマンドが変わったそうな。

jsp が javax.servlet.HttpServlet が見つからないと怒る

以下のサイトを参考にビルドパスに Web App Libraries を追加する。
http://stackoverflow.com/questions/4440479/how-can-i-add-jar-files-to-the-web-inf-lib-folder-in-eclipse

WEB-INF/lib に追加したはずの Maven Dependency ライブラリが追加されず、Tomcat 起動時にエラーになる

Eclipse に依存 jar が追加されていない。Eclipse のパッケージエクスプローラ等から [Maven - Update Project...] を実行する。

AppInsights と Power BI を使って Java Web アプリの例外発生をリアルタイム監視する

前回の記事である Application Insights と Power BI を使って Java の Web アプリの PageView を監視する にて、Application Insights の連続エクスポート/Stream Analytics の Power BI 出力を活用して PageView のログが簡単に可視化できることが分かった。
今回は Application Insights に例外ログを出力して Power BI に可視化するまでの手順を紹介する。なお、Application Insights の連続エクスポート、Stream Analytics の設定、Power BI 設定の細かな手順については前回の記事を参照してほしい。

Java アプリケーションの記載

以下の様に入力文字列が空の場合に IllegalArgumentException を出力するコードを作成する。

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<script type="text/javascript">
        var appInsights=window.appInsights||function(config){
            function r(config){t[config]=function(){var i=arguments;t.queue.push(function(){t[config].apply(t,i)})}}var t={config:config},u=document,e=window,o="script",s=u.createElement(o),i,f;for(s.src=config.url||"//az416426.vo.msecnd.net/scripts/a/ai.0.js",u.getElementsByTagName(o)[0].parentNode.appendChild(s),t.cookie=u.cookie,t.queue=[],i=["Event","Exception","Metric","PageView","Trace"];i.length;)r("track"+i.pop());return r("setAuthenticatedUserContext"),r("clearAuthenticatedUserContext"),config.disableExceptionTracking||(i="onerror",r("_"+i),f=e[i],e[i]=function(config,r,u,e,o){var s=f&&f(config,r,u,e,o);return s!==!0&&t["_"+i](config,r,u,e,o),s}),t
        }({
            instrumentationKey:"<my instrumentation key>"
        });
        
        window.appInsights=appInsights;
        appInsights.trackPageView();
    </script>
<title>AppInsights-site index page</title>
</head>
<body>
	<h2>ポストしてみる</h2>
	<form method="post" action="<%=request.getContextPath()%>/home">
		文字列: <input name="message" type="text" placeholder="文字列を入力してください" />
		<br /> <input type="submit" value="送信 " />
	</form>
</body>
</html>
package com.mydomain;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.microsoft.applicationinsights.TelemetryClient;

@WebServlet("/home")
public class HomeServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	private static final Logger logger = LogManager.getLogger(HomeServlet.class);

	/**
	 * @see HttpServlet#HttpServlet()
	 */
	public HomeServlet() {
		super();
		// TODO Auto-generated constructor stub
	}

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		TelemetryClient telemetryClient = new TelemetryClient();

		String message = request.getParameter("message");
		if (message == null || message.length() == 0) {
			IllegalArgumentException ex = new IllegalArgumentException("please input message");
			telemetryClient.trackException(ex);
			logger.warn("input warning");
		}
		RequestDispatcher dispatch = request.getRequestDispatcher("/second.jsp");
		dispatch.forward(request, response);
	}
}

非常にシンプルなコードなので、特に解説は不要だと思う。

Stream Analytics を設定する

前回の記事を参考にインプットに BLOB ストレージ、アウトプットに Power BI を指定する。特に Stream Analytics のインプットのパターンは以下となる。

  • プレフィックスのパターン: forpowerbiappinsights_/Exceptions/{date}/{time}
  • 日付形式: YYYY-MM-DD
  • 時刻形式: HH

そんな BLOB の格納される Exception のログは以下になる。

{  
   "basicException":[  
      {  
         "assembly":"Unknown",
         "exceptionType":"java.lang.IllegalArgumentException",
         "outerExceptionType":"java.lang.IllegalArgumentException",
         "outerExceptionThrownAtMethod":"com.mydomain.HomeServlet.doPost",
         "outerExceptionThrownAtAssembly":"",
         "failedUserCodeMethod":"com.mydomain.HomeServlet.doPost",
         "failedUserCodeAssembly":"",
         "exceptionGroup":"java.lang.IllegalArgumentException at com.mydomain.HomeServlet.doPost",
         "id":"Representative",
         "typeName":"java.lang.IllegalArgumentException",
         "handledAt":"UserCode",
         "count":1,
         "method":"com.mydomain.HomeServlet.doPost",
         "problemId":"",
         "outerExceptionMessage":"please input message"
      },
      {  
         "parsedStack":[  
            {  
               "method":"com.mydomain.HomeServlet.doPost",
               "fileName":"HomeServlet.java",
               "level":0,
               "line":78
            },
            {  
               "method":"javax.servlet.http.HttpServlet.service",
               "fileName":"HttpServlet.java",
               "level":1,
               "line":648
            },
            <中略>

上記の "basicException" 内にある "exceptionType" を取得するクエリは以下となる。"parsedStack" 部分をスキップしないと空白の count(*) が出力されるため、HAVING 句で "basicException" の無い項目はスキップしている。

    SELECT
      flat.ArrayValue.exceptionType,
      count(*)
    INTO
      [pbi-exception-output]
    FROM
      [export-input] A
    OUTER APPLY GetElements(A.[basicException]) as flat
    GROUP BY TumblingWindow(minute, 1), flat.ArrayValue.exceptionType
    HAVING LEN(flat.ArrayValue.exceptionType) > 0

Power BI でのリアルタイム表示

Power BI にログインし、Stream Analytics のアウトプットで指定したデータセットが表示されていることを確認する。ポータルから表示方法を設定し、"以下の様に ダッシュボード" にレポートを作成する*1
f:id:waritohutsu:20151012215131p:plain

体感値として、1分弱の時間で Application Insights の連続エクスポートが永続ログを吐き出し、2分~3分で Power BI 側に情報が反映される様だ。

*1:ダッシュボードに保存しないと、リアルタイムでの更新が確認できない

Application Insights と PowerBI を使って Java の Web アプリの PageView を監視する

半年ほど前に 割と普通なブログ > Application Insight を Java アプリケーションから利用する で紹介させて頂いた Java の Application Insights の利用方法だが、以下のように BLOB ストレージに永続ログとしてデータを出力できることをご存じだろうか。

連続エクスポートは Application Insights の標準以上の有料プランで利用する必要があるが、以下の様に "30日間のプレミアム試用版"という期間限定でお試しで無料で利用できるプランがある。
f:id:waritohutsu:20150930012936p:plainf:id:waritohutsu:20150930012946p:plain

今回はこちらを利用して Application Insights のログを Power BI の可視化までを無料で行ってみる。

連続エクスポートの設定

管理ポータルから Application Insights の設定タブより、宛先のストレージサービスを選択してログの出力先を指定する。
f:id:waritohutsu:20150930012954p:plain

この際、以下の様にエクスポートするデータの種類を選択できる。
f:id:waritohutsu:20150930013007p:plain

Java アプリケーションの作成

JSP ファイルが 3 個、Servlet が一つのシンプルな構成とする。特に解説はしないが、画面とサーブレットの関係が inde.jsp <- HomeServlet.jsp -> second.jsp かつ、third.jsp が独立している構成なことが理解いただけると思う。また、Application Insights へログ出力が必要な instrumentationKey 値は管理ポータルから取得すること。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.mydomain</groupId>
	<artifactId>AppInsightWebApp</artifactId>
	<packaging>war</packaging>
	<version>0.0.1-SNAPSHOT</version>
	<name>AppInsightWebApp Maven Webapp</name>
	<url>http://maven.apache.org</url>
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>3.8.1</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.microsoft.azure</groupId>
			<artifactId>applicationinsights-web</artifactId>
			<version>1.0.1</version>
		</dependency>
		<dependency>
			<groupId>com.microsoft.azure</groupId>
			<artifactId>applicationinsights-core</artifactId>
			<version>1.0.1</version>
		</dependency>
		<dependency>
			<groupId>com.microsoft.azure</groupId>
			<artifactId>applicationinsights-logging-log4j2</artifactId>
			<version>1.0.1</version>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-api</artifactId>
			<version>2.3</version>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-core</artifactId>
			<version>2.3</version>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.0.1</version>
			<scope>provided</scope>
		</dependency>
	</dependencies>
	<build>
		<finalName>AppInsightWebApp</finalName>
	</build>
</project>
package com.mydomain;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/home")
public class HomeServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {

		String nextPage = request.getParameter("next");
		if (nextPage == null) {
			nextPage = "index.jsp";
		}

		RequestDispatcher dispatch = request.getRequestDispatcher("/"
				+ nextPage);
		dispatch.forward(request, response);
	}

	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
	}
}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<script type="text/javascript">
        var appInsights=window.appInsights||function(config){
            function r(config){t[config]=function(){var i=arguments;t.queue.push(function(){t[config].apply(t,i)})}}var t={config:config},u=document,e=window,o="script",s=u.createElement(o),i,f;for(s.src=config.url||"//az416426.vo.msecnd.net/scripts/a/ai.0.js",u.getElementsByTagName(o)[0].parentNode.appendChild(s),t.cookie=u.cookie,t.queue=[],i=["Event","Exception","Metric","PageView","Trace"];i.length;)r("track"+i.pop());return r("setAuthenticatedUserContext"),r("clearAuthenticatedUserContext"),config.disableExceptionTracking||(i="onerror",r("_"+i),f=e[i],e[i]=function(config,r,u,e,o){var s=f&&f(config,r,u,e,o);return s!==!0&&t["_"+i](config,r,u,e,o),s}),t
        }({
            instrumentationKey:"<my instrumentationKey>"
        });
        
        window.appInsights=appInsights;
        appInsights.trackPageView();
    </script>
<title>AppInsights-site index</title>
</head>
<body>
	<h2>Hello World! - index page</h2>
	<a href="<%=request.getContextPath()%>/home?next=second.jsp">next
		page</a>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<script type="text/javascript">
        var appInsights=window.appInsights||function(config){
            function r(config){t[config]=function(){var i=arguments;t.queue.push(function(){t[config].apply(t,i)})}}var t={config:config},u=document,e=window,o="script",s=u.createElement(o),i,f;for(s.src=config.url||"//az416426.vo.msecnd.net/scripts/a/ai.0.js",u.getElementsByTagName(o)[0].parentNode.appendChild(s),t.cookie=u.cookie,t.queue=[],i=["Event","Exception","Metric","PageView","Trace"];i.length;)r("track"+i.pop());return r("setAuthenticatedUserContext"),r("clearAuthenticatedUserContext"),config.disableExceptionTracking||(i="onerror",r("_"+i),f=e[i],e[i]=function(config,r,u,e,o){var s=f&&f(config,r,u,e,o);return s!==!0&&t["_"+i](config,r,u,e,o),s}),t
        }({
            instrumentationKey:"<my instrumentationKey>"
        });
        
        window.appInsights=appInsights;
        appInsights.trackPageView();
    </script>
<title>AppInsights-site second</title>
</head>
<body>
	<h2>Hello World!- second page</h2>
	<a href="<%=request.getContextPath()%>/home?next=index.jsp">next
		page</a>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<script type="text/javascript">
        var appInsights=window.appInsights||function(config){
            function r(config){t[config]=function(){var i=arguments;t.queue.push(function(){t[config].apply(t,i)})}}var t={config:config},u=document,e=window,o="script",s=u.createElement(o),i,f;for(s.src=config.url||"//az416426.vo.msecnd.net/scripts/a/ai.0.js",u.getElementsByTagName(o)[0].parentNode.appendChild(s),t.cookie=u.cookie,t.queue=[],i=["Event","Exception","Metric","PageView","Trace"];i.length;)r("track"+i.pop());return r("setAuthenticatedUserContext"),r("clearAuthenticatedUserContext"),config.disableExceptionTracking||(i="onerror",r("_"+i),f=e[i],e[i]=function(config,r,u,e,o){var s=f&&f(config,r,u,e,o);return s!==!0&&t["_"+i](config,r,u,e,o),s}),t
        }({
            instrumentationKey:"<my instrumentationKey>"
        });
        
        window.appInsights=appInsights;
        appInsights.trackPageView();
    </script>
<title>AppInsights-site third@@@@@@</title>
</head>
<body>
	<h2>Hello World!- third page</h2>
</body>
</html>

次に、上記のアプリケーションを作成後、Tomcat 等で Web アプリケーションを起動する。

ログ出力の確認

起動した Web アプリケーションにブラウザ経由でアクセスすると、Application Insights へのログ出力が成功していれば以下のように管理ポータルからログが閲覧可能だ。
f:id:waritohutsu:20150930013020p:plain

更に、連続エクスポートが正常に行われている場合、blob container に以下の様にファイルが出力されているはずだ。今回は このうちの PageViews をターゲットとする。
f:id:waritohutsu:20150930013030p:plain

Stream Analytics でログを整形し、Power BI に出力する

Application Insights データの Power BI ビュー を参照いただければ問題ないと思うが、Stream Analytics の新規インスタンスを作成して以下の設定を行う。

  • 入力として BLOB ストレージを選択
  • 出力先として Power BI を選択
  • Stream Analytics のクエリを作成

上記で Stream Analytics の入出力設定は特に問題ないと思うが、Stream Analytics のクエリがドキュメントではうまく動かなかった( A.[view] 部分が間違っていた程度だが )ので以下を参考にしてほしい。

    SELECT
      flat.ArrayValue.name,
      count(*)
    INTO
      [pbi-output]
    FROM
      [export-input] A
    OUTER APPLY GetElements(A.[view]) as flat
    GROUP BY TumblingWindow(minute, 1), flat.ArrayValue.name

上記のクエリは以下の様な形式で BLOB に格納されているデータは以下のような URL でファイルを作成されログ出力される。

  • https://<アカウント名>.blob.core.windows.net/appinsightslog/forpowerbiappinsights_581bb4f68f56476c9c3ce13eca62c0f4/PageViews/2015-09-29/10/c565d13c-b5f9-42f4-b9eb-a82e05fa6695_20150929_100813.blob

ファイルの中身は以下となるが、上記のクエリは「"name": "AppInsights-site index"」における値を抜き出すクエリになっている。詳細は参考のリンク先における Stream Analytics のクエリを参考にしてほしい。

{
  "view": [
    {
      "urlData": {
        "port": 8080,
        "host": "localhost",
        "protocol": "http",
        "base": "/AppInsightWebApp/",
        "queryParameters": [ ],
        "hashTag": ""
      },
      "name": "AppInsights-site index",
      "count": 1,
      "durationMetric": {
        "value": 5980000.0,
        "count": 1.0,
        "sampledValue": 5980000.0
      },
      "url": "http://localhost:8080/AppInsightWebApp/"
    }
  ],
  "internal": {
    "data": {
      "id": "<my id>",
      "documentVersion": "1.6"
    }
  },
  "context": {
    "device": {
  <中略>

Power BI で閲覧

次に powerbi.microsoft.com へアクセスし、Stream Analytics の出力で設定した宛先を確認する。正常に Stream Analytics が動作していれば、以下の画面の様に「Stream Analytics の出力で設定した宛先」の箇所が追加されているので、こちらを操作して任意のグラフを作成できる。
f:id:waritohutsu:20150930015927p:plain

上記のように、Application Insights と Power BI を利用することで、容易に画面毎のアクセス数が確認できるビューを作成できることが分かった。もちろん Stream Analytics のクエリを変更することで任意の情報を Power BI 側に表示することが可能になる。

Microsoft Azure の ARM 版の仮想マシンで NIC を差し替えてみる

最近は ARM(Azure Resource Manager) 版の Azure Java SDK のお話をしてきたので、当ブログをを購読していただいている各位には特に説明不要だと思うが、現在の Microsoft Azure は Azure Service Management(旧版)と Azure Resource Manager(新版)が混在している。
Azure Resource Manager ではネットワークインタフェースを仮想マシンとは個別に定義することが可能になっており、ドキュメント VIRTUAL NETWORK ネットワーク リソース プロバイダー で記載されている「ネットワーク リソース モデルとその関係の概要」の図を確認してもらえれば理解いただけると思うが、NIC仮想マシン/仮想ネットワーク(のサブネット)/Network Security Group と関連付けられていることがわかる。
こちらを利用して、今回は既存の仮想マシンから NIC を抜き差しを試してみた。

NIC を付け替えるコード例

以下の PowerShell スクリプトは新規に NIC を作成し、ARM 版の仮想マシンに付随する既存 NIC と入れ替えるスクリプトだ。

# パラメータ
$vnetName = "<vnet name>"
$resourceGroupName = "<resource group name>"
$subnetName = "<subnet name>"
$networkInterfaceName = "<new nic name>"
$networkInterfaceLocation = "japaneast"
$networkInterfacePrivateIP = "<private ip>"
$vmName = "<vm name>"

# Azure Resource Manager に動作モードを変更
Switch-AzureMode AzureResourceManager

# 新しい NIC の作成
$vnet = Get-AzureVirtualNetwork -Name $vnetName -ResourceGroupName $resourceGroupName
$defaultSubnet = Get-AzureVirtualNetworkSubnetConfig -Name $subnetName -VirtualNetwork $vnet
$newNIC = New-AzureNetworkInterface -ResourceGroupName $resourceGroupName -Name $networkInterfaceName -Location $networkInterfaceLocation -PrivateIpAddress $networkInterfacePrivateIP -Subnet $defaultSubnet

# 仮想マシンに割り当て済みの NIC を新規 NIC と付け替え
$vm = Get-AzureVM -Name $vmName -ResourceGroupName $resourceGroupName
$vm = Remove-AzureVMNetworkInterface -VM $vm -NetworkInterfaceIDs $vm.NetworkInterfaceIDs[0]
$vm = Add-AzureVMNetworkInterface -VM $vm -NetworkInterface $newNIC
Update-AzureVM -VM $vm -ResourceGroupName $resourceGroupName

# リソースグループに紐づけられた NIC の一覧を取得し、名前を表示
Get-AzureNetworkInterface -ResourceGroupName $resourceGroupName | Select-Object -Property Name

# 不要な NIC が存在する場合、以下のコマンドで削除
$unnecessaryNICName = "<unnecessaryNICName>"
Remove-AzureNetworkInterface -ResourceGroupName $resourceGroupName -Name $unnecessaryNICName

New-AzureNetworkInterface で新規に NIC を作成し、Remove-AzureVMNetworkInterface で仮想マシンから既存 NIC を削除し、Add-AzureVMNetworkInterface で仮想マシンに新たな NIC を関連づけている。

どの程度の範囲で NIC の抜き差しができるのか?

更に、せっかくなので色々と試してみたが別の仮想ネットワークには移動できない様だ。

  • 別リージョンの仮想ネットワークに NIC を移動させようとした場合、以下のエラーが発生する。
Update-AzureVM : InvalidResourceReference: Resource /subscriptions/<subscription id>/resourceGroups/<resource group name>/providers/Microsoft.Network/networkInterfaces/<new nic name> referenced by /subscriptions/<subscription id>/resourceGroups/arm-study-group/providers/Microsoft.Compute/virtualMachines/<仮想マシン名> is not found. Make sure both resources are created in the same region / location.
OperationID : '29f106f8-ce4c-4329-be4a-67c9bd23038d'
発生場所 行:5 文字:1
+ Update-AzureVM -VM $vm -ResourceGroupName $resourceGroupName
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : CloseError: (:) [Update-AzureVM]、ComputeCloudException
    + FullyQualifiedErrorId : Microsoft.Azure.Commands.Compute.UpdateAzureVMCommand
  • 同一リージョンの別仮想ネットワークに NIC を移動させようとした場合、以下のエラーが発生する。
Update-AzureVM -VM $vm -ResourceGroupName $resourceGroupName
Update-AzureVM : SubnetsNotInSameVnet: Subnet <subnet name> referenced by resource /subscriptions/<subscription id>/resourceGroups/<resource group name>/providers/Microsoft.Network/networkInterfaces/<new nic name>/ipConfigurations/ipconfig1 is not in the same Virtual Network as the subnets of other VMs in the availability set.
OperationID : '46711037-dc43-4f3c-826e-3f5618afb3f8'
発生場所 行:10 文字:1
+ Update-AzureVM -VM $vm -ResourceGroupName $resourceGroupName
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : CloseError: (:) [Update-AzureVM]、ComputeCloudException
    + FullyQualifiedErrorId : Microsoft.Azure.Commands.Compute.UpdateAzureVMCommand

Microsoft Azure の ARM 版 Java SDK 2回目 - リソースの存在確認をする

先日利用した ARM 版の Azure Java SDK を利用して、リソースの存在確認を行う方法を紹介する。だらだら説明する前にまずはサンプルコードを以下に記載する。

public class CheckResourceExist {
	public static void main(String[] args) throws Exception {
		Configuration config = ConfigFactory.createConfiguration();
		ResourceManagementClient resourceManagementClient = ResourceManagementService
				.create(config);
		ResourceOperations resourceOperations = resourceManagementClient
				.getResourcesOperations();

		// クラウドサービスの生存確認
		dispResourceExist(resourceOperations, "winapserver",
				"Microsoft.ClassicCompute", "domainNames", "winapserver");

		// WebApps の生存確認
		dispResourceExist(resourceOperations, "normalianwebsite",
				"Microsoft.Web", "sites", "WebSiteDeployDevelopment");
	}

	public static void dispResourceExist(ResourceOperations resourceOperations,
			String resourceName, String providerNamespace, String resourceType,
			String resourceGroupName) throws IOException, ServiceException {
		ResourceIdentity resourceIdentity = new ResourceIdentity();
		resourceIdentity.setResourceName(resourceName);
		resourceIdentity.setResourceType(resourceType);
		resourceIdentity.setResourceProviderNamespace(providerNamespace);

		// The supported api-versions are '2014-01-01, 2014-06-01, 2015-06-01'
		resourceIdentity.setResourceProviderApiVersion("2015-06-01");

		ResourceExistsResult resourceExistsResult = resourceOperations
				.checkExistence(resourceGroupName, resourceIdentity);
		StringBuilder result = new StringBuilder("@"
				+ resourceIdentity.getResourceName() + " at "
				+ resourceGroupName + "is ");
		if (resourceExistsResult.isExists() == false) {
			result.append("not ");
		}
		result.append("exists");
		System.out.println(result.toString());
	}
}

ソースコードについては GitHub HelloAzureARMSDK/src/main/java/org/mydomain/armsdksample/CheckResourceExist.java を参照頂くとして、リソースの生存確認には以下のクラスが必要になる。

  • ResourceName:コード例では normalianwebsite
  • ResoruceType:コード例では sites
  • ResourceProviderNamespace:コード例では Microsoft.Web
  • ResourceProviderApiVersion:コード例では 2015-06-01

なぜ上記の情報が必要かについては Azure リソース マネージャー REST API リファレンス を見ていただければ詳細が載っているが、PowerShell を Debug 実行しても以下の様に情報が得られる。

PS C:\Windows\System32\WindowsPowerShell\v1.0> Get-AzureWebApp -ResourceGroupName WebSiteDeployDevelopment -Name normalianwebsite -Debug

<中略>
Absolute Uri:
https://management.azure.com/subscriptions/<subscriptionId>/resourceGroups/WebSiteDeployDevelopment/providers/Microsoft.Web/sites/normalianwebsite?api-version=2014-06-01

Headers:

Body:

デバッグ: ============================ HTTP RESPONSE ============================

<中略>
Body:
{
  "id": "/subscriptions/<subscriptionId>/resourceGroups/WebSiteDeployDevelopment/providers/Microsoft.Web/sites/normalianwebsite",
  "name": "normalianwebsite",
  "type": "Microsoft.Web/sites",
  "kind": null,
  "location": "Japan West",
  "tags": {
    "hidden-related:/subscriptions/<subscriptionId>/resourceGroups/WebSiteDeployDevelopment/providers/Microsoft.Web/serverfarms/WebSiteDeployDevelopment": "Resource"
  },

上記の "Absolute Uri" から、REST API が ResourceName, ResoruceType, ResourceProviderNamespace, ResourceProviderApiVersion の情報が必要になることがわかると思う。ResourceOperations#checkExistence() の処理を見ればわかる通り、これらの情報がない場合は NullPointerException が発生する点に注意が必要だ。

また、ResourceProviderApiVersion は '2014-01-01, 2014-06-01, 2015-06-01' し利用できない。誤った API version, Location を指定した場合、以下の ServiceException が発生するので注意が必要だ*1。上記に記載した PowerShell の Debug 実行だとなぜか内部で 2014-06-01 の API Version を呼んでいるが理由は不明。

Exception in thread "main" com.microsoft.windowsazure.exception.ServiceException:{  
   "error":{  
      "code":"NoRegisteredProviderFound",
      "message":"No registered resource provider found for location 'japaneast' and API version '2015-06-15' for type 'domainNames'.
       The supported api-versions are '2014-01-01, 2014-06-01, 2015-06-01'. The supported locations are 'eastasia, southeastasia,
        eastus, eastus2, westus, northcentralus, southcentralus, centralus, northeurope, westeurope, japaneast, japanwest, brazilsouth,
        australiaeast, australiasoutheast, southindia, centralindia, westindia, eastus2stage, northcentralusstage'."
   }
}

まだまだ確認必要な点はあるがいったん終了。

参考

REST API 側では "type": "Microsoft.Web/sites" となっているのに、Azure SDK では ResourceType が sites, ResourceProviderNamespace が Microsoft.Web になっていることもあり、ちょっと混乱してしまった。この辺り、Resource Provider Namespace を一覧化している情報は公にはなっていない様で、情報が拾いきれなかったものの ドキュメント VIRTUAL NETWORK ネットワーク リソース プロバイダー では ネットワーク リソース プロバイダーの値は Microsoft.Network という記載がある。

*1:整形している点に注意

Microsoft Azure の Preview 版 Java ARM SDK で疎通をとってみる

ご存じの方も多いと思うが Microsoft Azure には動作モードに ARM(Azure Resource Manager) と ASM(Azure Service Manager) という二つの動作モードが存在する。昨今、Java 版 ARM SDK の Preview 版が発表されたので利用してみた。

SDKソースコードGitHub にて公開されているので、全体が閲覧可能になっている。こちらのREADMEによれば Authenticating a service principal with Azure Resource Manager を参照してサービス プリンシパルの認証を設定する必要がある。

上記の記事を参考に実行したコマンドは以下になる。Authenticating a service principal with Azure Resource Manager の記事を参照すれば特に問題ないが、New-AzureRoleAssignment コマンド実行時に Role を Owner 等(少なくとも Reader ではダメ)にしないと後で SDK を利用したリソース作成ができない点に注意だ。

# ARM モードにして認証する
Switch-AzureMode AzureResourceManager
Add-AzureAccount

# Azure Active Directory アプリケーションのサービス プリンシパルを作成
$azureAdApplication = New-AzureADApplication -DisplayName "ARM Java SDK" -HomePage "<home page>" -IdentifierUris "<identifieruri>" -Password "<password>"
New-AzureADServicePrincipal -ApplicationId $azureAdApplication.ApplicationId

# ApplicationId か IdentifierUri をユーザ名として利用する
$creds = Get-Credential
Add-AzureAccount -Credential $creds -ServicePrincipal -Tenant $subscription.TenantId

# リソースへのアクセス権を付与する
New-AzureRoleAssignment -RoleDefinitionName Owner -ServicePrincipalName $azureAdApplication.ApplicationId

上記でサービスプリンシパル認証の設定が完了したので、Gradle を設定して依存関係を解決する。念のため省略記法では記載していないが、com.microsoft.azure の group は SDK を稼働させる際に必須になるので注意してほしい。

apply plugin: 'java'
apply plugin: 'eclipse'

sourceCompatibility = 1.8
version = '1.0'
jar {
	manifest {
		attributes 'Implementation-Title': 'Hello Azure ARM SDK Preview',
		'Implementation-Version': version
	}
}

repositories { mavenCentral() }

dependencies {
	compile group: 'com.microsoft.azure', name: 'azure-core', version: '0.8.0'
	compile group: 'com.microsoft.azure', name: 'azure-mgmt-resources', version: '0.8.0'
	compile group: 'com.microsoft.azure', name: 'azure-mgmt-storage', version: '0.8.0'
	compile group: 'com.microsoft.azure', name: 'azure-mgmt-network', version: '0.8.0'
	compile group: 'com.microsoft.azure', name: 'azure-mgmt-compute', version: '0.8.0'
	compile group: 'com.microsoft.azure', name: 'azure-mgmt-utility', version: '0.8.0'
	compile group: 'com.microsoft.azure', name: 'adal4j', version: '1.0.0'
	compile group: 'commons-logging', name: 'commons-logging', version: '1.2'
	compile group: 'org.slf4j', name: 'slf4j-jdk14', version: '1.5.6'
	testCompile group: 'junit', name: 'junit', version: '4.+'
}

test { systemProperties 'property': 'value' }

uploadArchives {
	repositories {
		flatDir { dirs 'repos' }
	}
}

MVN Repository の Home » com.microsoft.azure » azure-management-compute を参照したところ、0.8.0 が最新だったので上記のバージョンを設定しているが、適宜変更すること。

準備が整ったので以下のソースコードを実行するが、プロジェクト全体はGitHub/normalian/HelloAzureARMSDK を参照すること。

	public static void main(String[] args) throws Exception {
		Configuration config = createConfiguration();
		ResourceManagementClient resourceManagementClient = ResourceManagementService
				.create(config);
		StorageManagementClient storageManagementClient = StorageManagementService
				.create(config);
		ComputeManagementClient computeManagementClient = ComputeManagementService
				.create(config);
		NetworkResourceProviderClient networkResourceProviderClient = NetworkResourceProviderService
				.create(config);

		String resourceGroupName = "java-armsdk-group";

		// Get-AzureLocation で確認
		String region = "EastAsia";

		ResourceContext context = new ResourceContext(region,
				resourceGroupName, subscriptionId, false);

		System.out.println("Start create vm...");

		// VM 作成
		try {
			VirtualMachine vm = ComputeHelper.createVM(
					resourceManagementClient, computeManagementClient,
					networkResourceProviderClient, storageManagementClient,
					context, "javaSampleVM", "normalian", "P@ssw0rd2015")
					.getVirtualMachine();

			System.out.println(vm.getName() + " is created");
		} catch (Exception ex) {
			ex.printStackTrace();
		}

		// リソースグループごと仮想マシンを削除、残す場合はコメントアウトする
		resourceManagementClient.getResourceGroupsOperations().beginDeleting(
				context.getResourceGroupName());
		System.out.println("end");
	}

	public static Configuration createConfiguration() throws Exception {
		return ManagementConfiguration.configure(
				null,
				new URI(armUrl),
				subscriptionId,
				AuthHelper.getAccessTokenFromServicePrincipalCredentials(
						managementUri, armAadUrl, armTenant, armClientId,
						armClientkey).getAccessToken());
	}
}

上記を見て分かる通り、ソースコード自体は非常にシンプルだ。次回以降で ARM SDK を利用して細かな制御を確認したいと思う。

Gradle でスクレイピングをしてみる

普段は Maven を使ってしまっているが、結構前から Gradle が流行っていることは Java 界隈の方々には既知の事実であろう。人が作ったスクリプトでのビルドは利用する機会があるが、あんまり自分でスクリプトを弄らなかったのでちょっと息抜きに。

Gradle の入門記事自体は Gradle入門 を見れば大概乗ってるので特に問題ないと思う。ご存じの方も多いと思うが、おおざっぱに言うと「Gradle は Groovy で書かれた設定ファイルで動く」という代物なので、Groovy を学ぶことが重要になる(そういう Groovy もスクリプト言語の皮をかぶった Java だけど)。

スクリプトを動かす

最近、マイクロソフトのイベントであるFEST2015 のセッション情報一覧が表示されたので、こちらから興味深いセッションをスクレイピングするスクリプトを以下に記載した。

  • build.gradle
import org.jsoup.Jsoup;

buildscript {
	repositories { mavenCentral() }
	dependencies { classpath 'org.jsoup:jsoup:1.8.3' }
}

task festSession{
	description ="extract a fest session from html";
	doLast{
		def document = Jsoup.connect("https://www.microsoft.com/ja-jp/events/fest/2015/session.aspx").get()
		def e = document.select("div.Day3x div.boSess").get(0).select("div.sessAbstBox dl").get(0);
		println e.text()
	}
}

buildscript タスクにてスクレイピングに利用するライブラリである jsoup への依存関係を解決している。festSession タスク内では jsoup を利用して興味深いセッションへのスクレイピングを実施した。実行結果は以下になる(見やすさのために改行を入れている)が、実に興味深いセッションなので最前列で見たい。

>gradle festSession
:festSession
12:30 - 13:15[ DEV-301 ] Microsoft Love OSS!! はじめての Java Web アプリケーション開発 on Azure 寺田 佳央
 日本マイクロソフト株式会社 デベロッパーエバンジェリズム統括本部 シニア テクニカル エバンジェリスト Microsoft Azure
 は世界中のリージョンに存在するインスタンスの内、20 %以上が Linux が稼働しています。そして Linux 上で、PHP、Ruby、
 Java などのプログラミング言語で構築された Web アプリケーションを難なく動作させることができます。本セッションでは、
 Microsoft Azure の Linux 環境で、Java の Web アプリケーションを構築するための手順や便利な機能、さらに、Azure 
 で提供するさまざまなサービスを分かりやすくご紹介します。

BUILD SUCCESSFUL

Total time: 4.528 secs