コンテナベースのアプリケーションに対して Application Signals の設定全般を CDK で行ってみた
今回は ECS を利用したコンテナベースの Web アプリを作成して、CDK で Application Signals の設定全般を行ってみます。
Web アプリは下記技術スタックで作成します。
機能 | 技術スタック | 補足 |
---|---|---|
言語 | TypeScript | |
ランタイム | Node.js | |
IaC | AWS CDK | |
データベース | Amazon Aurora Serverless v2(PostgreSQL) | |
コンピューティング | AWS ECS on AWS Fargate | |
フレームワーク | Express |
構成
やっていることは [アップデート]Amazon CloudWatch Application Signals が GA しました!CDK でサンプル作ってみた とほぼ一緒ですが、下記を変えました。
- Python ではなく、Node.js のアプリケーション
- インスツルメントも CDK の ApplicationSignalsIntegration を利用
- SLO 周りも CDK 管理
特に ApplicationSignalsIntegration は下記のように記述するだけで CloudWatch エージェントや init コンテナの追加、各種環境変数の設定などを行うことができ非常に便利です。
new application_signals_alpha.ApplicationSignalsIntegration(
this,
"ApplicationSignalsIntegration",
{
serviceName: "sample-ecs-app",
taskDefinition: taskDefinition,
instrumentation: {
sdkVersion: application_signals_alpha.NodeInstrumentationVersion.V0_6_0,
},
cloudWatchAgentSidecar: {
containerName: "ecs-cwagent",
enableLogging: true,
cpu: 256,
memoryLimitMiB: 512,
},
}
);
アルファモジュールであることに注意が必要ですが、便利なので積極的に使っていきたいです。
AWS 公式ドキュメントでも、下記ページにて CDK で Application Signals 周りの設定全般を行う手順として紹介されています。
CDK 抜粋と補足
今回私が作成したソースコードは下記リポジトリで公開しているので、ご興味があればご参照下さい。
以後抜粋と補足を行います。
Application Signals のサービスロール作成
専用のサービスロールを作成することで、そのアカウントで Application Signals を利用できるようになります。
CfnDiscovery を利用することで、
AWSServiceRoleForCloudWatchApplicationSignals という名前でサービスロールが作成されます。
// Application Signals Service Role
new application_signals.CfnDiscovery(this, "ApplicationSignalsServiceRole", {});
権限としては AWS マネージドポリシーの CloudWatchApplicationSignalsServiceRolePolicy が付与される形になります。
具体的には下記権限が付与されていました。
- xray:GetServiceGraph
- logs:StartQuery
- logs:GetQueryResults
- cloudwatch:GetMetricData
- cloudwatch:ListMetrics
- tag:GetResources
- application-signals:ListServiceLevelObjectiveExclusionWindows
- application-signals:GetServiceLevelObjective
- autoscaling:DescribeAutoScalingGroups
今回は全部 CDK でやってみましたが、マネジメントアカウントから有効化しても良いです。
ここは無理に CDK 管理しなくても良い部分だと思います。
むしろアプリケーションインフラのスタック内に含めると、いずれサービスロールの同名エラーになる気しかしない...
CDK によるインスツルメント
Application Signals を利用するにあたり、サイドカーとして CloudWatch エージェントを追加したり、インスツルメント用の init コンテナを追加したり、環境変数を付与したりといった手順が必要です。
手順に沿って設定するだけではありますが、この辺りのインスツルメント作業はそれなりに手間です。
ただし、ApplicationSignalsIntegration を利用すれば、自動計装エージェントのバージョンを指定するくらいの設定で完了します。
// ECS Task Definition
const taskDefinition = new ecs.FargateTaskDefinition(this, "TaskDefinition", {
cpu: 512,
memoryLimitMiB: 1024,
taskRole: taskRole,
executionRole: taskExecutionRole,
});
const ecsContainer = taskDefinition.addContainer("App", {
image: ecs.ContainerImage.fromEcrRepository(
ecr.Repository.fromRepositoryName(
this,
"AppRepository",
props.repositoryName
),
props.imageTag
),
essential: true,
secrets: {
DATABASE_USER: ecs.Secret.fromSecretsManager(dbSecret, "username"),
DATABASE_PASSWORD: ecs.Secret.fromSecretsManager(dbSecret, "password"),
DATABASE_HOST: ecs.Secret.fromSecretsManager(dbSecret, "host"),
DATABASE_NAME: ecs.Secret.fromSecretsManager(dbSecret, "dbname"),
},
logging: ecs.LogDrivers.awsLogs({
streamPrefix: "ecs",
logGroup: new logs.LogGroup(this, "AppLogGroup", {
logGroupName: `/ecs/${props.repositoryName}`,
retention: logs.RetentionDays.ONE_WEEK,
removalPolicy: cdk.RemovalPolicy.DESTROY,
}),
}),
});
ecsContainer.addPortMappings({
containerPort: 80,
});
new application_signals_alpha.ApplicationSignalsIntegration(
this,
"ApplicationSignalsIntegration",
{
serviceName: "sample-ecs-app",
taskDefinition: taskDefinition,
instrumentation: {
sdkVersion: application_signals_alpha.NodeInstrumentationVersion.V0_6_0,
},
cloudWatchAgentSidecar: {
containerName: "ecs-cwagent",
enableLogging: true,
cpu: 256,
memoryLimitMiB: 512,
},
}
);
10 行程度追加するだけでコンテナの追加や環境変数の設定が完了しました。
補足
今回は、自動計装エージェントとして 0.6.0 を利用しています。
一時的な問題ではありますが、1 週間前に公開されていた 0.7.0 を選択して ApplicationSignalsIntegration を利用することはできませんでした。
CDK が対応しないと最新バージョンを利用できないという話はありますが、最新のものが公開されたタイミングで即座に更新するケースの方が少ないと思うので、特に問題にはならないとは思います。
今回は Node.js の場合で話していますが、他の言語 (Java, .Net, Python) でも自動計装エージェントのバージョンを扱う専用のクラスがあるので、そちらを利用して ApplicationSignalsIntegration を定義します。
SLO の設定
SLO 用の L2 コンスタントはまだ存在していないものの、CfnServiceLevelObjective を利用することで設定可能です。
export class MonitoringStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: MonitoringStackProps) {
super(scope, id);
// SLO for getUserAvailability - 可用性監視
const getUserAvailabilitySLO =
new application_signals.CfnServiceLevelObjective(
this,
"GetUserAvailabilitySLO",
{
name: "getUserAvailability",
requestBasedSli: {
requestBasedSliMetric: {
keyAttributes: {
Environment: `ecs:${props.clusterName}`,
Name: props.serviceName,
Type: "Service",
},
operationName: "GET /users",
metricType: "AVAILABILITY",
},
},
goal: {
attainmentGoal: 99.9,
warningThreshold: 60.0,
interval: {
rollingInterval: {
durationUnit: "DAY",
duration: 1,
},
},
},
}
);
// SLO for getUserLatency - レイテンシ監視
const getUserLatencySLO = new application_signals.CfnServiceLevelObjective(
this,
"GetUserLatency",
{
name: "getUserLatency",
requestBasedSli: {
requestBasedSliMetric: {
keyAttributes: {
Environment: `ecs:${props.clusterName}`,
Name: props.serviceName,
Type: "Service",
},
operationName: "GET /users",
metricType: "LATENCY",
},
comparisonOperator: "LessThan",
metricThreshold: 300,
},
goal: {
attainmentGoal: 99.9,
warningThreshold: 60.0,
interval: {
rollingInterval: {
durationUnit: "DAY",
duration: 1,
},
},
},
}
);
}
}
監視対象のアプリケーションが Application Signals に認識されていない状態だと作成に失敗するので、MonitoringStack として専用スタックに切り出しました(アプリケーションインフラを作成したから認識されるまでに若干ラグがあります)。
CloudWatch クロスアカウントオブザーバビリティなども使いやすくなりますし、モニタリング周りはとりあえずスタックを分けとくのが良さそうに思います。
今回は SLO が 2 つなのでベタ書きで定義してしまっていますが、SLO が多い場合は aws-observability/application-signals-demo
で実装されているように、共通部分を補完する関数を定義しておくと楽かと思います。
多くの設定値は各 SLO で共通になると思います。
動作確認
各種デプロイが完了すれば、Application Signals のサービスとして認識されます。
SLO 周りも上手く設定できました。
ランタイムメトリクスが表示されないですが、Node.js を利用している場合の仕様なのでこちらは問題ありません。
ランタイムメトリクスは、Node.js アプリケーションでは収集されません。
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/AppSignals-MetricsCollected.html#AppSignals-RuntimeMetrics