今回の変更は殆ど Pull Request でいただきました。感謝です。
リリース直後には初めて GitHub Sponsor までいただけました。
とても嬉しいし励みになります。ありがとうございます。
前に投稿した記事をご参照ください:
logback-access-spring-boot-starter を Kotlin で書き直した
Java 17 以上が必須となったため、 Java 8 と 11 のサポートは廃止しました。
Immutable な @ConfigurationProperties
クラスにおいて、
@ConstructorBinding
の付与が不要になったので削除しました。
META-INF/spring.factories
にクラス名を記載しておくと
そのモジュールを使う時に自動的に @Configuration
を走らせてくれる、
というライブラリ向けの機能がありました。
このファイルのパスとフォーマットが変わったので、新しい形に変更しました。
META-INF/spring.factories
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
Servlet API のバージョンが v4 → v6 に更新され、 Java EE は Jakarta EE になりました。
これに伴い、 javax.*
パッケージの import
を jakarta.*
に変更しました。
バージョンが一気に 2 つ上がってますが、
v5 は Jakarta EE 移管によるパッケージの変更のみのようです。
v6 は機能が増えてるようですが影響ありませんでした。
注意点として、 Spring Boot Starter の Jetty 11 では Servlet API 6.0 に対応していません。
(Tomcat 10, Undertow 2 では対応しています。)
そのため、 spring-boot-starter-jetty
を使う場合は、
jakarta.servlet-api
のバージョンを 5.0.0
に落とす必要がありました。
📝 背景:
本ライブラリは Tomcat, Jetty, Undertow をサポートしており、
どの Web サーバが使われているか判別して実装を切り替えてます。
Logback のバージョンが v1.2 → v1.4 に更新され、 Joran の作りが大きく変わりました。
これに伴い、新しい Joran のインターフェイスに合わせ実装を変更しました。
📝 Joran とは:
Logback の設定ファイルパース部分のフレームワークを Joran と呼ぶようです。
📝 背景:
Spring Boot 本体の Logback ロギングの内部実装では、
Joran に踏み込んで <springProfile>
, <springProperty>
タグを拡張していました。
それを参考に、本ライブラリでも Joran に踏み込んで同タグをサポートしています。
Logback のバージョンアップで、 SequenceNumberGenerator という機能が増えました。
ロギングイベントにシーケンス番号を付与できるようです。
イベントインターフェイス (IAccessEvent
) にこの関数が増えているため、実装を追加しました。
Deprecated となった関数があったので置き換えました。
本ライブラリでは次の関数が対象でした。
org.springframework.http.ResponseEntity#getStatusCodeValue()
org.springframework.util.SerializationUtils#deserialize(byte[])
自分の場合は、
とすることが多いです。
(ローカル開発時まで JSON 出力するのは読みにくいため)
application(-{環境名}).yml
に Spring Property app.log.appender
を定義します (下表 B) app.log.appender
によって出力先/フォーマットを切り替えます (下表 C) APP_LOG_APPENDER
でも柔軟に切り替え可能です環境 | Spring Profile (A) | Spring Property (B) | 出力先 (C) | フォーマット (C) |
---|---|---|---|---|
ローカル開発時 | なし | console-text | 標準出力 | テキスト |
本番実行時 | prod | console-json | 標準出力 | JSON |
JSON 出力する場合は logstash-logback-encoder を使うのが楽なので、依存関係に追加します。
<!-- pom.xml -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.3</version>
</dependency>
環境ごとの設定ファイル application(-{環境名}).yml
をクラスパスルートに作成します。
# application.yml (デフォルト, ローカル開発時用)
app.log.appender: console-text
# application-prod.yml (本番実行時用)
app.log.appender: console-json
Logback の設定ファイル logback-spring.xml
をクラスパスルートに作成します。
Spring Boot が自動的に読み込んでくれるので、ここで Logback 設定をカスタマイズできます。
Spring Property の取得には、 Spring Boot 提供の Logback 拡張 <springProperty>
タグが便利です。
<!-- logback-spring.xml -->
<configuration>
<!-- Spring Boot が提供しているデフォルト設定を読み込み -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- Spring Property (application.yml, 環境変数等で指定) から設定値を取得 -->
<springProperty name="APP_LOG_APPENDER" source="app.log.appender" defaultValue="console-text"/>
<!-- 標準出力向けテキスト出力 (Spring Boot が提供しているデフォルト設定から name だけ変えてます) -->
<!--include resource="org/springframework/boot/logging/logback/console-appender.xml" /-->
<appender name="console-text" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>${CONSOLE_LOG_CHARSET}</charset>
</encoder>
</appender>
<!-- 標準出力向け JSON 出力 -->
<appender name="console-json" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<!-- Spring Property によって Appender を切り替えて出力 -->
<root level="INFO">
<appender-ref ref="${APP_LOG_APPENDER}"/>
</root>
</configuration>
上記のサンプルコード全体はこちらに置いてます。
akkinoc/try-spring-boot-log-by-env - GitHub
Spring Profile の指定なしで実行すると、テキストフォーマットで出力されます。
$ mvn spring-boot:run
... (中略)
2023-03-26T14:21:28.360+09:00 INFO 77469 --- [ main] sample.App : Running App!
Spring Profile に prod
を指定して実行すると、 JSON フォーマットで出力されます。
$ SPRING_PROFILES_ACTIVE=prod mvn spring-boot:run
... (中略)
{"@timestamp":"2023-03-26T14:21:48.269814+09:00","@version":"1","message":"Running App!","logger_name":"sample.App","thread_name":"main","level":"INFO","level_value":20000}
環境変数を指定して柔軟に切り替えることも可能です (一時的に上書き変更したい場合等)。
$ APP_LOG_APPENDER=console-json mvn spring-boot:run
... (中略)
{"@timestamp":"2023-03-26T14:22:20.356888+09:00","@version":"1","message":"Running App!","logger_name":"sample.App","thread_name":"main","level":"INFO","level_value":20000}
application.yml
, logback-spring.xml
, どちらでも設定できます。
# application.yml
logging.level.your.package=debug
logging.level.root=warn
<!-- logback-spring.xml -->
<logger name="your.package" level="DEBUG"/>
<root level="WARN">...</root>
片方に集約されていれば、どちらで設定しても良いと思います。
個人的には application.yml
の方が、環境別に基本の値を定義できるので好きです。
どちらでも、環境変数 LOGGING_LEVEL_ROOT
, LOGGING_LEVEL_YOUR_PACKAGE
等で
一時的な上書き変更も可能です。
Spring Property logging.pattern.console
が用意されてます。
指定可能なパターンは Logback Manual: PatternLayout が参考になります。
# application.yml
logging.pattern.console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%t] [%c{30}] %m - %C.%M \\(%F:%L\\)%n%ex"
$ mvn spring-boot:run
... (中略)
2023-03-26 15:23:28.421 INFO [main] [sample.App] Running App! - sample.App.run (App.java:19)
logstash-logback-encoder: Usage に詳細に記載されています。
JSON を整形して読みやすくしたい場合は jsonGeneratorDecorator
が使えます。
<!-- logback-spring.xml -->
<appender name="console-json" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<jsonGeneratorDecorator class="net.logstash.logback.decorate.PrettyPrintingJsonGeneratorDecorator"/>
</encoder>
</appender>
$ APP_LOG_APPENDER=console-json mvn spring-boot:run
... (中略)
{
"@timestamp" : "2023-03-26T16:04:04.654775+09:00",
"@version" : "1",
"message" : "Running App!",
"logger_name" : "sample.App",
"thread_name" : "main",
"level" : "INFO",
"level_value" : 20000
}
もし Logstash を無視したオリジナルのフォーマットにしたい場合は
LoggingEventCompositeJsonEncoder
が使えます。
<!-- logback-spring.xml -->
<appender name="console-json" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<pattern>
<pattern>
{
"timestamp": "%d{yyyy-MM-dd'T'HH:mm:ss.SSSZZ}",
"level": "%p",
"thread": "%t",
"logger": "%c",
"message": "%m",
"class": "%C",
"method": "%M",
"file": "%F",
"line": "%L",
"exception": "%ex"
}
</pattern>
<omitEmptyFields>true</omitEmptyFields>
</pattern>
</providers>
<jsonGeneratorDecorator class="net.logstash.logback.decorate.PrettyPrintingJsonGeneratorDecorator"/>
</encoder>
</appender>
$ APP_LOG_APPENDER=console-json mvn spring-boot:run
... (中略)
{
"timestamp" : "2023-03-26T16:05:04.684+0900",
"level" : "INFO",
"thread" : "main",
"logger" : "sample.App",
"message" : "Running App!",
"class" : "sample.App",
"method" : "run",
"file" : "App.java",
"line" : "19"
}
今回も先日の記事に書いた方法を使う。
AWS API を GAS (Google Apps Script) から直接呼び出す
AWS Batch のジョブ定義, ジョブキュー, コンピューティング環境は既にある前提。
GAS から AWS API を呼び出すための IAM ユーザを作成し、アクセスキーを発行する。
ポリシーはこんな感じで batch:SubmitJob
だけ許可すれば OK。
アクセスキーは GAS にベタ書きしちゃうので、対象リソースをしっかり制限しとく。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "batch:SubmitJob",
"Resource": [
"arn:aws:batch:ap-northeast-1:178282380061:job-definition/{ジョブ定義名}",
"arn:aws:batch:ap-northeast-1:178282380061:job-queue/{ジョブキュー名}"
]
}
]
}
発行したアクセスキーで AWS.init
して、次のように AWS.request
すれば OK。
function DynamoDBPutItem() {
const res = AWS.request(
'batch',
'ap-northeast-1',
'SubmitJob',
{},
'POST',
{
jobName: '{ジョブ名}',
jobDefinition: '{ジョブ定義名}',
jobQueue: '{ジョブキュー名}',
parameters: {
'{パラメータ名}': '{パラメータ値}',
},
},
{ 'Content-Type': 'application/json' },
'/v1/submitjob',
)
const code = res.getResponseCode()
const text = res.getContentText()
if (code < 200 || code >= 300) throw Error(`AWS.request failed: ${code} - ${text}`)
Logger.log(`OK: ${table} - ${JSON.stringify(item)}`)
}
結論、 Google スプレッドシートに入力したテキストから、
Amazon Comprehend でキーフレーズを抽出する仕組みを作った。
自然言語処理は正直やったことなかったので、
真っ先に浮かんだのは形態素解析。
形態素解析と言えば mecab。
と言うことで試しにやってみた。
入力データは、僕の過去ツイートで試してみた。
$ cat mecab-in.txt
Server-Side Kotlin Lounge #2「JavaからKotlinへの移行を考える」に参加を申し込みました!
AWS Batch 動く場所をEC2→Fargateに切り替え試してみてるけど、起動ちょっと早くなっていい感じ。
CloudFormationテンプレートを書くたび、YAMLアンカー/エイリアス機能くださいって思ってる。
$ mecab <mecab-in.txt
Server 名詞,固有名詞,組織,*,*,*,*
- 名詞,サ変接続,*,*,*,*,*
Side 名詞,一般,*,*,*,*,*
Kotlin 名詞,一般,*,*,*,*,*
Lounge 名詞,一般,*,*,*,*,*
# 名詞,サ変接続,*,*,*,*,*
2 名詞,数,*,*,*,*,*
「 記号,括弧開,*,*,*,*,「,「,「
Java 名詞,固有名詞,組織,*,*,*,*
から 助詞,格助詞,一般,*,*,*,から,カラ,カラ
Kotlin 名詞,固有名詞,組織,*,*,*,*
へ 助詞,格助詞,一般,*,*,*,へ,ヘ,エ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
移行 名詞,サ変接続,*,*,*,*,移行,イコウ,イコー
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
考える 動詞,自立,*,*,一段,基本形,考える,カンガエル,カンガエル
」 記号,括弧閉,*,*,*,*,」,」,」
に 助詞,格助詞,一般,*,*,*,に,ニ,ニ
参加 名詞,サ変接続,*,*,*,*,参加,サンカ,サンカ
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
申し込み 動詞,自立,*,*,五段・マ行,連用形,申し込む,モウシコミ,モーシコミ
まし 助動詞,*,*,*,特殊・マス,連用形,ます,マシ,マシ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
! 記号,一般,*,*,*,*,!,!,!
EOS
AWS 名詞,固有名詞,組織,*,*,*,*
Batch 名詞,一般,*,*,*,*,*
動く 動詞,自立,*,*,五段・カ行イ音便,基本形,動く,ウゴク,ウゴク
場所 名詞,一般,*,*,*,*,場所,バショ,バショ
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
EC 名詞,一般,*,*,*,*,*
2 名詞,数,*,*,*,*,*
→ 記号,一般,*,*,*,*,→,→,→
Fargate 名詞,固有名詞,組織,*,*,*,*
に 助詞,格助詞,一般,*,*,*,に,ニ,ニ
切り替え 名詞,一般,*,*,*,*,切り替え,キリカエ,キリカエ
試し 動詞,自立,*,*,五段・サ行,連用形,試す,タメシ,タメシ
て 助詞,接続助詞,*,*,*,*,て,テ,テ
み 動詞,非自立,*,*,一段,連用形,みる,ミ,ミ
てる 動詞,非自立,*,*,一段,基本形,てる,テル,テル
けど 助詞,接続助詞,*,*,*,*,けど,ケド,ケド
、 記号,読点,*,*,*,*,、,、,、
起動 名詞,サ変接続,*,*,*,*,起動,キドウ,キドー
ちょっと 副詞,助詞類接続,*,*,*,*,ちょっと,チョット,チョット
早く 形容詞,自立,*,*,形容詞・アウオ段,連用テ接続,早い,ハヤク,ハヤク
なっ 動詞,自立,*,*,五段・ラ行,連用タ接続,なる,ナッ,ナッ
て 助詞,接続助詞,*,*,*,*,て,テ,テ
いい 形容詞,自立,*,*,形容詞・イイ,基本形,いい,イイ,イイ
感じ 名詞,一般,*,*,*,*,感じ,カンジ,カンジ
。 記号,句点,*,*,*,*,。,。,。
EOS
CloudFormation 名詞,固有名詞,組織,*,*,*,*
テンプレート 名詞,一般,*,*,*,*,テンプレート,テンプレート,テンプレート
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
書く 動詞,自立,*,*,五段・カ行イ音便,基本形,書く,カク,カク
たび 名詞,非自立,副詞可能,*,*,*,たび,タビ,タビ
、 記号,読点,*,*,*,*,、,、,、
YAML 名詞,固有名詞,組織,*,*,*,*
アンカー 名詞,一般,*,*,*,*,アンカー,アンカー,アンカー
/ 名詞,サ変接続,*,*,*,*,*
エイリアス 名詞,一般,*,*,*,*,*
機能 名詞,サ変接続,*,*,*,*,機能,キノウ,キノー
ください 動詞,非自立,*,*,五段・ラ行特殊,命令i,くださる,クダサイ,クダサイ
って 助詞,格助詞,連語,*,*,*,って,ッテ,ッテ
思っ 動詞,自立,*,*,五段・ワ行促音便,連用タ接続,思う,オモッ,オモッ
てる 動詞,非自立,*,*,一段,基本形,てる,テル,テル
。 記号,句点,*,*,*,*,。,。,。
EOS
とても細かく分解してくれた。
もしこれを使うなら、非エンジニアが使う UI を用意して、
裏で mecab を叩いて、固有名詞/一般名詞など必要そうな
単語だけ抽出したらいいだろうか。
そいえば AWS にも自然言語処理のサービスがあったな、
と思い出して Amazon Comprehend というサービスに行きつく。
マネコンに入るとリアルタイムに分析できた。便利。
キーフレーズ検出なので mecab よりは荒いけど、いい感じ。
今回の要件にはこちらの方が合ってそうだったので、こちらを採用。
UI 開発, サーバサイド開発を省きたかったので、
テキストは Google スプレッドシートに入力してもらい、
GAS (Google Apps Script) で Amazon Comprehend を呼ぶことにした。
GAS から AWS API を直接呼び出す方法は前回の記事に書いた。
AWS API を GAS (Google Apps Script) から直接呼び出す
コードはこんな感じ (AWS.init
は済んでる前提)。
ついでに DetectEntities (エンティティ検出) も書いておく。
function detectKeyPhrases(lang, text) {
var req = {
service: "comprehend",
region: "ap-northeast-1",
action: "Comprehend_20171127.DetectKeyPhrases",
method: "POST",
params: {},
headers: { "Content-Type": "application/x-amz-json-1.1" },
payload: { LanguageCode: lang, Text: text },
}
var res = AWS.request(req.service, req.region, req.action, req.params, req.method, req.payload, req.headers)
res = { code: res.getResponseCode(), headers: res.getHeaders(), payload: JSON.parse(res.getContentText()) }
if (res.code < 200 || res.code >= 300)
throw new Error("Amazon Comprehend DetectKeyPhrases failed: " + JSON.stringify(res))
return res.payload
}
function detectEntities(lang, text) {
var req = {
service: "comprehend",
region: "ap-northeast-1",
action: "Comprehend_20171127.DetectEntities",
method: "POST",
params: {},
headers: { "Content-Type": "application/x-amz-json-1.1" },
payload: { LanguageCode: lang, Text: text },
}
var res = AWS.request(req.service, req.region, req.action, req.params, req.method, req.payload, req.headers)
res = { code: res.getResponseCode(), headers: res.getHeaders(), payload: JSON.parse(res.getContentText()) }
if (res.code < 200 || res.code >= 300)
throw new Error("Amazon Comprehend DetectEntities failed: " + JSON.stringify(res))
return res.payload
}
あとは以下を実装して、いい感じに
メニューからキーフレーズ分解を実行できるようにした。
SpreadsheetApp.getUi().createMenu()
で以下を実行するメニューを追加。detectKeyPhrases
に入力テキストを渡して呼ぶ。自然言語処理、初めて使ってみたけども (だいぶ雑にしか触ってないけど)。
なかなか面白かったー。
そんななか、割と簡単に AWS API を GAS (Google Apps Script) から
直接呼び出す方法を見つけました。
僕は最近、簡単な業務効率化ツールや、
UI 開発を省略したプロトタイプ版ツールなど、
Google スプレッドシートを入力データとして、
AWS と連携するツールを開発することが多いです。
簡単なツールやプロトタイプ版ツールの開発なので、
UI だけでなくサーバサイドの開発工数も極力省きたい。
そうすると、データ入力された Google スプレッドシートの
GAS から AWS API を直接呼び出したいケースが出てきました。
しかし、 AWS SDK は GAS 向けには提供されていませんし、
AWS SDK for JavaScript も実行環境が Node.js, Web ブラウザとは
異なるため使えません。
AWS API のリクエスト発行を自前で実装するにしても、
認証周り (AWS API リクエストの署名) がとても面倒そうです。
この記事は、これを解決した内容になります。
こちらを使わせてもらいました。面倒な認証周りをやってくれます。
(2019 年にはあったのですね。もっと早く見つけたかった・・・。)
smithy545/aws-apps-scripts - GitHub
使い方はこんな感じ。シンプル。
AWS.init(...)
を呼び出して初期設定。AWS.request(...)
で AWS API リクエストを発行。function myFunction() { AWS.init("MY_ACCESS_KEY", "MY_SECRET_KEY"); var instanceXML = AWS.request('ec2', 'us-east-1', 'DescribeInstances', {"Version":"2015-10-01"}); ... }
API によっては、 AWS.request(...)
の引数に指定すべき値が
よく分からないことがありました。
そういう場合は、手元でデバッグフラグ (--debug
) 付きで AWS CLI を叩くと
生の HTTP リクエスト/レスポンスまで見れるので、そこから推測できました。
例えば DynamoDB PutItem の場合、
DynamoDB PutItem リファレンス も参照しつつ、
以下のように AWS.request(...)
の引数を特定できました。
$ aws dynamodb put-item --table-name my_table --item '{ "id": { "S": "my-item" } }' --debug
...
2022-05-15 13:56:02,141 - MainThread - botocore.endpoint - DEBUG - Making request for OperationModel(name=PutItem) with params: {'url_path': '/', 'query_string': '', 'method': 'POST', 'headers': {'X-Amz-Target': 'DynamoDB_20120810.PutItem', 'Content-Type': 'application/x-amz-json-1.0', 'User-Agent': 'aws-cli/2.5.4 Python/3.9.12 Darwin/21.4.0 source/x86_64 prompt/off command/dynamodb.put-item'}, 'body': b'{"TableName": "my_table", "Item": {"id": {"S": "my-item"}}}', 'url': 'https://dynamodb.ap-northeast-1.amazonaws.com/', 'context': {'client_region': 'ap-northeast-1', 'client_config': <botocore.config.Config object at 0x1085820d0>, 'has_streaming_input': False, 'auth_type': None}}
...
引数 | 引数の値 | 引数の値の特定方法 |
---|---|---|
1. サービス | dynamodb | ログ中の url のサブドメインを参照 |
2. リージョン | ap-northeast-1 | ログ中の url のサブドメインを参照 |
3. アクション | DynamoDB_20120810.PutItem | ログ中の headers の X-Amz-Target を参照 |
4. パラメータ | なし | ログ中の query_string を参照 (多分) |
5. メソッド | POST | ログ中の method を参照 |
6. ペイロード | { TableName: ..., Item: ... } | ログ中の body を参照 |
7. ヘッダ | { 'Content-Type': 'application/x-amz-json-1.0' } | ログ中の headers の Content-Type を参照 |
8. パス | なし (デフォルト: / ) | ログ中の url_path を参照 (多分) |
特に、 “3. アクション” は API のバージョン指定 (?) も含んでいるのか、
単純な API 名 (PutItem
) だけだと通らなかったので注意です。
また、 “7. ヘッダ” には上記 Content-Type
を指定しないと
HTTP 404 エラーになってしまったので、こちらも注意です。
AWS.request(...)
の返却値は URL Fetch Service の HTTPResponse 型 でした。
成功/失敗は getResponseCode()
(HTTP ステータスコード) で確認できました。
(HTTP 4xx, HTTP 5xx が発生しても例外はスローされません。)
EC2 インスタンス ID の一覧を出力する例です。
※ページングは考慮してません。件数が多いと一部しか出力されません。
function EC2DescribeInstances() {
const res = AWS.request(
'ec2',
'ap-northeast-1',
'DescribeInstances',
{ Version: '2016-11-15' },
)
const code = res.getResponseCode()
const text = res.getContentText()
if (code < 200 || code >= 300) throw Error(`AWS.request failed: ${code} - ${text}`)
const root = XmlService.parse(text).getRootElement()
const ns = root.getNamespace()
const reservations = root.getChild('reservationSet', ns).getChildren()
reservations.forEach(reservation => {
const instances = reservation.getChild('instancesSet', ns).getChildren()
instances.forEach(instance => {
const instanceId = instance.getChild('instanceId', ns)
Logger.log(`OK: ${instanceId.getText()}`)
})
})
}
S3 にオブジェクトをアップロードする例です。
function S3PutObject() {
const bucket = 'my-bucket'
const key = 'my-content.txt'
const content = 'My Content'
const res = AWS.request(
's3',
'ap-northeast-1',
'PutObject',
{},
'PUT',
content,
{ 'Content-Type': MimeType.PLAIN_TEXT },
`/${key}`,
{ Bucket: bucket },
)
const code = res.getResponseCode()
const text = res.getContentText()
if (code < 200 || code >= 300) throw Error(`AWS.request failed: ${code} - ${text}`)
Logger.log(`OK: ${bucket}/${key}`)
}
S3 からオブジェクトをダウンロードする例です。
function S3GetObject() {
const bucket = 'my-bucket'
const key = 'my-content.txt'
const res = AWS.request(
's3',
'ap-northeast-1',
'GetObject',
{},
'GET',
null,
{},
`/${key}`,
{ Bucket: bucket },
)
const code = res.getResponseCode()
const text = res.getContentText()
if (code < 200 || code >= 300) throw Error(`AWS.request failed: ${code} - ${text}`)
Logger.log(`OK: ${bucket}/${key}\n${text}`)
}
DynamoDB テーブルにアイテムを登録する例です。
function DynamoDBPutItem() {
const table = 'my_table'
const item = { id: { S: 'my-item' } }
const res = AWS.request(
'dynamodb',
'ap-northeast-1',
'DynamoDB_20120810.PutItem',
{},
'POST',
{ TableName: table, Item: item },
{ 'Content-Type': 'application/x-amz-json-1.0' },
)
const code = res.getResponseCode()
const text = res.getContentText()
if (code < 200 || code >= 300) throw Error(`AWS.request failed: ${code} - ${text}`)
Logger.log(`OK: ${table} - ${JSON.stringify(item)}`)
}
僕は試してないですが、こちらの記事が参考になりそうです。
(今回の AWS.request を見つけたキッカケになった記事です!)
Roche が Google スプレッドシートと Amazon Redshift Data API で データへのアクセスを民主化した方法 - Amazon Web Services ブログ
var resultJson = AWS.request( getTypeAWS_(), getLocationAWS_(), 'RedshiftData.ExecuteStatement', {"Version": getVersionAWS_()}, method='POST', payload={ "ClusterIdentifier": getClusterIdentifierReshift_(), "Database": getDataBaseRedshift_(), "DbUser": getDbUserRedshift_(), "Sql": sql }, headers={ "X-Amz-Target": "RedshiftData.ExecuteStatement", "Content-Type": "application/x-amz-json-1.1" } );
下記の手段もあったので参考にリンクしておきます。
ただどれも制限があるので、用途によって使い分けたいところです。
S3 にしか対応してないです。
インターフェイスがシンプルなので、 S3 だけ使う場合はこちらのが便利です。
AWS SDK for JavaScript を使えるのは便利そうです。
ただ、 HtmlService でサブウィンドウを表示/経由する必要があり、
全体的には少し煩雑になりそうだったため、僕は試していません。
アクセスログが溜まってもスキャンするデータ量を抑えるよう、パーティション分割もしました。
パーティション分割には、昨年追加された機能 “Partition Projection” を使ってみました。
環境を再現できるように、 CloudFormation のテンプレートも公開しています。
他社から大量のイベントデータを HTTP GET で受け取って、それを集計したい!
という要件が出てきたのが発端でした。
本当なら Kinesis Data Streams などリアルタイム処理も試してみたかったのですが、
とても納期が短かったので、経験のあった CloudFront と Athena で簡単に実現しました。
仕組みとデータフローは、こんな流れです。
CloudFront 接続元の正当性は、固定 IP アドレスで確認します。
今回はテスト用のアクセスも確認できるよう Athena 集計時に除外しましたが、
WAF 等で第三者はアクセス不可にするのもアリだと思います。
この記事ではイベントデータの受信と集計に応用しましたが、
シンプルに Web アクセスログの集計にも使える内容です。
アクセスログが溜まるとスキャンするデータ量の増大によって
集計時間や料金も増えてしまうので、日時でパーティション分割しました。
これまでだと ALTER TABLE ADD PARTITION
や MSCK REPAIR TABLE
で
事前にパーティションを追加する必要がありましたが、
“Partition Projection” という機能を使うと不要になりました。
ただ、 CloudFront が S3 に出力するパスそのままだと Partition Projection を適用できないため、
S3 ObjectCreated イベントをトリガーに Lambda でパスを移動するようにしました。
移動先パスの dt=YYYY-MM-DD-HH
の部分がパーティションキーになります。
<OPTIONAL-PREFIX>/<DISTRIBUTION-ID>.YYYY-MM-DD-HH.<UNIQUE-ID>.gz
<OPTIONAL-PREFIX>/dt=YYYY-MM-DD-HH/<DISTRIBUTION-ID>.YYYY-MM-DD-HH.<UNIQUE-ID>.gz
Lambda のコードは、 AWS 公式のサンプルを拝借し、移動先パスだけ調整しました。
year/month/day/hour 列に分ける形で良ければ、そのままでも良いと思います。
僕は列 1 つの方がクエリで範囲指定しやすかったので、文字列型の dt 列だけにしました。
- const targetKey = `${targetKeyPrefix}year=${year}/month=${month}/day=${day}/hour=${hour}/${filename}`;
+ const targetKey = `${targetKeyPrefix}dt=${year}-${month}-${day}-${hour}/${filename}`;
あとは、パーティションのキーとパラメータを与えてテーブル作成すれば、
Partition Projection を適用できました。
CREATE EXTERNAL TABLE IF NOT EXISTS cloudfront_accesslogs (
`date` DATE,
time STRING,
x_edge_location STRING,
sc_bytes BIGINT,
c_ip STRING,
cs_method STRING,
cs_host STRING,
cs_uri_stem STRING,
sc_status INT,
cs_referer STRING,
cs_user_agent STRING,
cs_uri_query STRING,
cs_cookie STRING,
x_edge_result_type STRING,
x_edge_request_id STRING,
x_host_header STRING,
cs_protocol STRING,
cs_bytes BIGINT,
time_taken FLOAT,
x_forwarded_for STRING,
ssl_protocol STRING,
ssl_cipher STRING,
x_edge_response_result_type STRING,
cs_protocol_version STRING,
fle_status STRING,
fle_encrypted_fields STRING,
c_port INT,
time_to_first_byte FLOAT,
x_edge_detailed_result_type STRING,
sc_content_type STRING,
sc_content_len BIGINT,
sc_range_start BIGINT,
sc_range_end BIGINT
)
PARTITIONED BY (
`dt` string -- 日時パーティションキー (Lambda で移動後のパスに含まれる値)
)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\t'
LOCATION 's3://<BUCKET-NAME>/<OPTIONAL-PREFIX>/' -- Lambda で移動後のパス
TBLPROPERTIES (
'skip.header.line.count' = '2',
'projection.enabled' = 'true', -- ここで Partition Projection を有効化
'projection.dt.type' = 'date', -- 以下は型, フォーマット, 範囲等の設定
'projection.dt.format' = 'yyyy-MM-dd-HH', --
'projection.dt.range' = '2021-10-31-15,NOW', --
'projection.dt.interval' = '1', --
'projection.dt.interval.unit' = 'hours' --
)
例えば、日付 (JST), 接続元 IP アドレス, メソッド, パス, クエリ, ステータスごとに、
直近 3 か月間のリクエスト数を集計するなら、こんな感じでいけました。
パスやクエリは URL エンコードされてるので、雑にデコードしてます。
SELECT
DATE(
FROM_ISO8601_TIMESTAMP(
CONCAT(TO_ISO8601(date), 'T', time, 'Z')
) AT TIME ZONE 'Asia/Tokyo'
) date,
c_ip client,
cs_method method,
URL_DECODE(URL_DECODE(cs_uri_stem)) path,
URL_DECODE(URL_DECODE(cs_uri_query)) query,
sc_status status,
COUNT(*) events
FROM
cloudfront_accesslogs
WHERE
dt >= FORMAT_DATETIME(CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - INTERVAL '3' MONTH, 'yyyy-MM-dd-HH')
GROUP BY
1, 2, 3, 4, 5, 6
ORDER BY
1, 2, 3, 4, 5, 6
上記環境を構築できる CloudFormation テンプレートも作成しました。
GitHub に置いてます。
こちらのコマンドで “store.yml” を構築すると、
$ aws cloudformation deploy \
--template-file store.yml \
--capabilities CAPABILITY_NAMED_IAM \
--stack-name cflogs-store \ # stack-name と、
--parameter-overrides Name=cflogs # Name パラメータを切り替えれば、複数構築できます
ざっくり次のリソースが出来上がります。
cflogs-store
cflogs-store-events
s3://cflogs-store/new-events/
にログ配置されたら起動s3://cflogs-store/events/dt=YYYY-MM-DD-HH/
にログを移動cflogs.events
s3://cflogs-store/events/dt=YYYY-MM-DD-HH/
を Partition Projection で反映あとは CloudFront 側で s3://cflogs-store/new-events/
に
アクセスログを出力するよう設定すれば完成です。
先日の記事:
Java フレームワーク Spring Boot の拡張ライブラリです。
Logback-access という Web アクセスのロギングライブラリがあるのですが、
このライブラリを Spring Boot に自動で組み込み、使いやすくします。
Logback-access の設定は、クラスパス上に “logback-access.xml” を配置すれば自動認識します。
akkinoc/logback-access-spring-boot-starter - GitHub
Kotlin で書き直しましたが、 Java からも使えます。
依存関係を追加するだけで、
<dependency>
<groupId>dev.akkinoc.spring.boot</groupId>
<artifactId>logback-access-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
Spring Boot Web アプリケーションへアクセスした時に、
こんなアクセスログ (Common Log Format) が標準出力に流れます。
0:0:0:0:0:0:0:1 - - [24/Oct/2021:15:32:03 +0900] "GET / HTTP/1.1" 200 319
0:0:0:0:0:0:0:1 - - [24/Oct/2021:15:32:03 +0900] "GET /favicon.ico HTTP/1.1" 404 111
0:0:0:0:0:0:0:1 - - [24/Oct/2021:15:32:04 +0900] "GET / HTTP/1.1" 304 0
出力先や出力フォーマットをカスタマイズしたい場合は、
クラスパス直下に “logback-access.xml” を配置/設定すれば OK です。
<configuration>
<!-- ex) 標準出力に Common Log Format, ファイルに Combined Log Format で出力 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>common</pattern>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.FileAppender">
<file>access.log</file>
<encoder>
<pattern>combined</pattern>
</encoder>
</appender>
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</configuration>
設定ファイルの書き方の詳細は Logback-access 公式ドキュメントを参照ください:
また、通常のロギングの Logback 設定 (“logback-spring.xml”) と同様に、
<springProfile>
, <springProperty>
タグも使えるように拡張してあります。
実行環境によって出力先や出力フォーマットを変えたい場合に便利だと思います :)
<springProfile name="staging"> <!-- configuration to be enabled when the "staging" profile is active --> </springProfile> <springProfile name="dev | staging"> <!-- configuration to be enabled when the "dev" or "staging" profiles are active --> </springProfile> <springProfile name="!production"> <!-- configuration to be enabled when the "production" profile is not active --> </springProfile>
<springProperty scope="context" name="fluentHost" source="myapp.fluentd.host" defaultValue="localhost"/> <appender name="FLUENT" class="ch.qos.logback.more.appenders.DataFluentAppender"> <remoteHost>${fluentHost}</remoteHost> ... </appender>
このライブラリには自分の中でいくつかコンセプトがあって、
そのために頑張って開発してる部分もあるので、ここで書き出しておきます。
クラスパス上の設定ファイル (“logback-access(-test)(-spring).xml”) を探して自動検知してます。
また、前記した通り <springProfile>
, <springProperty>
タグをサポートしています。
現状だと Tomcat, Jetty, Undertow をサポートしています。
生の Logback-access だと Tomcat, Jetty しかサポートされていませんが、
Spring Boot がサポートしている Web サーバなら、できるだけ多くサポートしたいと思ってます。
これをやるには、 Tomcat, Jetty, Undertow 等、
各 Web サーバに備わっているロギング機能がどう実装されているのか参考にするため、
それぞれの内部実装まで個別に理解する必要があるのが大変なところです。
Spring Boot で Web アプリケーション開発する場合、
Web MVC (Servlet Stack) の他に WebFlux (Reactive Stack) が選べます。
生の Logback-access だとサーブレットベースしかサポートされていませんが、
できるだけ多くの WebFlux での実装をサポートしたいと思ってます。
現状だと Tomcat, Jetty, Undertow ベースの WebFlux をサポートしています。
Tomcat, Jetty は Spring Boot が WebFlux な実装にラップしてるだけで、
内部的にはサーブレットベースなので楽に対応できました。
Undertow は内部的にはサーブレットベースではなく独自仕様で動いているため、
Logback-access を結構改造する必要があり大変でした (^^;
次の理由から、各 Web サーバのネイティブに近い部分まで潜り込んでロギングしています。
例えば Tomcat なら専用の Valve, Jetty なら専用の RequestLog を実装しています。
そのため、ここでも Web サーバごとの内部まで個別に理解して実装する必要があるので大変です。
色々な Web サーバや WebFlux に対応するなら、
サーブレットフィルタや WebFlux WebFilter でリクエスト/レスポンスを補足+ロギングして、
Web サーバの違いを一気に吸収できる形で実装した方が良かったかな?
とは今でも考えたりしてます。
(上記した理由を諦めることにはなりますが…)
次の理由から、全ての Web サーバに対して、 Web MVC 用, WebFlux 用の
全パターンを網羅してテストするようにしています。
Spring Boot はクラスパスに存在するクラスによって自動で Web サーバを選択/起動するため、
これを上手く切り替えてテストすることに苦労しました。
今はテスト開始時に Spring コンテキストのクラスローダをゴニョゴニョして、
テスト対象以外の Web サーバのクラスを見つけられないようにして切り替えています。
(例えば Tomcat でテストするなら、 Jetty, Undertow, Netty のクラスを隠してます。)
ロードバランサを使っている場合に、全てのアクセスログの接続元ホストが
ロードバランサの IP アドレスで記録されてしまう、ということがよくあると思います。
生の Logback-access on Tomcat でも、これは発生していました。
Spring Boot にはフォワードヘッダをサポートするプロパティ
(“server.forward-headers-strategy”) があるので、これと連動して、
リモートホスト等の一部の出力項目を書き換える、といったことをしてます。
Logback-access には、デバッグ用に TeeFilter という
リクエスト/レスポンスのコンテンツ部分までロギングする機能があります。
現状まだサーブレットベースで使った場合しか動きませんが、
これを簡単に組み込めるように、
プロパティ (“logback.access.tee-filter.*”) を用意しています。
このライブラリは Spring Boot x Logback-access を繋ぐものなので、
出力先や出力フォーマットをカスタマイズするような、
追加の Logback Appender はサポートしないようにしています。
過去に JSON で出力したい, SYSLOG に出力したい, という要望をいただきました。
こういった要件は、他のライブラリも組み合わせるか、
独自で Logback Appender を実装いただけたら、と思っています。
JSON 出力なら logstash-logback-encoder の “LogstashAccessEncoder” が便利そうです。
@Bean
Lite Mode (@Configuration(proxyBeanMethods = false)
) を使用次は Reactor Netty もサポートしたいところです。
(WebFlux の標準選択ですし、 Issue #53 で要望もいただいてますし。)
yaml-resource-bundle を Kotlin で書き直した
今回も Kotlin で全て書き直しました。
Java フレームワーク Spring Boot の拡張ライブラリです。
Orika (Java Bean マッピングライブラリ) を自動で DI コンテナに組み込み、使いやすくします。
Spring Boot アプリケーションプロパティや、ユーザ実装の設定クラスで、動作を設定できます。
akkinoc/orika-spring-boot-starter - GitHub
Kotlin で書き直しましたが、 Java からも使えます。
依存関係を追加して、
<dependency>
<groupId>dev.akkinoc.spring.boot</groupId>
<artifactId>orika-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
Orika の MapperFacade
を注入すれば、
import ma.glasnost.orika.MapperFacade;
@Autowired
private MapperFacade orikaMapperFacade;
MapperFacade
でマッピング処理を呼び出せます。
PersonSource src = new PersonSource("John", "Smith", 23);
System.out.println(src); // => "PersonSource(firstName=John, lastName=Smith, age=23)"
PersonDestination dest = orikaMapperFacade.map(src, PersonDestination.class);
System.out.println(dest); // => "PersonDestination(givenName=John, sirName=Smith, age=23)"
細かなマッピングの設定は、 OrikaMapperFactoryConfigurer
を継承して
@Component
で Spring コンテナに登録すれば OK です。
import dev.akkinoc.spring.boot.orika.OrikaMapperFactoryConfigurer;
import ma.glasnost.orika.MapperFactory;
@Component
public class PersonMapping implements OrikaMapperFactoryConfigurer {
@Override
public void configure(MapperFactory orikaMapperFactory) {
orikaMapperFactory.classMap(PersonSource.class, PersonDestination.class)
.field("firstName", "givenName")
.field("lastName", "sirName")
.byDefault()
.register();
}
}
@Bean
Lite Mode (@Configuration(proxyBeanMethods = false)
) を使ってみたアカウント登録とリポジトリ登録申請からやり直したのは、こんな理由です。
AdoptOpenJDK, GnuPG, Maven は、 macOS なら Homebrew で楽にインストールできます。
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
$ brew tap adoptopenjdk/openjdk
$ brew install --cask adoptopenjdk11
$ brew install gnupg
$ brew install maven
akkinoc/yaml-resource-bundle - GitHub
<dependency>
<groupId>dev.akkinoc.util</groupId>
<artifactId>yaml-resource-bundle</artifactId>
<version>${yaml-resource-bundle.version}</version>
</dependency>
Sonatype JIRA へサインアップして、アカウント登録します。
Sign up for Jira - Sonatype JIRA
ここで登録するアカウントは、 JIRA へのログインだけでなく、
Nexus Repository Manager へのログイン, Maven コマンドでのデプロイにも使います。
新規プロジェクトの Issue を作成して、リポジトリ登録申請します。
入力項目の Group Id はプロジェクトを一意に識別するための名前空間で、
自分が所有している Web ドメイン由来 (ドット区切りの逆順) である必要があります。
GitHub 等のユーザごとのサブドメイン (ex: “io.github.USERNAME”) も使えます。
僕の場合はこんな感じで Issue を書きました。
Group Id には、自分のドメイン “dev.akkinoc” を使っています。
*英語苦手なので英文は変かもしれません。ご容赦を…
OSSRH-71149 Create repository for dev.akkinoc - Sonatype JIRA
Group Id の所有者かチェックされるので、次のどれかで証明します。
僕の場合は TXT レコードで設定しました。
$ dig -t TXT akkinoc.dev
akkinoc.dev. 3600 IN TXT "OSSRH-71149"
10 分ほどでチェックが通り、準備できたとコメントがきました。速い!
TXT レコードはチェック通過後は削除して大丈夫です。
ここまでの申請は初回だけやれば大丈夫で、同じ Group Id 配下なら、
今後は別プロジェクトでも自由に公開できます。
(ex: “dev.akkinoc” で通ったなら “dev.akkinoc” と “dev.akkinoc.*” で公開できます。)
公開には GPG/PGP 署名が必要なので、 GnuPG でキーペアを用意します。
Git コミット署名等で既に持っているなら、それを使い回しても良いと思います。
僕の場合はこんな感じで作成しました。
途中で入力するパスフレーズは、今後の署名時に必要になるので覚えておきます。
$ gpg --full-gen-key
ご希望の鍵の種類を選択してください:
(1) RSA と RSA
(2) DSA と Elgamal
(3) DSA (署名のみ)
(4) RSA (署名のみ)
(9) ECC (署名と暗号化) *デフォルト
(10) ECC (署名のみ)
(14) カードに存在する鍵
あなたの選択は? 1
RSA 鍵は 1024 から 4096 ビットの長さで可能です。
鍵長は? (3072) 4096
鍵の有効期限を指定してください。
0 = 鍵は無期限
<n> = 鍵は n 日間で期限切れ
<n>w = 鍵は n 週間で期限切れ
<n>m = 鍵は n か月間で期限切れ
<n>y = 鍵は n 年間で期限切れ
鍵の有効期間は? (0) 10y
本名: Akihiro Kondo
電子メール・アドレス: akkinoc@gmail.com
コメント:
他の人が署名を検証できるように、作成したキーペアの公開鍵を
Maven Central Repository 推奨のキーサーバへ配布します。
- keyserver.ubuntu.com
- keys.openpgp.org
- pgp.mit.edu
各キーサーバは同期されるらしいのですが、
タイムラグがありそうだったので僕は全てに送りました。
$ gpg -k --keyid-format long
pub rsa4096/C1C97CE293FB5803 2021-04-12 [SC] [有効期限: 2031-04-10]
8CF0151763C741B898013592C1C97CE293FB5803
uid [ 究極 ] Akihiro Kondo <akkinoc@gmail.com>
sub rsa4096/532C94339BB63D3A 2021-04-12 [E] [有効期限: 2031-04-10]
$ gpg --keyserver keyserver.ubuntu.com --send-keys C1C97CE293FB5803
$ gpg --keyserver keys.openpgp.org --send-keys C1C97CE293FB5803
$ gpg --keyserver pgp.mit.edu --send-keys C1C97CE293FB5803
公開に必要なメタデータを pom.xml
ファイルに設定します。
今回のプロジェクトならこんな感じです。
<groupId>
には登録申請した Group Id (またはその配下) を設定します。
<groupId>dev.akkinoc.util</groupId>
<artifactId>yaml-resource-bundle</artifactId>
<version>2.0.0</version>
<name>yaml-resource-bundle</name>
<description>Java ResourceBundle for YAML format.</description>
<url>https://github.com/akkinoc/yaml-resource-bundle</url>
<scm>
<url>https://github.com/akkinoc/yaml-resource-bundle</url>
<connection>scm:git:git@github.com:akkinoc/yaml-resource-bundle.git</connection>
<developerConnection>scm:git:git@github.com:akkinoc/yaml-resource-bundle.git</developerConnection>
</scm>
<developers>
<developer>
<id>akkinoc</id>
<name>Akihiro Kondo</name>
<email>akkinoc@gmail.com</email>
<url>https://akkinoc.dev</url>
</developer>
</developers>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
公開にはソース JAR ファイルの添付が必要なので、
作成するよう pom.xml
ファイルに設定します。
<build>
<plugins>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>jar</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
公開には Javadoc JAR ファイルの添付が必要なので、
作成するよう pom.xml
ファイルに設定します。
Java なら標準のプラグインを使います。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>jar</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Kotlin なら Dokka で作成できます。
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.dokka</groupId>
<artifactId>dokka-maven-plugin</artifactId>
<version>1.5.0</version>
<executions>
<execution>
<id>javadocJar</id>
<phase>package</phase>
<goals>
<goal>javadocJar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
公開する各ファイルを GPG 署名するよう、 pom.xml
ファイルに設定します。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>sign</id>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
pom.xml
ファイルにデプロイ先を設定します。
<distributionManagement>
<repository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2</url>
</repository>
<snapshotRepository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
</distributionManagement>
pom.xml
ファイルにデプロイするためのプラグインを仕込みます。
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.8</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://s01.oss.sonatype.org</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
</plugins>
</build>
デフォルトの maven-deploy-plugin も使えるのですが、
それだと毎回デプロイ後に Nexus Repository Manager にログインして
Staging Repositories から Close → Release する必要があります。
nexus-staging-maven-plugin なら、そこまで全部やってくれるので楽です。
デプロイ先である OSSRH の認証情報を設定します。
~/.m2/settings.xml
ファイルに Sonatype JIRA アカウントの情報を記載すれば OK です。
<settings>
<servers>
<server>
<id>ossrh</id>
<username>USERNAME</username>
<password>PASSWORD</password>
</server>
</servers>
</settings>
ここまで準備すると、やっとデプロイできます。
Maven コマンドで deploy フェーズまで実行しましょう。
途中 GPG 署名のためパスフレーズを訊かれるので、適宜入力してやります。
mvn clean deploy
JIRA Issue のコメントに従い、初回リリースしたことを Issue コメントで報告します。
数時間ほど待って、次のサイトへ反映されれば公開完了です。
pom.xml
の <version>
を書き換えます。
エディタで書き換えても良いですし、 versions プラグインを使っても良いと思います。
$ mvn versions:set
あとは初回同様、 Maven コマンドを実行して数時間ほど待てば OK です。
mvn clean deploy
PC の乗り換え時など、 GPG キーペアは次のコマンドでエクスポート/インポートできます。
$ gpg -K --keyid-format long # ID 確認
$ gpg --export-secret-keys -a ID >key # key ファイルにエクスポート
$ gpg --import <key # key ファイルをインポート
$ echo "FINGERPRINT:6:" | gpg --import-ownertrust # key を信頼
GitHub ドメインを Group Id とする場合、
“com.github.USERNAME” での新規登録は現在は不可なようです。
“io.github.USERNAME” を使いましょう。
2021-04-01 - com.github.* is not supported anymore as a valid coordinate
GitHub Actions や CircleCI 等, クラウドの CI 環境には、できれば秘密鍵は置きたくありません。
しかし GPG キーペアがない場合、 maven-gpg-plugin が失敗してしまいます。
僕は pom.xml
にリリース用のプロファイルを用意し、
普段は GPG 署名をスキップすることで回避しています。
<properties>
<gpg.skip>true</gpg.skip>
</properties>
<profiles>
<profile>
<id>release</id>
<properties>
<gpg.skip>false</gpg.skip>
</properties>
</profile>
</profiles>
$ mvn clean deploy # CI 環境を含む通常時 (GPG 署名なし)
$ mvn clean deploy -Prelease # ローカル環境でのリリース時 (GPG 署名あり)
~/.m2/settings.xml
ファイルに記載する Sonatype JIRA アカウント情報は、
Nexus Repository Manager の Profile → User Token から発行できるトークンが使えます。
ログインパスワードを使わずにデプロイできるため、チョット意識高くできます。
今回、自分は Sonatype ユーザ名も変更しました。
過去の Issue を参考にしたところ、こんな手順が採られていました。
僕の場合はこんな Issue を立てて対応いただきました。