はじめに
こんにちは、ソリューションアーキテクトの林です。普段は、コンテナという技術にフォーカスして、さまざまな業種、業態のお客様を支援させていただいています。
本シリーズは、 AWS App Runner と Spring Boot をテーマに、 簡単なアプリケーションをデプロイするところから始めて、データベースやキャッシュサーバーとの接続、秘匿情報の管理、Spring Boot Actuator によるメトリクスの取得とその活用、カナリアリリースなど、本番環境で実施したい諸々の方法を皆様にご紹介していくものです。
前回 は、App Runner サービスから、AWS Systems Manager (SSM) Parameter Store や AWS Secrets Manager に保存された設定、秘匿情報を参照する方法を解説しました。これにより、データベースの接続情報や API のキー情報をセキュアに管理できるようになりました。
最終回となるこの記事では、App Runner サービスのモニタリングについて解説します。また、モニタリングした情報を元にアプリケーションのリリースを段階的に行う、Progressive Delivery について App Runner でどのように実施すればいいのか、例をご紹介します。
ご注意
本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。
builders.flash メールメンバー登録
builders.flash メールメンバー登録で、毎月の最新アップデート情報とともに、AWS を無料でお試しいただけるクレジットコードを受け取ることができます。
AWS App Runner とモニタリング
AWS App Runner は、デフォルトでモニタリングの仕組みを備えています。
ログ、メトリクス
サービスへのリクエストを発生させる
コマンド
watch curl -s https://xxx.xxx.awsapprunner.com # App Runner にデプロイしたサービスのURL
Metrics ダッシュボード

トレース
App Runner では、AWS X-Ray でトレースを取得 できます。
トレースを取得するための設定も容易です。
- App Runner のトレース機能を有効化する
- アプリケーションに OpenTelemetry SDK を組み込み、OpenTelemetry 形式でトレースをパブリッシュする
上記を試してみましょう。App Runner のトレース機能は、App Runner のコンソールから有効化できますが、今回はすでに AWS CDK でサービスを管理しているため、CDK から有効化してみます。
以下の通り、既存の CDK コードの lib/infra-stack.ts に設定を追加します。

ib/infra-stack.ts に設定を追加
スクリプト
...
const instanceRole = new iam.Role(this, 'InstanceRole', {
...
});
// サービスが X-Ray にトレース情報を公開できるよう権限を追加する
instanceRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("AWSXRayDaemonWriteAccess"));
// X-Ray 機能を有効化する、App Runner のオブザーバビリティ設定を追加
const appRunnerObservability = new apprunner.CfnObservabilityConfiguration(this, 'ObservabilityConfiguration', {
traceConfiguration: {
vendor: "AWSXRAY",
},
});
...
// App Runner サービスを作成
const service = new apprunner.CfnService(this, 'Service', {
sourceConfiguration: {
...
},
// 上記で定義したオブザーバビリティの設定をサービスに組み込む
observabilityConfiguration: {
observabilityEnabled: true,
observabilityConfigurationArn: appRunnerObservability.attrObservabilityConfigurationArn,
},
...
デプロイして App Runner サービスに反映
上記の設定をデプロイして App Runner サービスに反映します。
npx cdk deploy --all
成功 !
これにより、アプリケーションから X-Ray のトレース情報をパブリッシュできるようになりました。
続けて、アプリケーションが実際にトレース情報を出力するように修正します。 App Runner では、トレース情報を OpenTelemetry を使って公開することになっています。OpenTelemetry は、Cloud Native Computing Foundation (CNCF) でホストされているプロジェクトで、アプリケーションの分散トレース、メトリクス、ログを収集するための API、ライブラリ、エージェントを提供しています。
ここでは、AWS Distro for OpenTelemetry (ADOT) という、AWS が開発している OpenTelemetry のディストリビューションを利用して、アプリケーションがトレース情報を公開するように設定します。
AWS Distro for OpenTelemetry (ADOT) でトレースを取得する
ADOT は Java の自動計装に対応しており、アプリケーションコードを変更しなくてもライブラリを設定するだけでトレース情報を取得できます。 具体的には、以下のように起動時に Java エージェントの仕組みを利用することで、ADOT が Java コードにトレース出力のコードを挿入してくれます。
java -javaagent <ADOT のエージェントの jar> -jar <アプリケーションのjar>
アプリケーションのビルドと実行を変更
上記のような呼び出しになるよう、アプリケーションのビルドと実行を変更しましょう。 これまで作成したコードのうち、 build.gradle を以下の通り変更します。
repositories {
mavenCentral()
}
// Java エージェントのjarを格納する依存関係グループ
configurations {
javaAgent
}
dependencies {
...
// ADOT Java エージェントのダウンロード
javaAgent('software.amazon.opentelemetry:aws-opentelemetry-agent:1.30.0')
}
// ダウンロードした ADOT Java エージェントをコピー
task agentLibs(type: Copy) {
from configurations.javaAgent.singleFile
into layout.buildDirectory.dir('libs')
rename 'aws-opentelemetry-agent-.*\\.jar', 'aws-opentelemetry-agent.jar'
}
// アプリケーションの jar をビルドした際に ADOT の jar も取得する
bootJar {
dependsOn agentLibs
}
環境変数を設定
そして、OpenTelemetry でトレースを出力するための環境変数をアプリケーションに設定します。lib/service-stack.ts に以下の設定を追加します。
// App Runner サービスを作成
const service = new apprunner.CfnService(this, 'Service', {
sourceConfiguration: {
...
codeRepository: {
...
codeConfiguration: {
configurationSource: "API",
codeConfigurationValues: {
...
runtimeEnvironmentVariables: [
// Java エージェントを設定
{
name: "JAVA_TOOL_OPTIONS",
value: "-javaagent:/app/aws-opentelemetry-agent.jar",
},
// トレースに X-Ray を設定
{
name: "OTEL_PROPAGATORS",
value: "xray",
},
// OpenTelemetry はトレース以外にメトリクスも本来出力できるが、App Runner の
// OpenTelemetry サポートはメトリクスに対応していないため無効化する
{
name: "OTEL_METRICS_EXPORTER",
value: "none",
},
// X-Ray 上のサービス名を設定
{
name: "OTEL_RESOURCE_ATTRIBUTES",
value: "service.name=expt_apprunner_springboot",
},
],
...
CDK とアプリケーションをデプロイ
以上でトレースの出力設定は完了です。CDK とアプリケーションをデプロイしてみましょう。
npx cdk deploy --all
アプリケーションをデプロイ

トレース確認
完了後、アプリケーションに何度かアクセスすると、以下のように X-Ray でネットワーク、メソッド呼び出しのトレースが確認できるようになります。
watch curl -s https://xxx.xxx.awsapprunner.com # App Runner にデプロイしたサービスのURL
画面イメージ


Actuator でアプリケーションメトリクスを取得する
しかし、プロダクション環境では、さらに詳細なアプリケーションレベルのメトリクスを取得したいケースもあるでしょう。
Java アプリケーションでは、Micrometer という Java ライブラリでアプリケーションのメトリクスを取得することが多いのではないでしょうか。この Micrometer はメトリクスのファサードとして動作し、アプリケーションのメトリクスを CloudWatch などさまざまなモニタリングのサービスにメトリクスを送信できます。

同じくメトリクスを CloudWatch に送信
Spring Boot では、Actuator というモジュールでさまざまなメトリクスを公開できますが、この Micrometer を利用しているので、同じくメトリクスを CloudWatch に送信できます。そのための設定を見てみましょう。
ライブラリを追加
まず、これまで同様 build.gradle を編集して、Micrometer と Actuator のライブラリを追加します。
dependencyManagement {
imports {
mavenBom 'io.awspring.cloud:spring-cloud-aws-dependencies:2.4.4'
}
}
dependencies {
...
implementation('io.awspring.cloud:spring-cloud-starter-aws')
implementation('org.springframework.boot:spring-boot-starter-actuator')
implementation('io.micrometer:micrometer-registry-cloudwatch')
...
}
Spring Boot に設定追加
次に、src/main/resources/application.yaml を下記のように編集し、メトリクスを CloudWatch に送信するよう Spring Boot に設定を加えましょう。
...
spring:
redis:
host: ${CACHE_HOST:localhost}
port: ${CACHE_PORT:6379}
password: ${CACHE_PASSWORD:}
management:
endpoint:
health:
show-details: always
metrics:
enable:
process: true
spring: true
tomcat: true
jvm: true
export:
cloudwatch:
namespace: expt-apprunner-springboot
enabled: true
step: 1s
lib/infra-stack.ts に権限設定を追加
アプリケーションの設定は整いました。 ここまでの実装では、AppRunner から CloudWatch へメトリクスを送信する権限が不足しているため、既存の CDK コードの lib/infra-stack.ts に権限設定を追加します。
const instanceRole = new iam.Role(this, 'InstanceRole', {
assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'),
inlinePolicies: {
....
cloudwatch: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [ "cloudwatch:PutMetricData" ],
resources: [ "*" ],
}),
],
}),
},
});
アプリケーションをデプロイ
以上で、CloudWatch に対してメトリクスを送る準備は完了です。
それでは、デプロイして動作を確認してみましょう。まず、CDK をデプロイし、先ほどと同様にマネジメントコンソールからアプリケーションをデプロイします。
npx cdk deploy --all

メトリクス確認
完了後、アプリケーションに何度かアクセスすると、CloudWatch メトリクスのコンソール 上で、「expt-apprunner-springboot」以下に Spring Boot の Actuator が出力するさまざまなメトリクスが確認できるようになりました。
watch curl -s https://xxx.xxx.awsapprunner.com # App Runner にデプロイしたサービスのURL

成功 !
いかがだったでしょうか。アプリケーションメトリクスやトレースでは、ライブラリの追加と少しの App Runner 設定変更で簡単にデータを収集できることがお分かりいただけたかと思います。また、CPU 使用率や HTTP リクエスト数などの Web サービスにおいて一般的に必要とされるメトリクスやログに至っては、標準機能として予め App Runner に備わっていることも知ることができました!
次は、こうして収集されたメトリクスを使って高度なリリース戦略を実現してみましょう。
アプリケーションメトリクスを元にリリースを管理する
AWS App Runner は ブルーグリーン形式でサービスをデプロイします。サービスの起動に失敗した場合は、トラフィックを失敗したサービスに振り向けることなくロールバックするため、サービスアクセスでエラーが発生することはありません。
しかし、プロダクションでアプリケーションを運用する場合、もう少し複雑なリリース戦略を採用したくなるかもしれません。例えば、現在、 App Runner は前述の通りさまざまなメトリクスを取得できますが、そのメトリクスを元にロールバックしたり、Progressive Delivery と呼ばれるような、サービスを少しずつエンドユーザーに公開し、メトリクスで異常があった場合にロールバックするというような手法などはサポートされていません。
しかし、他の AWS サービスをビルディングブロックとして組み合わせることで、App Runner でもこうしたリリース戦略を実現できます。
AWS AppConfig で Progressive Delivery を実装する
AWS AppConfig は、アプリケーションの設定を管理、デプロイするためのマネージドサービスです。アプリケーションは、AppConfig から実行時に設定を動的に読み込むことで、アプリケーションそのものの再デプロイや再起動を行うことなくその挙動を変更できます。
AppConfig には機能フラグを管理する仕組みがあります。これにより、新しい機能のリリースを AppConfig で管理できます。つまり、アプリケーションを再デプロイすることなく、AppConfig のコンソールや API を使って機能のリリースを制御できます。
AppConfig には CloudWatch のアラームを受けて設定をロールバックする機能もあるため、モニタリング結果をリリースに反映させることも可能です。
ここでは、新しい機能を本番環境の Spring Boot アプリケーションに導入する、という想定で、この AppConfig を使った機能リリースを体験してみましょう。

AWS AppConfig の設定
まず、AppConfig に機能フラグを設定するために、AppConfig のコンソール 上でアプリケーションの作成を行います。この時、選択しているリージョンが App Runner でアプリケーションをデプロイしているリージョンと同じであることを確認しておきましょう。

名前を設定
コンソール上では、「Create application」をクリックし、「expt-apprunner-springboot」などの名前を設定します。


プロファイルを作成
次に、プロファイルを作成していきます。Configuration Profiles and Feature Flags の項目で「Create」をクリックして新しいプロファイルを用意します。

プロファイルタイプ
続くプロファイルタイプとして「Feature Flag」を選び、「feature1」という名前をつけましょう。



機能フラグを追加
さて、プロファイルは出来上がったので、ここに具体的な機能フラグを追加していきます。「+ Add new flag」をクリックし、詳細設定画面で Name に mode と入力しますと、Key にも自動的に反映されます。



新しいバージョンとして保存
フラグが追加できたら、「Save new version」をクリックして、この設定を新しいバージョンとして保存します。

デプロイのための設定を追加
正常に保存ができたら、次は「Start deployment」をクリックしてデプロイのための設定を追加していきます。

環境定義
現時点では、デプロイ先の環境を選択するドロップボックスに候補がないので、「Create environment」をクリックして環境の定義を作成しましょう。

名前を設定
今回は本番環境へのデプロイを想定しているため、デプロイ先の環境として「prod」という名前を設定し「Create environment」をクリックします。

AppConfig.AllAtOnece (Quick) を選択
またデプロイ戦略としては、すべてのターゲットに対して即座に設定のデプロイを行う「AppConfig.AllAtOnece (Quick)」を選択します。
AppConfig で用意しているその他のデプロイ戦略や、独自のデプロイ戦略の作成方法については AppConfig のドキュメント にまとまっています。

設定のデプロイ
最後に「Start deployment」をクリックして設定のデプロイを行いましょう。Deployment status のプログレスバーが進んでいることを確認してください。

Spring Boot アプリケーションに新機能を作成する
Spring Boot アプリケーションが AWS AppConfig から上記で設定した機能フラグを取得するために、AppConfig 用の AWS SDK をライブラリとしてアプリケーションの設定に追加していきます。
build.gradle を編集
まず、build.gradle を以下のように編集します。
dependencies {
....
// AppConfig の SDK
implementation('software.amazon.awssdk:appconfigdata:2.20.162')
runtimeOnly('software.amazon.awssdk:sts:2.20.162')
...
}
ファイルを作成
次に、アプリケーションに新しい Controller を作成したいので、新規に以下の内容で src/main/java/com/example/exptapprunnerspringboot/FeatureOneController.java ファイルを作成してください。
package com.example.exptapprunnerspringboot;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FeatureOneController {
@Autowired
private FeatureFlag featureFlag;
@GetMapping("/feature1")
public Map<String, Boolean> root() throws InterruptedException {
if (featureFlag.isFeatureOneEnabled()) {
// The feature is assumed to consume a bit time.
Thread.sleep(3000);
}
return Map.of("feature1", featureFlag.isFeatureOneEnabled());
}
}
FeatureFlag クラスを作成
続いて、AppConfig から機能フラグを取得するための FeatureFlag クラスを作成します。こちらも、新規に以下の内容で src/main/java/com/example/exptapprunnerspringboot/FeatureFlag.java ファイルを作成してください。Java アプリケーションから AppConfig 上の設定を取得する詳細な方法については、AppConfig のドキュメント や AWS SDK for Java のドキュメント を参照してください。
FeatureFlag クラスを作成
コード
package com.example.exptapprunnerspringboot;
import java.io.UncheckedIOException;
import java.io.Serializable;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient;
import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse;
import software.amazon.awssdk.services.appconfigdata.model.ResourceNotFoundException;
import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest;
@Component
public class FeatureFlag {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final ObjectMapper objectMapper;
private final AppConfigDataClient client = AppConfigDataClient.builder().build();
private final String appName;
private final String envName;
private String sessionToken;
private volatile boolean feature1 = false;
public FeatureFlag(@Autowired ObjectMapper objectMapper,
@Value("${settings.app.name}") String appName,
@Value("${settings.env.name}") String envName) {
this.objectMapper = objectMapper;
this.appName = appName;
this.envName = envName;
}
@Scheduled(fixedRate = 5000)
public void scheduling() {
String configurationProfile = "feature1";
try {
refresh(appName, envName, configurationProfile);
} catch (ResourceNotFoundException ex) {
logger.warn("The AppConfig resource has not been deployed: {}, {}, {}, {}",
appName, envName, configurationProfile, ex.getMessage());
}
}
public void refresh(String appName, String envName, String configurationProfile) {
if (this.sessionToken == null) {
StartConfigurationSessionRequest request = StartConfigurationSessionRequest.builder()
.applicationIdentifier(appName)
.environmentIdentifier(envName)
.configurationProfileIdentifier(configurationProfile)
.build();
this.sessionToken = client.startConfigurationSession(request).initialConfigurationToken();
}
GetLatestConfigurationResponse response = client.getLatestConfiguration((s) -> {
s.configurationToken(this.sessionToken);
});
if (response.configuration().asByteArray().length > 0) {
String featureValue = response.configuration().asUtf8String();
logger.info("AppConfig changed: {}", featureValue);
try {
FeatureMode featureOne = objectMapper.readValue(featureValue, FeatureMode.class);
this.feature1 = featureOne.isEnabled();
} catch (JsonProcessingException | UncheckedIOException e) {
throw new RuntimeException(e);
}
}
this.sessionToken = response.nextPollConfigurationToken();
}
public boolean isFeatureOneEnabled() {
return this.feature1;
}
private static class FeatureMode implements Serializable {
@JsonProperty
private Map<String, Boolean> mode;
public boolean isEnabled() {
if (mode != null) {
return mode.get("enabled");
} else {
return false;
}
}
}
}
src/main/resources/application.yaml を編集
また、src/main/resources/application.yaml を下記のように編集し、AppConfig のアプリケーション名とデプロイ先環境名の環境変数を設定します。
settings:
app:
name: expt-apprunner-springboot
env:
name: ${APPCONFIG_ENV_NAME:dev}
lib/service-stack.ts に設定を追加
アプリケーションへの設定が完了したので、次は CDK コード lib/service-stack.ts にも設定を追加しましょう。 まず、App Runner から AppConfig の機能フラグを取得するために必要な権限を追加します。そして、先ほど application.yaml に記載した環境変数の設定も追加しています。
const instanceRole = new iam.Role(this, 'InstanceRole', {
assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'),
inlinePolicies: {
appconfig: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"appconfig:GetLatestConfiguration",
"appconfig:StartConfigurationSession",
],
resources: [ "*" ],
}),
],
}),
....
codeConfiguration: {
configurationSource: "API",
codeConfigurationValues: {
runtime: "CORRETTO_11",
buildCommand: "./gradlew bootJar && cp build/libs/*.jar ./",
startCommand: "java -jar ./expt-apprunner-springboot.jar",
runtimeEnvironmentVariables: [
...
{
name: "APPCONFIG_ENV_NAME",
value: "prod",
},
動作確認
以上で、AppConfig を利用した機能フラグの実装が完了しました。 それでは、デプロイして動作を確認してみましょう。まず、CDK をデプロイし、先ほどと同様にマネジメントコンソールからアプリケーションをデプロイします。
npx cdk deploy --all
追加した機能フラグの振る舞いを確認
デプロイが完了したら、追加した機能フラグの振る舞いを確認するために、/feature1 のパスにアクセスします。
watch curl -s https://xxx.xxx.awsapprunner.com/feature1 # App Runner にデプロイしたサービスのURL
{"feature1":false}
AppConfig で mode と名付けたフラグを有効化
まだ機能フラグは有効化されていないので、feature1 に対して false が返ってきましたね。
それでは、新機能のリリースを想定して AppConfig で mode と名付けたフラグを有効化してみましょう。
まず、AppConfig のコンソール上で mode のトグルボタンをクリックしてフラグを有効にします。

設定のデプロイを開始
次に、「Save new version」で mode が有効な状態を新たなバージョンとして保存し、「Start deployment」をクリックして設定のデプロイを開始します。

/feature1 のパスにアクセスして確認
デプロイは成功したでしょうか。再度、/feature1 のパスにアクセスして確認してみましょう。
watch curl -s https://xxx.xxx.awsapprunner.com/feature1 # App Runner にデプロイしたサービスのURL
{"feature1":true}
まとめ
おめでとうございます!無事に新機能はリリースされ、feature1 に対して true が返ってきましたね。
いかがでしたでしょうか。AppConfig による機能リリースをご紹介しました。 AppConfig には、CloudWatch のアラームがトリガーされたときに、設定をロールバックする機能もあります。前述の通り、App Runner ではさまざまなメトリクスを CloudWatch で収集できるので、それらのメトリクスを使って、新機能リリースで何かエラーが発生したり、閾値を超えてしまったときに自動的にロールバックする、といったリリースも実現可能です。
さて、4 回にわたって連載してきた Spring Boot と AWS App Runner の連載も、今回で最終回となります。
App Runner では、連載を通してご覧いただいた通り、アプリケーションを簡単にデプロイすることができます。一方で、IaC で環境を管理したり、モニタリングやさまざまなリリース戦略など、複雑な運用が必要になってきたときでもさまざまな戦略を実装することが可能です。
皆さんもぜひ、お手持ちの Spring Boot アプリケーションを、運用の容易な App Runner でデプロイしてみていただければと思います。
筆者プロフィール

林 政利 (@literalice)
アマゾン ウェブ サービス ジャパン合同会社
コンテナスペシャリスト ソリューションアーキテクト
フリーランスや Web 系企業で業務システムや Web サービスの開発、インフラ運用に従事。近年はベンダーでコンテナ技術の普及に努めており、現在、AWS Japan で Amazon ECS や Amazon EKS でのコンテナ運用や開発プロセス構築を中心にソリューションアーキテクトとして活動中。

吉田 英史
アマゾン ウェブ サービス ジャパン合同会社
技術統括本部 ソリューションアーキテクト
東海地方でこれから AWS を使い始めるお客様を技術面でサポートしているソリューションアーキテクトです。
かつては製造業で製品の組み込みソフトウェア開発や工場 IoT 構築などに従事してきました。これからクラウド活用に踏み出されるお客様の支援ができることを、何より嬉しく感じています。ーションアーキテクトです。
かつては製造業で製品の組み込みソフトウェア開発や工場 IoT 構築などに従事してきました。これからクラウド活用に踏み出されるお客様の支援ができることを、何より嬉しく感じています。
Did you find what you were looking for today?
Let us know so we can improve the quality of the content on our pages