前回の記事で、ECSにデプロイしたDockerコンテナからFireLensとfluentbitでログ出力ができるようになりました。
今回は、ログの出し分けについて説明します。
出し分けるログ
ログはExpress.jsへのアクセスログと、アプリケーション内からのアプリケーションログの2つを想定します。
まずは、それぞれのログの出し方です。
アクセスログ
Express.jsのアプリのアクセスログは、Morganというパッケージを使うと簡単に出力できます。
まずは、morganをインストールします。
npm i --save morgan
npm i --save-dev @types/morgan
アプリのindex.ts
に下記のように記述します。
import 'dotenv/config'
import http from 'http'
import morgan from 'morgan' // この行を追記
import corm from 'cors'
import express from 'express'
// ログの出力フォーマット(これはWebサーバのアクセスログ風)
const morganFormat = '[ACCESS] :remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'
const app: express.Express = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(morgan(morganFormat)) // この行を追記
app.use(cors())
const server: http.Server = http.createServer(app)
server.listen(process.env.PORT, () => {
console.log(`Start on port ${process.env['PORT']}.`)
})
これでアクセスログが出力されるようになります。
先頭に[ACCESS]
と付けているのは、後でログを出し分けるためです。
アプリケーションログ
アプリケーションログは、log4jsを使って出力すると良いでしょう。
まずはパッケージのインストールを行います。
npm i --save log4js
npm i --save-dev @types/log4js
index.ts
と同じフォルダに、logger.ts
を作成し、下記のようにします。
import * as log4js from 'log4js'
log4js.configure({
appenders: {
out: {
type: 'stdout',
layout: {
type: 'pattern',
pattern: '[%p] %c - %m'
}
}
},
categories: {
default: { appenders: ['out'], level: 'debug' }
}
})
export const logger = log4js.getLogger()
ここでエクスポートしたlogger
を、ログを出したい箇所でインポートしてログ出力します。
例えば、index.ts
の中で・・・(追記箇所のみ)
import { logger } from './logger'
const server: http.Server = http.createServer(app)
server.listen(process.env.PORT, () => {
logger.info(`Start on port ${process.env['PORT']}.`) // この行を追記
})
このようにすると、ログレベルに合わせてログが出力されます。また、logger.tsではログレベルをログの先頭に出力するようにしているので、この例では[INFO]
という文字列になります。
ログの出し分け
いよいよログの出し分けです。前の記事ではCloudWatchにのみログを出していますが、今回はアクセスログはS3、アプリケーションログはCloudWatchという風にしてみましょう。
FireLensからS3にログを出力する際に、Kinesys Firehoseを使用します。
CDKのlib/cdk-stack.ts
を下記のようにします。(関連箇所のみ)
ログ出力用のS3バケットの作成と、Firehoseのストリーム作成が重要です。コンテナから出力されるログとFirehoseストリームの紐付けはここでは行わず、次にextra.conf
で行います。
// タスク定義
const taskDefinition = new ecs.FargateTaskDefinition(this, 'MyTaskDefinition', {
runtimePlatform: {
operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
cpuArchitecture: ecs.CpuArchitecture.X86_64,
}
})
// ログ出力用のS3バケットの作成
const logBucket = new s3.Bucket(this, 'MyLogBucket', {
bucketName: '<ログを出力するバケット名>',
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true
})
// Firehose用のロール定義
const firehoseRole = new iam.Role(this, 'FirehoseRole', {
assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com')
})
firehoseRole.addToPolicy(new iam.PolicyStatement({
actions: [
's3:PutObject',
's3:GetObject',
'a3:ListBucket'
],
resources: [
logBucket.bucketArn,
`${logBucket.bucketArn}/*`
]
}))
// S3へのログ出力用のストリーム作成
const stream = new firehose.CfnDeliveryStream(this, 'MyDeliveryStream', {
s3DestinationConfiguration: {
bucketArn: logBucket.bucketArn,
roleArn: firehoseRole.roleArn
},
deliveryStreamName: 'helloecs-stream'
})
// 前の記事で説明したconfファイルの読み込み
const parserConfAsset = new assets.Asset(this, 'ParserConfAsset', {
path: path.join(__dirname, 'parser.conf')
})
const extraConfAsset = new assets.Asset(this, 'ExtraConfAsset', {
path: path.join(__dirname, 'extra.conf')
})
// この辺は前の記事と同様
taskDefinition.addFirelensLogRouter('MyLogRouter', {
firelensConfig: {
type: ecs.FirelensLogRouterType.FLUENTBIT,
},
environment: {
aws_fluent_bit_init_s3_1: `arn:aws:s3:::${parserConfAsset.s3BucketName}/${parserConfAsset.s3ObjectKey}`,
aws_fluent_bit_init_s3_2: `arn:aws:s3:::${extraConfAsset.s3BucketName}/${extraConfAsset.s3ObjectKey}`,
},
image: ecs.ContainerImage.fromRegistry(
'public.ecr.aws/aws-observability/aws-for-fluent-bit:init-latest'
),
logging: ecs.LogDriver.awsLogs({
streamPrefix: 'log-router'
})
})
taskDefinition.defaultContainer = taskDefinition.addContainer('MyAppContainer', {
image: ecs.ContainerImage.fromEcrRepository(repository, versionTag),
logging: ecs.LogDrivers.firelens({
options: {}
}),
portMappings: [
{
containerPort: 3000,
}
],
})
// ログ出力用のポリシー定義
const logPolicyStatement = new iam.PolicyStatement({
actions: [
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:DescribeLogStreams",
"logs:PutLogEvents",
"s3:GetObject",
"s3:GetBucketLocation",
"firehose:PutRecordBatch",
],
resources: ["*"],
effect: iam.Effect.ALLOW,
});
taskDefinition.addToTaskRolePolicy(logPolicyStatement);
次に、extra.conf
です。
[SERVICE]
Parsers_File parser.conf
[FILTER]
Name parser
Match *-firelens-*
Key_Name log
Parser json
Preserve_Key false
Reserve_Data true
[FILTER]
Name rewrite_tag
Match *-firelens-*
Rule $log ^\[(DEBUG|INFO|WARN|ERROR)\] APPLOG false
Emitter_Name app_logs
[FILTER]
Name rewrite_tag
Match *-firelens-*
Rule $log ^\[ACCESS\] ACCESSLOG false
Emitter_Name access_logs
[OUTPUT]
Name cloudwatch
Match APPLOG
region ap-northeast-1
log_group_name /hello/ecs
log_stream_prefix ecs-fluentbit-
auto_create_group true
[OUTPUT]
Name firehose
Match ACCESSLOG
region ap-northeast-1
delivery_stream helloecs-stream
コンテナの標準出力の内容(アクセスログやアプリケーションログ)は、FireLens(fluentbit)で取り込まれます。
その際、-firelens-
というタグが自動的に付与されるので、それにマッチするログを処理します。
ログはJSON形式でやってきて、アクセスログやアプリケーションログの文字列は$log
に格納されています。
その$log
の文字列の先頭の値([INFO]
や[ACCESS]
など)を正規表現でマッチさせ、APPLOGやACCESSLOGというタグに置き換えます(rewrite_tagフィルタ)。
後は、2つの[OUTPUT]
の設定でAPPLOGやACCESSLOGというタグをマッチさせ、CloudWatchまたはFirehose(経由でS3)のそれぞれに処理させるというわけです。
先ほど、cdk-stack.ts
で作成したFirehoseストリームとの紐付けは、delivery_stream
の設定値で行います。
これでログの出し分けができるようになりました。