S3 にアップロードされた画像ファイルから Lambda でサムネイル画像を生成してみる
概要
記事一覧はこちらです。
S3 にアップロードした画像ファイルから Lambda でサムネイル画像を生成してみます。
- アップロードする画像ファイルのフォーマットは JPEG とする。
- サムネイル画像のフォーマットも JPEG とする。サイズは幅320 x 高さ180 とする。
- 画像ファイルをアップロードする S3 Bucket は ksbysample-upload-bucket、サムネイル画像を置く S3 Bucket は ksbysample-resize-bucket とする。
- サムネイル画像のファイル名は <オリジナルの画像ファイル名>_thumb.jpg とする。
よく聞くパターンなので簡単だと思っていましたが、結構苦労しました。。。
参照したサイト・書籍
AWS Lambdaで画像ファイル加工~環境構築から実行確認まで~ 手順紹介
https://business.ntt-east.co.jp/content/cloudsolution/column-try-17.htmlLambda trigger on existing s3 bucket
https://forum.serverless.com/t/lambda-trigger-on-existing-s3-bucket/6056Using existing buckets
https://www.serverless.com/framework/docs/providers/aws/events/s3#using-existing-bucketsHow many records can be in S3 put() event lambda trigger?
https://stackoverflow.com/questions/40765699/how-many-records-can-be-in-s3-put-event-lambda-triggerSample Amazon S3 function code
https://docs.aws.amazon.com/lambda/latest/dg/with-s3-example-deployment-pkg.htmlPythonで文字列を置換(replace, translate, re.sub, re.subn)
https://note.nkmk.me/python-str-replace-translate-re-sub/Pythonでパス文字列からファイル名・フォルダ名・拡張子を取得、結合
https://note.nkmk.me/python-os-basename-dirname-split-splitext/AWS Lambdaで運用した実績から得られた、serverless frameworkのオススメ設定とプラグインの知見
https://tech.ga-tech.co.jp/entry/2018/12/12/120000Serverless Python Requirements
https://www.serverless.com/plugins/serverless-python-requirements/Why can't Python import Image from PIL?
https://stackoverflow.com/questions/26505958/why-cant-python-import-image-from-pil/31728305AWS LambdaでPython Pillowを使うための手順
https://qiita.com/ryasuna/items/9051cfdc0576134bb46cAWS Lambda で Pillow を使おうとしたらハマった
https://michimani.net/post/aws-use-pillow-in-lambda/amazonlinux
https://hub.docker.com/_/amazonlinuxlambci / docker-lambda
https://github.com/lambci/docker-lambda- しかも Lambda 実行用の Docker Image もあるとは!
AWS Lambda runtimes
https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.htmlWebP変換 & 画像キャッシュサービスをサーバレスで構築する - Feed re:Architect vol.1 -
https://buildersbox.corp-sansan.com/entry/2019/06/06/124752AWS LambdaでTensorFlow 2.0を使った画像分類
https://tech.unifa-e.com/entry/2019/09/17/085400- 今回の記事とは関係ありませんが、いろいろ調べている時に見かけて良い参考になりそうだったのでメモしておきます。
目次
- resize-image-app-project プロジェクトを作成する
- Serverless Framework で resize-service サブプロジェクトを作成する
- S3 Bucket を作成してイベント発生時に Lambda が呼び出されるよう serverless.yml を記述する
- S3 Bucket にファイルをアップロードした時の event の内容を確認する
- サムネイル画像を生成するよう handler.py の resize 関数を実装する
- serverless-python-requirements プラグインをインストールする
- deploy する
- 画像をアップロードしてサムネイル画像を生成してみる
- 最後に
手順
resize-image-app-project プロジェクトを作成する
以下の手順で share-s3bucket-with-multi-servces プロジェクトを作成します。具体的な手順は IntelliJ IDEA+Node.js+npm+serverless framework+Python の組み合わせで開発環境を構築して AWS Lambda を作成してみる 参照。
- resize-image-app-project の Empty Project を作成する。
- Python の仮想環境を作成する。
- Serverless Framework をローカルインストールする。
- .envrc を作成する。
Serverless Framework で resize-service サブプロジェクトを作成する
プロジェクトのルートディレクトリの下で npx sls create --template aws-python3 --path resize-service
を実行して resize-service サブプロジェクトを作成します。
自動生成された serverless.yml に対し、以下の設定を追加します。
- リソースが東京リージョン(ap-northeast-1)に生成されるようにする。
S3 Bucket を作成してイベント発生時に Lambda が呼び出されるよう serverless.yml を記述する
2つの S3 Bucket ksbysample-upload-bucket
、ksbysample-resize-bucket
を作成して、ksbysample-upload-bucket
にファイルがアップロードされた時に Lambda が呼び出されるよう serverless.yml に以下の記述を追加します。handler.py の関数名も resize に変更します。
service: resize-service provider: name: aws runtime: python3.8 stage: dev region: ap-northeast-1 iamRoleStatements: - Effect: "Allow" Action: - "s3:GetObject" Resource: - "arn:aws:s3:::ksbysample-upload-bucket/*" - Effect: "Allow" Action: - "s3:PutObject" Resource: - "arn:aws:s3:::ksbysample-resize-bucket/*" functions: resize: handler: handler.resize events: # Using existing buckets # https://www.serverless.com/framework/docs/providers/aws/events/s3#using-existing-buckets - s3: ksbysample-upload-bucket resources: Resources: KsbysampleResizeBucket: Type: AWS::S3::Bucket Properties: BucketName: ksbysample-resize-bucket
- events の下に
- s3: ksbysample-upload-bucket
を記述すると S3 Bucket が生成されます。resources に S3 Bucket の記述をする必要はありません。(新規作成するリソースは resources に記述するものと思い resources に S3 Bucket の記述をしてから- s3: ksbysample-upload-bucket
も書いて試しに deploy してみたら CREATE_FAILED が出てしばらく悩みました。。。) - もしイベント発生元の S3 Bucket を terraform 等で別に作成する場合には Using existing buckets を参考に設定すれば良さそうです。
ksbysample-resize-bucket
はイベント発生元ではないので resources の下に記述します。- この2つを書いただけでは S3 Bucket の GetObject、PutObject が出来ないので iamRoleStatements に必要な権限を記述します。
S3 Bucket にファイルをアップロードした時の event の内容を確認する
event がログに出力されるよう handler.py を以下のように変更してから、
import logging logger = logging.getLogger() logger.setLevel(logging.INFO) def resize(event, context): logger.info(event)
deploy して S3 Bucket に sample.jpg をアップロードしてみると以下の json がログに出力されました(名前に principal が含まれているものと IP アドレスはマスクしています)。
{ 'Records': [ { 'eventVersion': '2.1', 'eventSource': 'aws:s3', 'awsRegion': 'ap-northeast-1', 'eventTime': '2020-06-10T00:45:25.995Z', 'eventName': 'ObjectCreated:Put', 'userIdentity': { 'principalId': 'AWS:XXXXXXXXXXXXXXXXXXXXX' }, 'requestParameters': { 'sourceIPAddress': 'xxx.xxx.xxx.xxx' }, 'responseElements': { 'x-amz-request-id': '11F4BEB2A6E4D924', 'x-amz-id-2': 'k2RJ6yeU8sk5tRp7ntfyM8QDmR58rF1TIdacJimdExhLOOSQ3N6pOtIAWXyk6pGjTeBGOBx995ELqaK53zhdAo6Ky/PZb08e' }, 's3': { 's3SchemaVersion': '1.0', 'configurationId': '66e09001-2a37-4900-9a98-06da126cc896', 'bucket': { 'name': 'ksbysample-upload-bucket', 'ownerIdentity': { 'principalId': 'XXXXXXXXXXXXXX' }, 'arn': 'arn:aws:s3:::ksbysample-upload-bucket' }, 'object': { 'key': 'sample.jpg', 'size': 669286, 'eTag': 'febe0aaf4d817457776b6be293973442', 'sequencer': '005EE02D28714EE2DC' } } } ] }
Records が配列になっているので複数ファイルをアップロードしたら複数ここに入ってくるのかな?と思い、今度は sample.jpg、sample2.jpg の2つのファイルをアップロードしてみると、1 ファイルずつ event が発生しています。
stackoverflow に How many records can be in S3 put() event lambda trigger? があり、これを読むと 1 ファイル 1 event のようです。
サムネイル画像を生成するよう handler.py の resize 関数を実装する
Sample Amazon S3 function code の Python 3 のサンプルを参考にして実装します。
import logging import os import re import uuid from urllib.parse import unquote_plus import boto3 from PIL import Image thumbnail_size = 320, 180 logger = logging.getLogger() logger.setLevel(logging.INFO) s3_client = boto3.client('s3') def resize_image(image_path, resized_path): with Image.open(image_path) as image: image.thumbnail(thumbnail_size) image.save(resized_path) def resize(event, context): for record in event['Records']: bucket = record['s3']['bucket']['name'] key = unquote_plus(record['s3']['object']['key']) # ディレクトリの場合には何もしない if key.endswith('/'): return tmpkey = key.replace('/', '') download_path = '/tmp/{}{}'.format(uuid.uuid4(), tmpkey) resized_path = '/tmp/resized-{}'.format(tmpkey) filename = key.split('/')[-1] dirname = re.sub(filename + '$', '', key) basename, ext = os.path.splitext(filename) resized_key = '{}{}_thumb{}'.format(dirname, basename, ext) s3_client.download_file(bucket, key, download_path) resize_image(download_path, resized_path) s3_client.upload_file(resized_path, "ksbysample-resize-bucket", resized_key) logger.info('サムネイルを生成しました({})'.format(resized_key))
IDEA の Terminal を起動して boto3、Pillow をインストールします。IDEA の赤波下線を消したり補完を効かせるために入れているもので、deploy する時のパッケージは serverless-python-requirements プラグインを利用して収集します。
pip install boto3
pip install Pillow
serverless-python-requirements プラグインをインストールする
外部ライブラリを利用しているので serverless-python-requirements プラグインをインストールして自動でパッケージしてくれるようにします。
特に今回使用している Pillow は OS 依存のバイナリがあるそうなので、作業している Windows にインストールしたものではなく Docker を利用して Amazon Linux 2 上でライブラリのファイルをインストールする必要があります。AWS Lambda が動作している OS は AWS Lambda runtimes で確認できます。
serverless-python-requirements プラグインのサイトではインストールコマンドは sls plugin install -n serverless-python-requirements
と書いてありますが、今回作成しているプロジェクトでは package.json と serverless.xml の位置が異なるので npm install --save-dev serverless-python-requirements
でインストールして serverless.yml には自分で設定を追加することにします。
npm install --save-dev serverless-python-requirements
でインストールしてから、
serverless.yml を以下のように変更します。
service: resize-service plugins: - serverless-python-requirements custom: pythonRequirements: dockerizePip: true provider: name: aws runtime: python3.8 ..........
- plugins を追加して serverless-python-requirements を記述します。
- custom を追加して pythonRequirements を記述します。Docker を利用して deploy する外部ライブラリを収集するので
dockerizePip: true
を設定します。
IDEA の Terminal で pip freeze > requirements.txt
コマンドを実行して resize-service ディレクトリの下に requirements.txt を作成します。
requirements.txt には以下のように出力されましたが、
boto3==1.13.26 botocore==1.16.26 docutils==0.15.2 jmespath==0.10.0 Pillow==7.1.2 python-dateutil==2.8.1 s3transfer==0.3.3 six==1.15.0 urllib3==1.25.9
Pillow 以外は不要なので削除します。
Pillow==7.1.2
deploy する
最初に serverless-python-requirements プラグインが利用する Docker コンテナが C:\Users\<ユーザ名>\AppData\Local\UnitedIncome\
の下のファイルを参照するので、アクセスできるよう Docker の設定に追加します。追加しなくても delploy 時に追加するか聞かれるのですが、ランダム文字列を含むパスが追加されるので上位のディレクトリを追加しておくことにします。
npx sls deploy -v
コマンドを実行して deploy します。
最初に lambci/lambda:build-python3.8
の Docker Image をダウンロードして requirements.txt を参照して外部ライブラリをインストールしているのが分かります。
deploy が完了すると AWS マネジメントコンソールから ksbysample-upload-bucket
、ksbysample-resize-bucket
の2つの S3 Bucket が作成されていることが確認できました。
画像をアップロードしてサムネイル画像を生成してみる
ksbysample-upload-bucket に sample.jpg をアップロードすると、
ksbysample-resize-bucket に sample_thumb.jpg が生成されていました!
S3 Bucket 内の画像ファイルを削除してから npx sls remove -v
で delploy したリソース一式を削除します。
最後に
Serverless Framework と serverless-python-requirements プラグインがあったから何とか動かすことが出来た感が強いです。serverless-python-requirements プラグインなしで Python で Lambda 書くなんて考えられないんじゃないかな、と正直思いました。
履歴
2020/06/10
初版発行。
別途作成しておいた IAM Role、S3 Bucket を複数の Serverless Framework のプロジェクトから利用できるのか?
概要
記事一覧はこちらです。
Servlerless Framework で作成したプロジェクトで deploy すると python-first-lambda-dev-ap-northeast-1-lambdaRole という IAM Role と python-first-lambda-dev-serverlessdeploymentbucke-7j3614vgkvv3 という S3 Bucket が作成されました。
デフォルトではプロジェクト毎に IAM Role、S3 Bucket が作成されるようですが、別途作成しておいた IAM Roke、S3 Bucket を複数のプロジェクトから利用することができるのか確認してみます。
参照したサイト・書籍
Serverless Framework: Reusing S3 bucket for multiple projects deploy
https://medium.com/@oieduardorabelo/reusing-s3-bucket-for-multiple-serverless-framework-projects-deploy-828e3a45f713Custom IAM Roles
https://www.serverless.com/framework/docs/providers/aws/guide/iam/#custom-iam-roles
目次
- delploy した時の IAM Role はどのような設定か?
- delploy した時の S3 Bucket には何がアップロードされているのか?
- share-s3bucket-with-multi-servces プロジェクトを作成する
- Terraform で ksbysample-serverless-lambdaRole、ksbysample-serverless-deploymentbucket を作成する
- Serverless Framework のサブプロジェクトを2つ作成する
- deploy してみる
- remove してみる
手順
delploy した時の IAM Role はどのような設定か?
python-first-lambda-dev-ap-northeast-1-lambdaRole には python-first-lambda-dev-lambda という名前のインラインポリシーがアタッチされていて、以下の内容でした。
{ "Version": "2012-10-17", "Statement": [ { "Action": [ "logs:CreateLogStream", "logs:CreateLogGroup" ], "Resource": [ "arn:aws:logs:ap-northeast-1:446693287859:log-group:/aws/lambda/python-first-lambda-dev*:*" ], "Effect": "Allow" }, { "Action": [ "logs:PutLogEvents" ], "Resource": [ "arn:aws:logs:ap-northeast-1:446693287859:log-group:/aws/lambda/python-first-lambda-dev*:*:*" ], "Effect": "Allow" } ] }
また信頼関係で以下のポリシーが設定されています。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }
delploy した時の S3 Bucket には何がアップロードされているのか?
serverless/python-first-lambda/dev/1590287550668-2020-05-24T02:32:30.668Z
の下に以下のファイルがアップロードされています。
プロジェクト名がパスの途中に入っているので、複数のプロジェクトで利用しても問題は出ないと思われます。
share-s3bucket-with-multi-servces プロジェクトを作成する
以下の手順で share-s3bucket-with-multi-servces プロジェクトを作成します。具体的な手順は IntelliJ IDEA+Node.js+npm+serverless framework+Python の組み合わせで開発環境を構築して AWS Lambda を作成してみる 参照。
- share-s3bucket-with-multi-servces の Empty Project を作成する。
- Python の仮想環境を作成する。
- Serverless Framework をローカルインストールする。
- .envrc を作成する。
Terraform で ksbysample-serverless-lambdaRole、ksbysample-serverless-deploymentbucket を作成する
Serverless Framework でも IAM Role、S3 Bucket を作成できるようなのですが、Terraform を使うことにします(個人的な好みで深い理由はありません)。
Terraform の実行環境は tfenv+aws-vault+direnv を組み合わせて Windows 上に Terraform の実行環境を構築する の手順で構築しています。
terraform は最新の 0.12.25 を使用します。
プロジェクトのルートディレクトリの下に .terraform-version を作成し、その中に 0.12.25
と記述します。
プロジェクトのルートディレクトリの下に terraform/shared-resources ディレクトリを作成し、その下に main.cf を作成して以下の内容を記述します。
- IAM Role 名は
ksbysample-serverless-lambdaRole
。 - IAM Role の Policy で指定する CloudWatch ロググループは
/aws/lambda/*
で指定します。 - S3 Bucket 名は
ksbysample-serverless-deploymentbucket
。
provider "aws" { region = "ap-northeast-1" } /////////////////////////////////////////////////////////////////////////////// // IAM Role // data "aws_iam_policy_document" "assume_role_lambda" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } } } resource "aws_iam_role" "lambda_role" { name = "ksbysample-serverless-lambdaRole" assume_role_policy = data.aws_iam_policy_document.assume_role_lambda.json } data "aws_iam_policy_document" "lambda_policy" { statement { effect = "Allow" actions = [ "logs:CreateLogStream", "logs:CreateLogGroup", "logs:PutLogEvents" ] resources = ["arn:aws:logs:ap-northeast-1:*:log-group:/aws/lambda/*:*"] } } resource "aws_iam_role_policy" "lambda_policy" { name = "lambda_policy" role = aws_iam_role.lambda_role.id policy = data.aws_iam_policy_document.lambda_policy.json } /////////////////////////////////////////////////////////////////////////////// // S3 Bucket // resource "aws_s3_bucket" "serverless_deployment_bucket" { bucket = "ksbysample-serverless-deploymentbucket" force_destroy = true } resource "aws_s3_bucket_public_access_block" "serverless_deployment_bucket" { bucket = aws_s3_bucket.serverless_deployment_bucket.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true }
以下のコマンドを実行して IAM Role、S3 Bucket を作成します。
tf init
tf plan
(ここのキャプチャは省略)tf apply
IAM Role は serverless.yml に指定する時に ARN が必要になるので画面で確認します。
また .gitignore に以下の設定を追加します。
.......... # Ignore terraform .terraform/ terraform.tfstate* terraform.tfvars
Serverless Framework のサブプロジェクトを2つ作成する
プロジェクトのルートディレクトリの下に services ディレクトリを作成し、その下に service-a、service-b サブプロジェクトを作成します。
まずは service-a から。npx sls create --template aws-python3 --path service-a
を実行します。
services/service-a/serverless.yml を以下のように変更します(コメントの部分は取り除いています)。
service: service-a provider: name: aws runtime: python3.8 stage: dev region: ap-northeast-1 role: arn:aws:iam::446693287859:role/ksbysample-serverless-lambdaRole deploymentBucket: name: "ksbysample-serverless-deploymentbucket" functions: hello: handler: handler.hello
- IAM Role は provider - role で指定します。
- S3 Bucket は provider - deploymentBucket - name で指定します。
次に service-b です。npx sls create --template aws-python3 --path service-b
を実行します。
services/service-b/serverless.yml を以下のように変更します(コメントの部分は取り除いています)。
service: service-b provider: name: aws runtime: python3.8 stage: dev region: ap-northeast-1 role: arn:aws:iam::446693287859:role/ksbysample-serverless-lambdaRole deploymentBucket: name: "ksbysample-serverless-deploymentbucket" functions: hello: handler: handler.hello
deploy してみる
まずは service-a から deploy します。service-a のディレクトリに移動してから aws-vault exec $AWS_PROFILE -- bash -c "npx sls deploy -v"
を実行します。
以前は表示されていた以下のメッセージは表示されなくなりました。
CREATE_IN_PROGRESS - AWS::S3::Bucket
CREATE_IN_PROGRESS - AWS::IAM::Role
AWS マネジメントコンソールで確認すると service-a-dev-hello が作成されており実行ロールに ksbysample-serverless-lambdaRole がセットされています。
次に service-b を deploy します。service-b のディレクトリに移動してから aws-vault exec $AWS_PROFILE -- bash -c "npx sls deploy -v"
を実行します。
こちらも AWS::S3::Bucket と AWS::IAM::Role の CREATE_IN_PROGRESS は表示されません。
AWS マネジメントコンソールで確認すると service-b-dev-hello が作成されており実行ロールに ksbysample-serverless-lambdaRole がセットされています。
ksbysample-serverless-deploymentbucket 見ると service-a、service-b の compiled-cloudformation-template.json、zip ファイルがアップロードされていました。
service-a、service-b それぞれで aws-vault exec $AWS_PROFILE -- bash -c "npx sls invoke -f hello"
を実行すると結果が返ってきます。
プロジェクト毎に IAM Role、S3 Bucket は作成されず Terraform で作成したものが利用されていました。
remove してみる
service-a、service-b で aws-vault exec $AWS_PROFILE -- bash -c "npx sls remove -v"
を実行します。
どちらも IAM Role、S3 Bucket の DELETE_IN_PROGRESS は表示されませんでした。
AWS マネジメントコンソールで確認すると ksbysample-serverless-lambdaRole、ksbysample-serverless-deploymentbucket のどちらも残っています。
作成した IAM Role と S3 Bucket は tf destroy
を実行して削除します。
履歴
2020/05/26
初版発行。
IntelliJ IDEA+Node.js+npm+serverless framework+Python の組み合わせで開発環境を構築して AWS Lambda を作成してみる
概要
記事一覧はこちらです。
Serverless Framework を触ってみたいと思ったので IntelliJ IDEA で開発環境を構築して AWS Lambda を1つ作成してみます。言語は Python 3.8 にします。
作成したプロジェクトは https://github.com/ksby/ksbysample-serverless レポジトリに入れます。また D:\project-serverless\ksbysample-serverless に clone します。
IntelliJ IDEA は PyCharm ではなく Ultimate Edition(2020.1.1)に Python Plugin をインストールして使用します。
Python は Python Release Python 3.8.3 から Windows x86-64 executable installer のリンクをクリックして python-3.8.3-amd64.exe をダウンロードした後、実行して D:\Python38 にインストールします。環境変数に
D:\Python38
、D:\Python38\Scripts
を追加します。Node.js、npm は Spring Boot + npm + Geb で入力フォームを作ってテストする ( その85 )( Node.js を 10.15.3 → 12.16.3 へ、npm を 6.9.0 → 6.14.5 へバージョンアップする ) でバージョンアップした 12.16.3、6.14.5 を使用します。
Get started with Serverless Framework Open Source & AWS を見ると Serverless Framework を
npm install -g serverless
でグローバルインストールするよう記載されていますが、ローカルインストールでも動作するようなのでローカルインストールします。Serverless Framework の sls コマンドを実行する時に tfenv+aws-vault+direnv を組み合わせて Windows 上に Terraform の実行環境を構築する でインストールした aws-vault、direnv を利用します。
参照したサイト・書籍
Serverless Framework
https://www.serverless.com/Python Release Python 3.8.3
https://www.python.org/downloads/release/python-383/PyCharmでVenvをGitで共有するときにすること
https://hiropon-progra.com/?p=82Windows 上の Python で UTF-8 をデフォルトにする
https://qiita.com/methane/items/9a19ddf615089b071e71Hello World Python Example
https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/python/Serverless Frameworkの使い方まとめ
https://qiita.com/horike37/items/b295a91908fcfd4033a2
目次
- idea-serverless-python-first プロジェクトを作成する
- Python の仮想環境を作成する
- Serverless Framework をローカルインストールする
- .envrc を作成する
- python-first-lambda サブプロジェクトを作成する
- Lambda が東京リージョン(ap-northeast-1)に作成されるよう serverless.yml を編集する
- deploy する
- 作成した Lambda を実行する
- AWS に作成されたリソースを確認する
- 作成した Lambda を削除する
- 削除した後に再度 deploy するとバージョンが上がる
- 最後に
手順
idea-serverless-python-first プロジェクトを作成する
IntelliJ IDEA で Empty Project を作成します。
「New Project」ダイアログを表示してから画面左側で Empty Project を選択して「Next」ボタンをクリックします。
「Project Name」に idea-serverless-python-first
、「Project location」に D:\project-serverless\ksbysample-serverless\idea-serverless-python-first
を入力して「Finish」ボタンをクリックします。
Project が開くと「Project Structure」ダイアログが開きますが、今は何もせずに「Cancel」ボタンをクリックして閉じます。
ダイアログを閉じた直後は Project Tool Window に .idea ディレクトリが表示されていなかったので、一旦 IntelliJ IDEA を閉じて idea-serverless-python-first プロジェクトを開き直します。そうすると Project Tool Window が以下の表示になります。
D:\project-serverless\ksbysample-serverless
の直下に .gitignore を新規作成し、以下の内容を記述します。
# Intellij project files *.iml *.ipr *.iws .idea/ out/ # Ignore direnv .envrc # Ignore node.js, npm node_modules/
Python の仮想環境を作成する
IntelliJ IDEA のメインメニューから「File」-「Project Structure...」を選択して「Project Structure」ダイアログを表示します。
画面中央上部の「+」ボタンをクリックしてから「Add Python SDK...」を選択します。
「Add Python Interpreter」ダイアログが表示されます。表示直後が下記の状態だったので、そのまま「OK」ボタンをクリックします。
「Create Virtual Environment」ダイアログが表示されて仮想環境が構築されます(少し時間がかかります)。
構築が完了すると中央のリストに「Python 3.8 (idea-serverless-python-first)」が追加されます。
画面左側で「Project Settings」-「Project」を選択した後、画面右側の「Project SDK」で作成した Python の SDK と「SDK default」を選択してから「OK」ボタンをクリックします。
Project Tool Window に venv ディレクトリが表示されています。
IntelliJ IDEA の Terminal を起動すると venv 環境で動くようになります。
venv ディレクトリは git に入れないので .gitignore に設定を追加します。
.......... # Ignore Python venv venv/
Serverless Framework をローカルインストールする
コマンドラインから以下のコマンドを実行します。
cd /d d:\project-serverless\ksbysample-serverless\idea-serverless-python-first
npm init -y
npm install --save-dev serverless
npx sls -v
.envrc を作成する
プロジェクトのルート直下に .envrc を新規作成し、以下の内容を記述します。
export AWS_PROFILE=<aws-vault exec 実行時に渡すプロファイル名> # Windows 上の Python で UTF-8 をデフォルトにする # https://qiita.com/methane/items/9a19ddf615089b071e71 export PYTHONUTF8=1
python-first-lambda サブプロジェクトを作成する
Hello World Python Example を参考に python-first-lambda サブプロジェクトを作成します。以下のコマンドを実行します。--template
オプションに渡す文字列は aws-python ではなく aws-python3 にします。
npx sls create --template aws-python3 --path python-first-lambda
Project Tool Window を見ると python-first-lambda ディレクトリが作成されて、その下に .gitignore, handler.py, serverless.yml の3つのファイルが作成されます。
作成したばかりの serverless.yml は以下の内容です(コメントの部分は取り除いています)。
service: python-first-lambda provider: name: aws runtime: python3.8 functions: hello: handler: handler.hello
Lambda が東京リージョン(ap-northeast-1)に作成されるよう serverless.yml を編集する
デフォルトの serverless.yml では米国東部(バージニア北部)リージョン(us-east-1 )に作成されてしまうので、serverless.yml に設定を追加して東京リージョン(ap-northeast-1)が作成先になるようにします。
また Serverless Framework で作成する Lambda の名前には stage 名が入るのですが、serverless.yml に明記するようにします(書かなかった時も stage 名は dev なのですが明記することにします)。
serverless.yml を以下のように変更します。
service: python-first-lambda provider: name: aws runtime: python3.8 stage: dev region: ap-northeast-1 functions: hello: handler: handler.hello
- provider に以下の2行を追加します。
stage: dev
region: ap-northeast-1
deploy する
git-cmd.exe(direnv+aws-vault を利用するので cmd.exe ではなく git-cmd.exe 環境で実行する、D:\git\git-cmd.exe --command=usr/bin/bash.exe -l -i
)で python-first-lambda ディレクトリに移動してから deploy コマンドを実行します。deploy コマンドについては AWS - deploy 参照。
aws-vault exec $AWS_PROFILE -- bash -c "npx sls deploy -v"
deploy すると python-first-lambda サブプロジェクト内に .serverless ディレクトリが作成されます。アップロードする zip ファイルと create, update 用の CloudFormation の JSON ファイル、serverless-state.json というファイルが作成されています。
作成した Lambda を実行する
invoke コマンドを実行すると AWS 上の Lambda を実行して結果を取得することができます。invoke コマンドについては AWS - Invoke 参照。
aws-vault exec $AWS_PROFILE -- bash -c "npx sls invoke -f hello"
--log
オプションを指定すればログを取得することもできます。
aws-vault exec $AWS_PROFILE -- bash -c "npx sls invoke -f hello --log"
logs コマンドでログだけ取得することも可能です。log コマンドは AWS - Logs 参照。
aws-vault exec $AWS_PROFILE -- bash -c "npx sls logs -f hello"
AWS に作成されたリソースを確認する
マネジメントコンソールから作成されたリソースを確認すると以下の5つが作成されています。
- AWS Lambda
- IAM Role
- python-first-lambda-dev-ap-northeast-1-lambdaRole
- CloudWatch ロググループ
- S3 バケット
- python-first-lambda-dev-serverlessdeploymentbucke-7j3614vgkvv3
- CloudFormation スタック
- python-first-lambda-dev
作成した Lambda を削除する
remove コマンドを実行して作成したリソース一式を削除します。invoke コマンドについては AWS - Remove 参照。
aws-vault exec $AWS_PROFILE -- bash -c "npx sls remove -v"
コマンド実行後、上に書いたリソースは全て削除されていました。
削除した後に再度 deploy するとバージョンが上がる
削除した後に再度 delploy するとバージョンが 2 に上がります。
何もソースを変更せずに再度 delploy するとバージョンは 2 のままです。
handler.py を少し変更して delploy するとバージョン 3 が追加されます。
remove してから .serverless ディレクトリを削除して deploy するとバージョンは 1 に戻らず 4 に上がりました。.serverless ディレクトリの下のファイルを見てみましたが、最新のバージョン番号が書かれているファイルはないですね。どこに最新バージョンを保持しているのでしょうか?
最後に
やってみると Serverless Framework は使いやすそうです。個人的には Serverless Step Functions が便利と聞いたので、もう少し基本的なところを調べたら触ってみるつもりです。
履歴
2020/05/24
初版発行。
徒然なるままに serverless( 大目次 )
GitHub は https://github.com/ksby/ksbysample-serverless
- IntelliJ IDEA+Node.js+npm+serverless framework+Python の組み合わせで開発環境を構築して AWS Lambda を作成してみる
- 別途作成しておいた IAM Role、S3 Bucket を複数の Serverless Framework のプロジェクトから利用できるのか?
- S3 にアップロードされた画像ファイルから Lambda でサムネイル画像を生成してみる
- resize-image-app-project プロジェクトで作成した AWS Lambda のユニットテストを作成する(local動作版)
- resize-image-app-project プロジェクトで作成した AWS Lambda のユニットテストを Docker コンテナ上で動作させる
- 外部パッケージの Pillow と独自モジュール(.py ファイル)を Lambda Layer に配置する(前編)
- 外部パッケージの Pillow と独自モジュール(.py ファイル)を Lambda Layer に配置する(後編)
- API Gateway で受信したメッセージを SNS 経由で Slack へ通知する
- serverless-domain-manager プラグインを利用して独自ドメインで API Gateway にアクセスする
- API Gateway で受信するデータを JSON Schema Validation でチェックしてから SQS へ送信する
- aws-lambda-powertools を試してみる(Logger 編)
- aws-lambda-powertools を試してみる(Tracer&X-Ray 編その1)
- aws-lambda-powertools を試してみる(Tracer&X-Ray 編その2)
- boto3 のインスタンス生成をグローバルで行っても moto を利用したユニットテストを成功させるには?
- Serverless Framework で deploy 用ディレクトリへ移動→環境変数を設定する方法で deploy する環境を切り替える(その1)
- Serverless Framework で deploy 用ディレクトリへ移動→環境変数を設定する方法で deploy する環境を切り替える(その2)
- Serverless Framework で deploy 用ディレクトリへ移動→環境変数を設定する方法で deploy する環境を切り替える(その3、CircleCI に deploy する)
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その91 )( Doma 2 を 2.28.0 → 2.34.0 へバージョンアップする+domaGen タスクを doma-codegen-plugin を利用したものに作り直す )
概要
記事一覧はこちらです。
終わったと思いましたが、domaGen タスクが動作しなかったので Doma 2 を最新バージョンまで上げていませんでした。最新バージョンに上げて domaGen タスクが動くようにします。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
Building an application - Build with Gradle
https://doma.readthedocs.io/en/latest/build/?highlight=org.seasar.doma%3Adoma#build-with-gradledomaframework / doma-codegen-plugin
https://github.com/domaframework/doma-codegen-plugin
目次
手順
Doma 2 を 2.28.0 → 2.34.0 へバージョンアップする
https://doma.readthedocs.io/en/latest/build/?highlight=org.seasar.doma%3Adoma#build-with-gradle に従い build.gradle の以下の点を変更します。この時点では Doma-Gen に関する設定は変更しません(build 時にエラーにはならないため)
dependencies { .......... def domaVersion = "2.34.0" .......... // for Doma implementation("org.seasar.doma:doma-core:${domaVersion}") annotationProcessor("org.seasar.doma:doma-processor:${domaVersion}") domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}") domaGenRuntime("com.h2database:h2:1.4.200") .......... }
def domaVersion = "2.28.0"
→def domaVersion = "2.34.0"
に変更します。implementation("org.seasar.doma:doma:${domaVersion}")
→implementation("org.seasar.doma:doma-core:${domaVersion}")
に変更します。annotationProcessor("org.seasar.doma:doma:${domaVersion}")
→annotationProcessor("org.seasar.doma:doma-processor:${domaVersion}")
に変更します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
clean タスク実行 → Rebuild Project 実行 → build タスク実行をすると BUILD SUCCESSFUL が表示されます。
gebTest タスクも特に問題なく BUILD SUCCESSFUL が表示されました。
ただし domaGen タスクを実行すると Could not resolve all files for configuration ':domaGenRuntime'.
のエラーメッセージが表示されて実行できません。
以前 Doma-Gen メンテナンス終了のアナウンスが出ていましたが、2.29.0 からなくなったようです。
DomaのDBからコード生成する機能をGradleプラグインに移行しました。これまでのAntベースのDoma-Genはメンテナンス終了予定です。https://t.co/xpg4o7OY79
— toshihiro nakamura (@nakamura_to) April 4, 2020
domaGen タスクを doma-codegen-plugin を利用したものに変更する
doma-codegen-plugin がリリースされていますので、それを利用した方法に変更します。build.gradle を以下のように変更します。
buildscript { .......... dependencies { // for doma-codegen-plugin classpath "com.h2database:h2:1.4.200" } } plugins { .......... id "org.seasar.doma.codegen" version "0.0.2" } .......... configurations { developmentOnly runtimeClasspath { extendsFrom developmentOnly } // annotationProcessor と testAnnotationProcessor、compileOnly と testCompileOnly を併記不要にする testAnnotationProcessor.extendsFrom annotationProcessor testImplementation.extendsFrom compileOnly // for SpotBugs spotbugsStylesheets { transitive = false } } .......... dependencies { .......... // for Doma implementation("org.seasar.doma:doma-core:${domaVersion}") annotationProcessor("org.seasar.doma:doma-processor:${domaVersion}") .......... } .......... // for doma-codegen-plugin // まず変更が必要なもの def rootPackageName = "ksbysample.webapp.bootnpmgeb" def rootPackagePath = "src/main/java/ksbysample/webapp/bootnpmgeb" def dbUrl = "jdbc:h2:tcp://localhost:9092/mem:bootnpmgebdb" def dbUser = "sa" def dbPassword = "" def dbTableNamePattern = ".*" // おそらく変更不要なもの def entityPackagePath = rootPackagePath + "/entity" def daoPackagePath = rootPackagePath + "/dao" def importOfComponentAndAutowiredDomaConfig = "${rootPackageName}.util.doma.ComponentAndAutowiredDomaConfig" def workDirPath = "work" def workEntityDirPath = "${workDirPath}/entity" def workDaoDirPath = "${workDirPath}/dao" task domaGen(group: "doma code generation") { // このタスク自体は何もしない。実行する時の起点用タスクとして作成している。 } task beforeDomaCodeGen { doLast { // 作業用ディレクトリを削除する delete "${workDirPath}" // 現在の Dao インターフェースのバックアップを取得する copy() { from "${daoPackagePath}" into "${workDaoDirPath}/org" } } } domaCodeGen { db { url = "${dbUrl}" user = "${dbUser}" password = "${dbPassword}" tableNamePattern = "${dbTableNamePattern}" ignoredTableNamePattern = "flyway_schema_history|SPRING_SESSION.*" entity { packageName = "${rootPackageName}.entity" useListener = false } dao { overwrite = true packageName = "${rootPackageName}.dao" } } } task afterDomaCodeGen { doLast { // 生成された Entity クラスを作業用ディレクトリにコピーし、 // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加する copy() { from "${entityPackagePath}" into "${workEntityDirPath}/replace" filter { line -> line.replaceAll('@Entity', '@SuppressWarnings({"PMD.TooManyFields"})\n@Entity') } } // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加した Entity クラスを // entity パッケージへ戻す copy() { from "${workEntityDirPath}/replace" into "${entityPackagePath}" } // 生成された Dao インターフェースを作業用ディレクトリにコピーし、 // @ComponentAndAutowiredDomaConfig アノテーションを付加し、 // Javadoc の @param に説明文を追加する copy() { from "${daoPackagePath}" into "${workDaoDirPath}/replace" filter { line -> line.replaceAll('import org.seasar.doma.Dao;', "import ${importOfComponentAndAutowiredDomaConfig};\nimport org.seasar.doma.Dao;") .replaceAll('@Dao', '@Dao\n@ComponentAndAutowiredDomaConfig') .replaceAll('@param (\\S+)$', '@param $1 $1') } } // @ComponentAndAutowiredDomaConfig アノテーションを付加した Dao インターフェースを // dao パッケージへ戻す copy() { from "${workDaoDirPath}/replace" into "${daoPackagePath}" } // 元々 dao パッケージ内にあったファイルを元に戻す copy() { from "${workDaoDirPath}/org" into "${daoPackagePath}" } // 作業用ディレクトリを削除する delete "${workDirPath}" } } // 内部で domaCodeGenDbDao.dependsOn domaCodeGenDbEntity になっている domaCodeGenDbEntity.dependsOn beforeDomaCodeGen domaGen.dependsOn domaCodeGenDbDao domaGen.finalizedBy afterDomaCodeGen
- buildscript block に
dependencies { classpath "com.h2database:h2:1.4.200" }
を追加します。 - plugins block に
id "org.seasar.doma.codegen" version "0.0.2"
を追加します。 - configurations block から
domaGenRuntime
を削除します。 - dependencies block から以下の行を削除します。
domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}")
domaGenRuntime("com.h2database:h2:1.4.200")
task domaGen { ... }
は以下のように変更します。- 変数は全て domaGen タスクの外に出します。
- domaGen タスクは何も実装しません(中の処理を全て削除します)。また
(group: "doma code generation")
を記述して domaGen タスクが doma code generation グループに表示されるようにします。 task beforeDomaCodeGen { ... }
を追加します。domaCodeGen { ... }
を追加します。task afterDomaCodeGen { ... }
を追加します。- beforeDomaCodeGen → domaCodeGenDbEntity → domaCodeGenDbDao → domaGen(何も処理しない)→ afterDomaCodeGen の順で処理されるよう dependsOn、finalizedBy を設定します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新すると doma code generation グループが表示されて、その下に domaCodeGenDb~ で始まる各種タスクと domaGen タスクが表示されます。
Tomcat を起動してから domaGen タスクを実行すると BUILD SUCCESSFUL が表示され entity クラス、dao インターフェースが生成されます。
履歴
2020/05/15
初版発行。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その90 )( Checkstyle を 8.19 → 8.32 へ、SpotBugs を 1.6.9 → 4.0.2 へ、PMD を 6.13.0 → 6.23.0 へ、error-prone を 2.3.3 → 2.3.4 へバージョンアップする )
概要
記事一覧はこちらです。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その89 )( Spring Boot を 2.1.4 → 2.2.7 へバージョンアップする ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- Checkstyle を 8.19 → 8.32 へ、SpotBugs を 1.6.9 → 4.0.2 へ、PMD を 6.13.0 → 6.23.0 へ、error-prone を 2.3.3 → 2.3.4 へバージョンアップします。
参照したサイト・書籍
- spotbugs / spotbugs-gradle-plugin
https://github.com/spotbugs/spotbugs-gradle-plugin
目次
- Checkstyle を 8.19 → 8.32 へバージョンアップする
- testJUnit4AndSpock と test タスクで実行されるテスト数が同じ問題を解消する
- SpotBugs を 1.6.9 → 4.0.2 へバージョンアップする
- PMD を 6.13.0 → 6.23.0 へバージョンアップする
- error-prone を 2.3.3 → 2.3.4 へバージョンアップする
手順
Checkstyle を 8.19 → 8.32 へバージョンアップする
build.gradle の以下の点を変更します。
checkstyle { configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml") toolVersion = "8.32" sourceSets = [project.sourceSets.main] }
toolVersion = "8.19"
→toolVersion = "8.32"
に変更します。
最新版の google_checks.xml から差分を反映します。
clean タスク実行 → Rebuild Project 実行 → build タスク実行すると BUILD SUCCESSFUL が表示されました。。。が、testJUnit4AndSpock と test タスクで実行されるテスト数がどちらも 147 tests になっていることに気づきました。。。
testJUnit4AndSpock と test タスクで実行されるテスト数が同じ問題を解消する
Spring Boot 2.2 から spring-boot-starter-test が JUnit 5 に切り替わったのに junit-vintage-engine を除くのを忘れていました。
build.gradle の以下の点を変更します。
dependencies { .......... testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude group: "org.junit.vintage", module: "junit-vintage-engine" } .......... } .......... test { // test タスクの jvmArgs は tasks.withType(Test) { ... } で定義している // for JUnit 5 useJUnitPlatform() testLogging { afterSuite printTestCount } }
- dependencies block で
testImplementation("org.springframework.boot:spring-boot-starter-test")
→testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude group: "org.junit.vintage", module: "junit-vintage-engine" }
に変更します。 - test タスクから
exclude "geb/**"
を削除します。
clean タスク実行 → Rebuild Project 実行 → build タスク実行すると BUILD SUCCESSFUL が表示され、testJUnit4AndSpock タスクが 147 tests、test タスクが 0 tests と Spring Boot をバージョンアップする前のテスト数になりました。
SpotBugs を 1.6.9 → 4.0.2 へバージョンアップする
build.gradle の以下の点を変更します。
plugins { .......... id "com.github.spotbugs" version "4.0.8" .......... } .......... configurations { .......... // for SpotBugs spotbugsStylesheets { transitive = false } } .......... spotbugs { ignoreFailures = true toolVersion = "4.0.2" spotbugsTest.enabled = false } spotbugsMain { reports { html { enabled = true stylesheet = "color.xsl" } } } .......... dependencies { .......... def spotbugsVersion = "4.0.2" .......... // for SpotBugs compileOnly("com.github.spotbugs:spotbugs:${spotbugsVersion}") compileOnly("net.jcip:jcip-annotations:1.0") compileOnly("com.github.spotbugs:spotbugs-annotations:${spotbugsVersion}") testImplementation("com.google.code.findbugs:jsr305:3.0.2") spotbugsStylesheets("com.github.spotbugs:spotbugs:${spotbugsVersion}") spotbugsPlugins("com.h3xstream.findsecbugs:findsecbugs-plugin:1.10.1") .......... }
- plugins block で
id "com.github.spotbugs" version "1.6.9"
→id "com.github.spotbugs" version "4.0.8"
に変更します。 - configurations block に
spotbugsStylesheets { transitive = false }
を追加します。 - spotbugs block の以下の点を変更します。
toolVersion = "3.1.11"
→toolVersion = "4.0.2"
に変更します。effort = "max"
を削除します。excludeFilter = file("${rootProject.projectDir}/config/spotbugs/spotbugs-exclude-filter.xml")
を削除します。
tasks.withType(com.github.spotbugs.SpotBugsTask) { ... }
→spotbugsMain { ... }
に変更します。- dependencies block の以下の点を変更します。
def spotbugsVersion = "3.1.11"
→def spotbugsVersion = "4.0.2"
に変更します。spotbugsStylesheets("com.github.spotbugs:spotbugs:${spotbugsVersion}")
を追加します。spotbugsPlugins("com.h3xstream.findsecbugs:findsecbugs-plugin:1.10.1")
を追加します。
clean タスク実行 → Rebuild Project 実行 → build タスク実行すると org.gradle.api.GradleException: Verification failed: SpotBugs violation found: 2
のメッセージが表示されました。
レポートファイル build/reports/spotbugs/main.html を開くと Medium Priority Warnings が2件出ています。
org/slf4j/Logger.info(Ljava/lang/String;)V を使用すると CRLF 文字をログメッセージに含めることができます。
Freemarker テンプレートの潜在的なテンプレートインジェクションです。
上の2件は Spring Boot 2.1.x の Web アプリを 2.2.x へバージョンアップする ( その10 )( SpotBugs プラグインの findsecbugs-plugin を導入する ) で対応済なので、同様に対応します。
clean タスク実行 → Rebuild Project 実行 → build タスク実行すると The following classes needed for analysis were missing:
以外のメッセージが表示されなくなりました。
PMD を 6.13.0 → 6.23.0 へバージョンアップする
build.gradle の以下の点を変更します。
pmd { toolVersion = "6.23.0" sourceSets = [project.sourceSets.main] ignoreFailures = true consoleOutput = true ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml") ruleSets = [] }
toolVersion = "6.13.0"
→toolVersion = "6.23.0"
に変更します。
clean タスク実行 → Rebuild Project 実行 → build タスク実行すると pmdMain タスクは何もメッセージが表示されませんでした。
error-prone を 2.3.3 → 2.3.4 へバージョンアップする
build.gradle の以下の点を変更します。
plugins { .......... id "net.ltgt.errorprone" version "1.1.1" .......... } .......... dependencies { .......... def errorproneVersion = "2.3.4" .......... // for Error Prone ( http://errorprone.info/ ) errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}") compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}") .......... }
- plugins block で
id "net.ltgt.errorprone" version "0.7.1"
→id "net.ltgt.errorprone" version "1.1.1"
に変更します。 - dependencies block で
def errorproneVersion = "2.3.3"
→def errorproneVersion = "2.3.4"
に変更します。
clean タスク実行 → Rebuild Project 実行 → build タスク実行するとエラーメッセージは出ずに BUILD SUCCESSFUL が表示されました。
これで今回ののバージョンアップは完了です。思ったより苦労はしなかったかな。
履歴
2020/05/12
初版発行。
IntelliJ IDEA 2020.1.1 で .properties ファイルの空行が削除される現象を解消する
記事一覧はこちらです。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その89 )( Spring Boot を 2.1.4 → 2.2.7 へバージョンアップする ) を書いている時に気づいたのですが、IntelliJ IDEA で .properties ファイルを開いて保存するとなぜか空行が削除されます。
これが、
server.tomcat.basedir=C:/webapps/boot-npm-geb-sample logging.file.name=${server.tomcat.basedir}/logs/boot-npm-geb-sample.log spring.autoconfigure.exclude=com.integralblue.log4jdbc.spring.Log4jdbcAutoConfiguration spring.mail.host=localhost spring.mail.port=25 valueshelper.classpath.prefix=BOOT-INF.classes.
自動で保存されてこうなります。
server.tomcat.basedir=C:/webapps/boot-npm-geb-sample logging.file.name=${server.tomcat.basedir}/logs/boot-npm-geb-sample.log spring.autoconfigure.exclude=com.integralblue.log4jdbc.spring.Log4jdbcAutoConfiguration spring.mail.host=localhost spring.mail.port=25 valueshelper.classpath.prefix=BOOT-INF.classes.
IntelliJ IDEA の設定を確認したところ、Editor -> Code Style -> Properties の「Keep blank lines」がチェックされていないことが原因でした。この設定をチェックすると空行が維持されるようになります。