こんにちは。クラウドインテグレーション事業部の上原です。
弊社では「クラウドインテグレーション」の名前の通り、様々なクラウドサービスを活用したシステムを提供しています。
私がいま参加しているプロジェクトでは、インフラとして
ググっても情報が出てこないことでおなじみ Microsoft Azureを採用しています。
このプロジェクトで新しくバッチアプリケーションを作成することになり、そのためのサービスや構成について色々と調査する機会がありましたので、今回はこちらを共有させて頂こうと思います。
対象アプリケーション
環境
Azure CLI のインストール | Microsoft Docs
要件
あまり具体的には言及できないので、仮に「社用車の動態管理システム」と置き換えて説明します。
今回作成するバッチは、15分間隔で以下の処理を行います。
- 支社(Branch)の一覧をDBから取得する
- 支社毎に、管理されている社用車(Vehicle)の走行データを外部システムから収集する
- 社用車がどのように移動したかを分析し、社用車に紐づく取引先への訪問履歴レコード(VisitHistory)を生成する
- 訪問履歴レコードをDBに登録する
このバッチはSpring Boot製の既存アプリケーションの拡張という位置づけであり、既存の資産を流用する必要があります。 そのため、このバッチもSpring Bootで実装し、できるだけコードがAzureに依存しないような構成を目指しました。
構成1:とにかく動かしてみる
まずはとにかく動かしてみましょう。
15分おきの実行制御から分析処理までをアプリケーション内で完結させ、それをサーバ上で常時稼働させます。
Azureでサーバインスタンスを立ち上げるには、App Service というサービスを使用します。
まずは稼働するサーバの性能を定義する App Service Plan を作成し、その中にVMにあたるApp Serviceを作成します。 この辺り、AWSやGCPとはちょっと勝手が異なりますね。
Azureには、Dockerイメージをプライベートに格納できる Azure Container Registry(ACR) というサービスがあります。
そしてACRには、イメージをpushすると自動的にApp Serviceまでデプロイしてくれる機能があります。ありがたく使わせていただきましょう。
アプリケーションの実装
アプリケーションの方ですが、Spring Batchを使って実装してみます。
まずは Spring Initializr(https://start.spring.io/)で雛形を生成。プロジェクトを立ち上げます。
バージョンは既存のアプリケーションに合わせるため、ちょっと古めです。
Spring BatchにはChunk方式とTasklet方式があるのですが、今回は既存の資産を使い回すこともあり、記述の自由度が高いTasklet方式を採用します。
バッチの起動スケジュールやJobの定義はConfigurationに記述し、実装はTaskletに書いていきます。
▼Application.java
@SpringBootApplication @EnableScheduling // スケジューリングを有効化 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
▼MyConfiguration.java
@Configuration @EnableBatchProcessing public class MyConfiguration { private final JobLauncher jobLauncher; private final JobBuilderFactory jobBuilderFactory; private final StepBuilderFactory stepBuilderFactory; private final MyTasklet myTasklet; @Autowired public MyConfiguration( JobLauncher jobLauncher, JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, MyTasklet myTasklet) { this.jobLauncher = jobLauncher; this.jobBuilderFactory = jobBuilderFactory; this.stepBuilderFactory = stepBuilderFactory; this.myTasklet = myTasklet; } @Scheduled(cron = "0 */1 * * * *") // ここで実行間隔を設定 public void schedule() throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException { jobLauncher.run(job(), new JobParametersBuilder().toJobParameters()); } @Bean public Job job() { return this.jobBuilderFactory.get("job") .incrementer(new RunIdIncrementer()) .start(step()) .build(); } @Bean public Step step() { return this.stepBuilderFactory.get("step") .allowStartIfComplete(true) .tasklet(myTasklet) .build(); } }
@Component public class MyTasklet implements Tasklet { @Override public RepeatStatus execute(@Nullable StepContribution contribution, @Nullable ChunkContext chunkContext) { List<Branch> branches = new ArrayList<>(); // (支社リストをDBから取得する処理) List<VisitHistory> visitHistories = branches.parallelStream() .map(this::analyze) .flatMap(Collection::stream) .collect(Collectors.toList()); // (訪問履歴を登録する処理) return RepeatStatus.FINISHED; } private List<VisitHistory> analyze(Branch branch) { List<VisitHistory> results = new ArrayList<>(); // (解析処理) return results; } }
デフォルトの挙動だと起動直後にバッチ処理が実行されてしまうため、設定ファイル(yaml)から自動実行をオフにします。
DBへの接続情報も忘れずに書いておきましょう。build.gradle
のdependenciesにjdbcドライバを追加しておく必要もあります。
▼application.yml
spring: datasource: url: # (jdbc接続文字列) driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver batch: job: enabled: false
▼build.gradle
dependencies { implementation 'org.springframework.boot:spring-boot-starter-batch' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc:8.4.0.jre8' // 以下略 }
では、まずはローカルで動かしてみます。
$ ./gradlew bootRun
ちゃんと定義どおりのスケジュールで動いてくれているようです。
Azureへのデプロイ
続いて、作成したアプリケーションをAzure環境にデプロイします。
なお、リソース(ACR・App Service)はあらかじめAzureポータル上で作成済であるものとします。
プロジェクト直下にDockerfileを作成し、コンテナの起動と同時にアプリケーションが実行されるように設定します。
https://docs.microsoft.com/ja-jp/azure/container-registry/container-registry-get-started-docker-cli
▼Dockerfile
FROM adoptopenjdk/openjdk8:alpine EXPOSE 8080 ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","/app.jar"]
App Service側の設定も必要です。
App Serviceはデフォルトでポート80をリッスンする仕様になっているため、これをアプリに合わせて8080に変更します。 継続的デプロイ*1やログの有効化も忘れずに行いましょう。
*2
▼setupAppService.sh
resourceGroupName=my-resource-group # リソースグループ名 appName=my-batch-app # App Serviceのリソース名 dockerName=my_batch # イメージ名 registryName=mybatchregistry # ACRのリソース名 # 環境変数の設定 az webapp config appsettings set --resource-group ${resourceGroupName} \ --name ${appName} \ --settings WEBSITES_PORT=8080 && # デプロイ設定 az webapp config container set --name ${appName} \ --resource-group ${resourceGroupName} \ --docker-custom-image-name ${registryName}.azurecr.io/${dockerName}:latest \ --docker-registry-server-url https://${registryName}.azurecr.io && # 継続的デプロイの有効化 az webapp deployment container config --name ${appName} \ --resource-group ${resourceGroupName} \ --enable-cd true && # ログの有効化 az webapp log config --resource-group ${resourceGroupName} \ --name ${appName} \ --docker-container-logging filesystem
それでは、Dockerイメージを作成し、ACRにデプロイしてみましょう。
▼deploy.sh
dockerName=my_batch # イメージ名 registryName=mybatchregistry # ACRのリソース名 # ビルド ./gradlew clean build -x test && docker build . -t ${dockerName} && # デプロイ az acr login --name ${registryName} && docker tag ${dockerName} ${registryName}.azurecr.io/${dockerName}:latest && docker push ${registryName}.azurecr.io/${dockerName}:latest && docker rmi ${registryName}.azurecr.io/${dockerName}:latest
▼ Azure Portalの「コンテナーの設定」
無事に動き始めました。
デメリット
以上でひとまず動くようになりましたが、この構成にはいろいろと問題があります。
App Serviceには自動/手動でインスタンス数を変える(スケールアウト)機能があるのですが、
今の実装は1アプリケーション内で処理が完結する前提になっているため、何かしらの工夫をしない限り、台数を増やすことができません。
幸い、分析処理は支社毎に独立して実行できるため、キューを利用してメッセージ数(支社数)に応じてスケールするパターンが適用できそうです。
また、サーバは常時稼働となるため、待機中も料金がかかり続けます。
Azureにはサーバレスの仕組みがあるので、うまく利用して実行中だけ課金されるようにしたいですね。
…こうしてどんどん深みにハマっていくわけですが、詳しくは次回の記事でご説明しようと思います。
それでは!