かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

S3 にアップロードされた画像ファイルから Lambda でサムネイル画像を生成してみる

概要

記事一覧はこちらです。

S3 にアップロードした画像ファイルから Lambda でサムネイル画像を生成してみます。

f:id:ksby:20200610082312p:plain

  • アップロードする画像ファイルのフォーマットは JPEG とする。
  • サムネイル画像のフォーマットも JPEG とする。サイズは幅320 x 高さ180 とする。
  • 画像ファイルをアップロードする S3 Bucket は ksbysample-upload-bucket、サムネイル画像を置く S3 Bucket は ksbysample-resize-bucket とする。
  • サムネイル画像のファイル名は <オリジナルの画像ファイル名>_thumb.jpg とする。

よく聞くパターンなので簡単だと思っていましたが、結構苦労しました。。。

参照したサイト・書籍

  1. AWS Lambdaで画像ファイル加工~環境構築から実行確認まで~ 手順紹介
    https://business.ntt-east.co.jp/content/cloudsolution/column-try-17.html

  2. Lambda trigger on existing s3 bucket
    https://forum.serverless.com/t/lambda-trigger-on-existing-s3-bucket/6056

  3. Using existing buckets
    https://www.serverless.com/framework/docs/providers/aws/events/s3#using-existing-buckets

  4. How 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-trigger

  5. Sample Amazon S3 function code
    https://docs.aws.amazon.com/lambda/latest/dg/with-s3-example-deployment-pkg.html

  6. Pythonで文字列を置換(replace, translate, re.sub, re.subn)
    https://note.nkmk.me/python-str-replace-translate-re-sub/

  7. Pythonでパス文字列からファイル名・フォルダ名・拡張子を取得、結合
    https://note.nkmk.me/python-os-basename-dirname-split-splitext/

  8. AWS Lambdaで運用した実績から得られた、serverless frameworkのオススメ設定とプラグインの知見
    https://tech.ga-tech.co.jp/entry/2018/12/12/120000

  9. Serverless Python Requirements
    https://www.serverless.com/plugins/serverless-python-requirements/

  10. Pillow
    https://pillow.readthedocs.io/en/stable/index.html

  11. Why can't Python import Image from PIL?
    https://stackoverflow.com/questions/26505958/why-cant-python-import-image-from-pil/31728305

  12. AWS LambdaでPython Pillowを使うための手順
    https://qiita.com/ryasuna/items/9051cfdc0576134bb46c

  13. AWS Lambda で Pillow を使おうとしたらハマった
    https://michimani.net/post/aws-use-pillow-in-lambda/

  14. amazonlinux
    https://hub.docker.com/_/amazonlinux

    • Amazon Linux って Docker Image があるんですね。今回初めて知りました。
  15. lambci / docker-lambda
    https://github.com/lambci/docker-lambda

    • しかも Lambda 実行用の Docker Image もあるとは!
  16. AWS Lambda runtimes
    https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html

  17. WebP変換 & 画像キャッシュサービスをサーバレスで構築する - Feed re:Architect vol.1 -
    https://buildersbox.corp-sansan.com/entry/2019/06/06/124752

  18. AWS LambdaでTensorFlow 2.0を使った画像分類
    https://tech.unifa-e.com/entry/2019/09/17/085400

    • 今回の記事とは関係ありませんが、いろいろ調べている時に見かけて良い参考になりそうだったのでメモしておきます。

目次

  1. resize-image-app-project プロジェクトを作成する
  2. Serverless Framework で resize-service サブプロジェクトを作成する
  3. S3 Bucket を作成してイベント発生時に Lambda が呼び出されるよう serverless.yml を記述する
  4. S3 Bucket にファイルをアップロードした時の event の内容を確認する
  5. サムネイル画像を生成するよう handler.py の resize 関数を実装する
  6. serverless-python-requirements プラグインをインストールする
  7. deploy する
  8. 画像をアップロードしてサムネイル画像を生成してみる
  9. 最後に

手順

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 サブプロジェクトを作成します。

f:id:ksby:20200531190301p:plain

自動生成された serverless.yml に対し、以下の設定を追加します。

  • リソースが東京リージョン(ap-northeast-1)に生成されるようにする。

S3 Bucket を作成してイベント発生時に Lambda が呼び出されるよう serverless.yml を記述する

2つの S3 Bucket ksbysample-upload-bucketksbysample-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 が発生しています。

f:id:ksby:20200610095224p:plain

stackoverflow に How many records can be in S3 put() event lambda trigger? があり、これを読むと 1 ファイル 1 event のようです。

サムネイル画像を生成するよう handler.py の resize 関数を実装する

Sample Amazon S3 function codePython 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

f:id:ksby:20200604071741p:plain f:id:ksby:20200610113304p:plain

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 でインストールしてから、

f:id:ksby:20200610111025p:plain

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 を作成します。

f:id:ksby:20200610111730p:plain

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 時に追加するか聞かれるのですが、ランダム文字列を含むパスが追加されるので上位のディレクトリを追加しておくことにします。

f:id:ksby:20200610191306p:plain

npx sls deploy -v コマンドを実行して deploy します。

f:id:ksby:20200610190944p:plain f:id:ksby:20200610191049p:plain

最初に lambci/lambda:build-python3.8 の Docker Image をダウンロードして requirements.txt を参照して外部ライブラリをインストールしているのが分かります。

deploy が完了すると AWS マネジメントコンソールから ksbysample-upload-bucketksbysample-resize-bucket の2つの S3 Bucket が作成されていることが確認できました。

f:id:ksby:20200610191945p:plain

画像をアップロードしてサムネイル画像を生成してみる

ksbysample-upload-bucket に sample.jpg をアップロードすると、

f:id:ksby:20200610192308p:plain f:id:ksby:20200610192450p:plain

ksbysample-resize-bucket に sample_thumb.jpg が生成されていました!

f:id:ksby:20200610192554p:plain f:id:ksby:20200610192741p:plain

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 を複数のプロジェクトから利用することができるのか確認してみます。

参照したサイト・書籍

  1. Serverless Framework: Reusing S3 bucket for multiple projects deploy
    https://medium.com/@oieduardorabelo/reusing-s3-bucket-for-multiple-serverless-framework-projects-deploy-828e3a45f713

  2. Custom IAM Roles
    https://www.serverless.com/framework/docs/providers/aws/guide/iam/#custom-iam-roles

目次

  1. delploy した時の IAM Role はどのような設定か?
  2. delploy した時の S3 Bucket には何がアップロードされているのか?
  3. share-s3bucket-with-multi-servces プロジェクトを作成する
  4. Terraform で ksbysample-serverless-lambdaRole、ksbysample-serverless-deploymentbucket を作成する
  5. Serverless Framework のサブプロジェクトを2つ作成する
  6. deploy してみる
  7. remove してみる

手順

delploy した時の IAM Role はどのような設定か?

python-first-lambda-dev-ap-northeast-1-lambdaRole には python-first-lambda-dev-lambda という名前のインラインポリシーがアタッチされていて、以下の内容でした。

f:id:ksby:20200524130344p:plain

{
  "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"
    }
  ]
}

また信頼関係で以下のポリシーが設定されています。

f:id:ksby:20200524130440p:plain

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

delploy した時の S3 Bucket には何がアップロードされているのか?

f:id:ksby:20200524130838p:plain

serverless/python-first-lambda/dev/1590287550668-2020-05-24T02:32:30.668Z の下に以下のファイルがアップロードされています。

  • compiled-cloudformation-template.json
  • python-first-lambda.zip

プロジェクト名がパスの途中に入っているので、複数のプロジェクトで利用しても問題は出ないと思われます。

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 を使用します。

f:id:ksby:20200524172528p:plain

プロジェクトのルートディレクトリの下に .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

f:id:ksby:20200524183507p:plain f:id:ksby:20200524183729p:plain f:id:ksby:20200524183828p:plain f:id:ksby:20200524183920p:plain

IAM Role は serverless.yml に指定する時に ARN が必要になるので画面で確認します。

f:id:ksby:20200524191841p:plain

また .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 を実行します。

f:id:ksby:20200524190026p:plain

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 を実行します。

f:id:ksby:20200524193027p:plain

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" を実行します。

f:id:ksby:20200525233012p:plain

以前は表示されていた以下のメッセージは表示されなくなりました。

  • CREATE_IN_PROGRESS - AWS::S3::Bucket
  • CREATE_IN_PROGRESS - AWS::IAM::Role

AWS マネジメントコンソールで確認すると service-a-dev-hello が作成されており実行ロールに ksbysample-serverless-lambdaRole がセットされています。

f:id:ksby:20200525234328p:plain

次に service-b を deploy します。service-b のディレクトリに移動してから aws-vault exec $AWS_PROFILE -- bash -c "npx sls deploy -v" を実行します。

f:id:ksby:20200525233930p:plain

こちらも AWS::S3::BucketAWS::IAM::Role の CREATE_IN_PROGRESS は表示されません。

AWS マネジメントコンソールで確認すると service-b-dev-hello が作成されており実行ロールに ksbysample-serverless-lambdaRole がセットされています。

f:id:ksby:20200525234408p:plain

ksbysample-serverless-deploymentbucket 見ると service-a、service-b の compiled-cloudformation-template.json、zip ファイルがアップロードされていました。

f:id:ksby:20200525234504p:plain

service-a、service-b それぞれで aws-vault exec $AWS_PROFILE -- bash -c "npx sls invoke -f hello" を実行すると結果が返ってきます。

f:id:ksby:20200525235117p:plain

プロジェクト毎に IAM Role、S3 Bucket は作成されず Terraform で作成したものが利用されていました。

remove してみる

service-a、service-b で aws-vault exec $AWS_PROFILE -- bash -c "npx sls remove -v" を実行します。

f:id:ksby:20200525235824p:plain

どちらも IAM Role、S3 Bucket の DELETE_IN_PROGRESS は表示されませんでした。

AWS マネジメントコンソールで確認すると ksbysample-serverless-lambdaRole、ksbysample-serverless-deploymentbucket のどちらも残っています。

f:id:ksby:20200526000024p:plain f:id:ksby:20200526000225p:plain

作成した IAM Role と S3 Buckettf destroy を実行して削除します。

履歴

2020/05/26
初版発行。

IntelliJ IDEA+Node.js+npm+serverless framework+Python の組み合わせで開発環境を構築して AWS Lambda を作成してみる

概要

記事一覧はこちらです。

Serverless Framework を触ってみたいと思ったので IntelliJ IDEA で開発環境を構築して AWS Lambda を1つ作成してみます。言語は Python 3.8 にします。

参照したサイト・書籍

  1. Serverless Framework
    https://www.serverless.com/

  2. Python Release Python 3.8.3
    https://www.python.org/downloads/release/python-383/

  3. PyCharmでVenvをGitで共有するときにすること
    https://hiropon-progra.com/?p=82

  4. Windows 上の PythonUTF-8 をデフォルトにする
    https://qiita.com/methane/items/9a19ddf615089b071e71

  5. Hello World Python Example
    https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/python/

  6. Serverless Frameworkの使い方まとめ
    https://qiita.com/horike37/items/b295a91908fcfd4033a2

目次

  1. idea-serverless-python-first プロジェクトを作成する
  2. Python の仮想環境を作成する
  3. Serverless Framework をローカルインストールする
  4. .envrc を作成する
  5. python-first-lambda サブプロジェクトを作成する
  6. Lambda が東京リージョン(ap-northeast-1)に作成されるよう serverless.yml を編集する
  7. deploy する
  8. 作成した Lambda を実行する
  9. AWS に作成されたリソースを確認する
  10. 作成した Lambda を削除する
  11. 削除した後に再度 deploy するとバージョンが上がる
  12. 最後に

手順

idea-serverless-python-first プロジェクトを作成する

IntelliJ IDEA で Empty Project を作成します。

「New Project」ダイアログを表示してから画面左側で Empty Project を選択して「Next」ボタンをクリックします。

f:id:ksby:20200523112336p:plain

「Project Name」に idea-serverless-python-first、「Project location」に D:\project-serverless\ksbysample-serverless\idea-serverless-python-first を入力して「Finish」ボタンをクリックします。

f:id:ksby:20200523112555p:plain

Project が開くと「Project Structure」ダイアログが開きますが、今は何もせずに「Cancel」ボタンをクリックして閉じます。

ダイアログを閉じた直後は Project Tool Window に .idea ディレクトリが表示されていなかったので、一旦 IntelliJ IDEA を閉じて idea-serverless-python-first プロジェクトを開き直します。そうすると Project Tool Window が以下の表示になります。

f:id:ksby:20200523113049p:plain

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...」を選択します。

f:id:ksby:20200523114954p:plain

「Add Python Interpreter」ダイアログが表示されます。表示直後が下記の状態だったので、そのまま「OK」ボタンをクリックします。

f:id:ksby:20200523115059p:plain

「Create Virtual Environment」ダイアログが表示されて仮想環境が構築されます(少し時間がかかります)。

f:id:ksby:20200523115522p:plain

構築が完了すると中央のリストに「Python 3.8 (idea-serverless-python-first)」が追加されます。

f:id:ksby:20200523115727p:plain

画面左側で「Project Settings」-「Project」を選択した後、画面右側の「Project SDK」で作成した PythonSDK と「SDK default」を選択してから「OK」ボタンをクリックします。

f:id:ksby:20200523115912p:plain

Project Tool Window に venv ディレクトリが表示されています。

f:id:ksby:20200523122414p:plain

IntelliJ IDEA の Terminal を起動すると venv 環境で動くようになります。

f:id:ksby:20200523122727p:plain

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

f:id:ksby:20200523185429p:plain f:id:ksby:20200523185838p:plain

.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

f:id:ksby:20200523195452p:plain

Project Tool Window を見ると python-first-lambda ディレクトリが作成されて、その下に .gitignore, handler.py, serverless.yml の3つのファイルが作成されます。

f:id:ksby:20200523195620p:plain

作成したばかりの 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"

f:id:ksby:20200523203103p:plain f:id:ksby:20200523203202p:plain

deploy すると python-first-lambda サブプロジェクト内に .serverless ディレクトリが作成されます。アップロードする zip ファイルと create, update 用の CloudFormation の JSON ファイル、serverless-state.json というファイルが作成されています。

f:id:ksby:20200523214740p:plain

作成した Lambda を実行する

invoke コマンドを実行すると AWS 上の Lambda を実行して結果を取得することができます。invoke コマンドについては AWS - Invoke 参照。

  • aws-vault exec $AWS_PROFILE -- bash -c "npx sls invoke -f hello"

f:id:ksby:20200523213749p:plain

--log オプションを指定すればログを取得することもできます。

  • aws-vault exec $AWS_PROFILE -- bash -c "npx sls invoke -f hello --log"

f:id:ksby:20200523214304p:plain

logs コマンドでログだけ取得することも可能です。log コマンドは AWS - Logs 参照。

  • aws-vault exec $AWS_PROFILE -- bash -c "npx sls logs -f hello"

f:id:ksby:20200523214530p:plain

AWS に作成されたリソースを確認する

マネジメントコンソールから作成されたリソースを確認すると以下の5つが作成されています。

  • AWS Lambda
    • python-first-lambda-dev-hello f:id:ksby:20200523215817p:plain バージョンは 1、エイリアスは $LATEST のみ(つまり何も作成はされない)。
      f:id:ksby:20200523221323p:plainf:id:ksby:20200523221415p:plain
  • IAM Role
    • python-first-lambda-dev-ap-northeast-1-lambdaRole f:id:ksby:20200523220213p:plain
  • CloudWatch ロググループ
  • S3 バケット
    • python-first-lambda-dev-serverlessdeploymentbucke-7j3614vgkvv3 f:id:ksby:20200523220851p:plain
  • CloudFormation スタック

作成した Lambda を削除する

remove コマンドを実行して作成したリソース一式を削除します。invoke コマンドについては AWS - Remove 参照。

  • aws-vault exec $AWS_PROFILE -- bash -c "npx sls remove -v"

f:id:ksby:20200523224047p:plain

コマンド実行後、上に書いたリソースは全て削除されていました。

削除した後に再度 deploy するとバージョンが上がる

削除した後に再度 delploy するとバージョンが 2 に上がります。

f:id:ksby:20200523225109p:plain

何もソースを変更せずに再度 delploy するとバージョンは 2 のままです。

f:id:ksby:20200523225451p:plain

handler.py を少し変更して delploy するとバージョン 3 が追加されます。

f:id:ksby:20200523225706p:plain

remove してから .serverless ディレクトリを削除して deploy するとバージョンは 1 に戻らず 4 に上がりました。.serverless ディレクトリの下のファイルを見てみましたが、最新のバージョン番号が書かれているファイルはないですね。どこに最新バージョンを保持しているのでしょうか?

f:id:ksby:20200523230309p:plain

最後に

やってみると Serverless Framework は使いやすそうです。個人的には Serverless Step Functions が便利と聞いたので、もう少し基本的なところを調べたら触ってみるつもりです。

履歴

2020/05/24
初版発行。

徒然なるままに serverless( 大目次 )

GitHubhttps://github.com/ksby/ksbysample-serverless

  1. IntelliJ IDEA+Node.js+npm+serverless framework+Python の組み合わせで開発環境を構築して AWS Lambda を作成してみる
  2. 別途作成しておいた IAM Role、S3 Bucket を複数の Serverless Framework のプロジェクトから利用できるのか?
  3. S3 にアップロードされた画像ファイルから Lambda でサムネイル画像を生成してみる
  4. resize-image-app-project プロジェクトで作成した AWS Lambda のユニットテストを作成する(local動作版)
  5. resize-image-app-project プロジェクトで作成した AWS Lambda のユニットテストを Docker コンテナ上で動作させる
  6. 外部パッケージの Pillow と独自モジュール(.py ファイル)を Lambda Layer に配置する(前編)
  7. 外部パッケージの Pillow と独自モジュール(.py ファイル)を Lambda Layer に配置する(後編)
  8. API Gateway で受信したメッセージを SNS 経由で Slack へ通知する
  9. serverless-domain-manager プラグインを利用して独自ドメインで API Gateway にアクセスする
  10. API Gateway で受信するデータを JSON Schema Validation でチェックしてから SQS へ送信する
  11. aws-lambda-powertools を試してみる(Logger 編)
  12. aws-lambda-powertools を試してみる(Tracer&X-Ray 編その1)
  13. aws-lambda-powertools を試してみる(Tracer&X-Ray 編その2)
  14. boto3 のインスタンス生成をグローバルで行っても moto を利用したユニットテストを成功させるには?
  15. Serverless Framework で deploy 用ディレクトリへ移動→環境変数を設定する方法で deploy する環境を切り替える(その1)
  16. Serverless Framework で deploy 用ディレクトリへ移動→環境変数を設定する方法で deploy する環境を切り替える(その2)
  17. Serverless Framework で deploy 用ディレクトリへ移動→環境変数を設定する方法で deploy する環境を切り替える(その3、CircleCI に deploy する)

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その91 )( Doma 2 を 2.28.0 → 2.34.0 へバージョンアップする+domaGen タスクを doma-codegen-plugin を利用したものに作り直す )

概要

記事一覧はこちらです。

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 へバージョンアップする ) の続きです。

終わったと思いましたが、domaGen タスクが動作しなかったので Doma 2 を最新バージョンまで上げていませんでした。最新バージョンに上げて domaGen タスクが動くようにします。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 を 2.28.0 → 2.34.0 へバージョンアップします。
    • 2.29.0 以降現行の domaGen タスクが動かなくなったので doma-codegen-plugin を利用したものに変更します。

参照したサイト・書籍

  1. Building an application - Build with Gradle
    https://doma.readthedocs.io/en/latest/build/?highlight=org.seasar.doma%3Adoma#build-with-gradle

  2. domaframework / doma-codegen-plugin
    https://github.com/domaframework/doma-codegen-plugin

目次

  1. Doma 2 を 2.28.0 → 2.34.0 へバージョンアップする
  2. domaGen タスクを 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 が表示されます。

f:id:ksby:20200513223551p:plain

gebTest タスクも特に問題なく BUILD SUCCESSFUL が表示されました。

f:id:ksby:20200513225040p:plain

ただし domaGen タスクを実行すると Could not resolve all files for configuration ':domaGenRuntime'. のエラーメッセージが表示されて実行できません。

f:id:ksby:20200513225306p:plain

以前 Doma-Gen メンテナンス終了のアナウンスが出ていましたが、2.29.0 からなくなったようです。

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 タスクが表示されます。

f:id:ksby:20200515000016p:plain

Tomcat を起動してから domaGen タスクを実行すると BUILD SUCCESSFUL が表示され entity クラス、dao インターフェースが生成されます。

f:id:ksby:20200515000402p:plain

履歴

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 へバージョンアップします。

参照したサイト・書籍

  1. spotbugs / spotbugs-gradle-plugin
    https://github.com/spotbugs/spotbugs-gradle-plugin

目次

  1. Checkstyle を 8.19 → 8.32 へバージョンアップする
  2. testJUnit4AndSpock と test タスクで実行されるテスト数が同じ問題を解消する
  3. SpotBugs を 1.6.9 → 4.0.2 へバージョンアップする
  4. PMD を 6.13.0 → 6.23.0 へバージョンアップする
  5. 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 になっていることに気づきました。。。

f:id:ksby:20200510175856p:plain

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 をバージョンアップする前のテスト数になりました。

f:id:ksby:20200510234021p:plain

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 のメッセージが表示されました。

f:id:ksby:20200512005712p:plain

レポートファイル build/reports/spotbugs/main.html を開くと Medium Priority Warnings が2件出ています。

f:id:ksby:20200512010032p:plain f:id:ksby:20200512010145p:plain f:id:ksby:20200512010238p:plain

  • 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: 以外のメッセージが表示されなくなりました。

f:id:ksby:20200512012403p:plain

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 タスクは何もメッセージが表示されませんでした。

f:id:ksby:20200512013651p:plain

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 が表示されました。

f:id:ksby:20200512224201p:plain

これで今回ののバージョンアップは完了です。思ったより苦労はしなかったかな。

履歴

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」がチェックされていないことが原因でした。この設定をチェックすると空行が維持されるようになります。

f:id:ksby:20200510111728p:plain