normalian blog

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

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

WebApps の Web ジョブで Java アプリを稼働させる

WebApps には簡易な定期処理を実行することができる Web ジョブと呼ばれる機能が提供されており、Web ジョブでバックグラウンド タスクを実行するバッチ処理の実行方法が記載されているが、今回は Java アプリケーションを実行する方法を紹介する。

前提知識として、Web ジョブ自体は WebApps のインスタンス上で動作するため、WebApps にバンドルされている実行ファイル( 今回紹介する Java とか )は利用可能だ。2015年8月現在では WebApps には以下の JDK がインストールされている。
f:id:waritohutsu:20150818165625p:plain

Web ジョブ デプロイ用の zip ファイルの作成

上記のうち、こちらに対し、以下のディレクトリ構造を持つ zip ファイルを作成する。

run.zip
└─bin
   │  run.bat
   │
   └─com
       └─mydoman
               Main.class

こちらに格納した run.bat, Main.class の元となる Main.java はそれぞれ以下になる。

  • run.bat
set JAVA_HOME=D:\Program Files\Java\jdk1.8.0_25
"%JAVA_HOME%"\bin\java com.mydoman.Main
package com.mydoman;

import java.io.File;

public class Main {
	public static void main(String[] args) {
		String path = new File(".").getAbsoluteFile().getParent();
		System.out.println("current folder is " + path);
	}
}

管理ポータルでのデプロイ

管理ポータルから作成済みの WebApps を選択し、設定から Web ジョブを選んで新規に作成する。この際に上記の zip ファイルを Web ジョブに格納し、オンデマンドで実行する。
実行後は管理ポータルでログを確認できるが、以下のような実行結果になっているはずだ。

[08/18/2015 06:00:31 > 5b618a: SYS INFO] Status changed to Initializing
[08/18/2015 06:00:31 > 5b618a: SYS INFO] Run script 'run.bat' with script host - 'WindowsScriptHost'
[08/18/2015 06:00:31 > 5b618a: SYS INFO] Status changed to Running
[08/18/2015 06:00:31 > 5b618a: INFO] 
[08/18/2015 06:00:31 > 5b618a: INFO] D:\local\Temp\jobs\triggered\HelloJavaJob\wq4oddg2.nbr>set JAVA_HOME=D:\Program Files\Java\jdk1.8.0_25 
[08/18/2015 06:00:31 > 5b618a: INFO] 
[08/18/2015 06:00:31 > 5b618a: INFO] D:\local\Temp\jobs\triggered\HelloJavaJob\wq4oddg2.nbr>"D:\Program Files\Java\jdk1.8.0_25"\bin\java com.mydoman.Main 
[08/18/2015 06:00:31 > 5b618a: INFO] current folder is D:\local\Temp\jobs\triggered\HelloJavaJob\wq4oddg2.nbr
[08/18/2015 06:00:31 > 5b618a: SYS INFO] Status changed to Success

上記を確認すればわかるが、D ドライブの Web ジョブ専用の領域で実行されていることがわかる。

まとめ

  • Web ジョブで Java は利用できるが、jar を自分でまとめて放り込む必要あり
  • .NET 違って Web ジョブの SDK はない

Micrsoft Azure の Web Apps で WildFly を稼働させる

随分と長い間 JDK 7 縛りにあって半泣きになっていた Web Apps だが、ARM から設定すると Azure Web Apps で JDK 8 と Tomcat 8 が使えるようになっていた で記載されている通り、Web Apps で JDK 8 と Tomcat 8 が使えるようになった。更に、直近では以下のように管理ポータルからも JDK 8 と Tomcat 8 が利用可能となっている。
f:id:waritohutsu:20150809111510p:plain

以前に Web Apps で JDK8 の利用を試みた際は、Tomcat 起動時に JDK8 がネットワークインターフェースの一覧を取得する Win32 API をたたいておりエラーが発生して利用できなかった。Tomcat8/JDK8 が利用できるため、どうやら JDK 8 向けの対応が完了したらしい*1
ここで気になったのが 2015年8月時点での Web Apps では WildFly が利用できるのではないかという点だ。なぜなら、JDK7&WildFly on Web Apps の利用時もネットワークインターフェース一覧を取得する Win32 API を叩く際のエラーが発生していたためだ。
確認した結果、以下の様に WildFly on Web Apps の稼働が確認でき、Web Apps 上で JavaEE7 を利用した Java アプリを存分に稼働できることが分かった。今回はこちらの設定方法を紹介する。
f:id:waritohutsu:20150809111525p:plain

設定手順

Web Apps には WildFly のパッケージは同梱されていないので、WildFly のダウンロードサイトから取得する必要がある。Web Apps では curl コマンドが仕込まれているので、Kudu のコンソールにて以下を実行して WildFly のパッケージを取得し、パッケージの中身を展開する。ファイル名のバージョンは適宜変更すること。

D:\home>cd site\wwwroot

D:\home\site\wwwroot>curl -O http://download.jboss.org/wildfly/9.0.1.Final/wildfly-9.0.1.Final.zip

D:\home\site\wwwroot>unzip wildfly-9.0.1.Final.zip

次に D:\home\site\wwwroot\web.config を作成して以下を記載する。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <handlers>
            <add name="httpPlatformHandler" path="*" verb="*" 
                 modules="httpPlatformHandler" resourceType="Unspecified" />
        </handlers>
        <httpPlatform processPath="D:\home\site\wwwroot\wildfly-9.0.1.Final\bin\standalone.bat" arguments="-Djboss.http.port=%HTTP_PLATFORM_PORT%" startupTimeLimit="60">
            <environmentVariables>
                <environmentVariable name="JAVA_OPTS" value="-Djava.net.preferIPv4Stack=true" />
            </environmentVariables>
        </httpPlatform>
    </system.webServer>
</configuration>

WildFly が利用する HTTP ポートを HTTP_PLATFORM_PORT 環境変数にて設定し、httpPlatformHandler のプロセス起動待ち時間を60秒に設定している点に注意してほしい。WildFlyEJB コンテナを持つ JavaEE サーバであるため、Tomcat や Jetty に比べるとプロセスの起動に時間がかかる。そのため、startupTimeLimit デフォルトの 10秒では WildFly のプロセス起動に失敗する場合がある。

注意点

  • 2015年8月時点ではデフォルトの %JAVA_HOME% が JDK7 になっているので、JDK8 を利用する場合は管理ポータルで JDK8 を有効化する等、設定を見直すこと
  • WildFly 自体は利用できるが、HttpPlatformHandler は単一ポートの HTTP トラフィックしか流してくれないので JMX や管理ポータルに疎通できない

*1:念のため Kudu 経由で ipconfig をしたら相変わらず Access is denied とでたので、JDK 向けに特殊な処理をしているらしい