かんがるーさんの日記

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

Spring Boot 2.2.x の Web アプリを 2.3.x へバージョンアップする ( その2 )( Spring Boot を 2.2.2 → 2.2.9 へ、Gradle を 6.0.1 → 6.5.1 へバージョンアップする )

概要

記事一覧はこちらです。

Spring Boot 2.2.x の Web アプリを 2.3.x へバージョンアップする ( その1 )( 概要 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Spring Boot のバージョンを 2.2 系の最新バージョンである 2.2.9 へ、Gradle のバージョンを 6.x 系の最新バージョンである 6.5.1 に上げて build できることを確認します。
    • 今回は問題がなければライブラリはバージョンアップしません。

参照したサイト・書籍

目次

  1. 2.3.x ブランチの作成
  2. Spring Boot を 2.2.2 → 2.2.9 にバージョンアップする
  3. Gradle を 6.0.1 → 6.5.1 にバージョンアップする

手順

2.3.x ブランチの作成

master から 2.3.x ブランチを、2.3.x から feature/135-issue ブランチを作成します。

Spring Boot を 2.2.2 → 2.2.9 にバージョンアップする

build.gradle の以下の点を変更します。

buildscript {
    ext {
        group "ksbysample"
        version "2.2.9-RELEASE"
    }
    repositories {
        mavenCentral()
        maven { url "https://repo.spring.io/release/" }
        gradlePluginPortal()
    }
}

plugins {
    id "java"
    id "eclipse"
    id "idea"
    id "org.springframework.boot" version "2.2.9.RELEASE"
    id "io.spring.dependency-management" version "1.0.9.RELEASE"
    id "groovy"
    id "checkstyle"
    id "com.github.spotbugs" version "3.0.0"
    id "pmd"
    id "net.ltgt.errorprone" version "1.1.1"
    id "com.gorylenko.gradle-git-properties" version "2.2.0"
}

..........
  • buildscript block の以下の点を変更します。
    • version "2.2.2-RELEASE"version "2.2.9-RELEASE"
  • plugins block の以下の点を変更します。
    • id "org.springframework.boot" version "2.2.2.RELEASE"id "org.springframework.boot" version "2.2.9.RELEASE"
    • id "io.spring.dependency-management" version "1.0.8.RELEASE"id "io.spring.dependency-management" version "1.0.9.RELEASE"

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新した後、clean タスク実行 → Rebuild Project 実行。。。すると以下の画像のエラーが出ました。

f:id:ksby:20200808195347p:plain

Web で検索しても該当しそうな記事が見つかりません。

groovy の compile 時のエラーのようなので groovy のバージョンを確認したところ、2.2.2 の時は 2.5.8、2.2.9 の時は 2.5.13(https://mvnrepository.com/artifact/org.codehaus.groovy/groovy を見ると 2.5 系の最終バージョンの模様)でした。

build.gradle で groovy のバージョンを 2.5.12 にバージョンダウンすると Rebuild Project 実行時にエラーが出なくなりました。

dependencyManagement {
    imports {
        // mavenBom は以下の URL のものを使用する
        // https://repo.spring.io/release/org/springframework/boot/spring-boot-starter-parent/2.2.9.RELEASE/
        // bomProperty に指定可能な property は以下の URL の BOM に記述がある
        // https://repo.spring.io/release/org/springframework/boot/spring-boot-dependencies/2.2.9.RELEASE/spring-boot-dependencies-2.2.9.RELEASE.pom
        mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) {
            // Spring Boot の BOM に定義されているバージョンから変更する場合には、ここに以下のように記述する
            // bomProperty "thymeleaf.version", "3.0.9.RELEASE"
            bomProperty "groovy.version", "2.5.12"
        }
        mavenBom("org.junit:junit-bom:5.5.2")
    }
}
  • dependencyManagement block に bomProperty "groovy.version", "2.5.12" を追加します。

build タスクを実行すると、今度は以下の画像のエラーが出ました。Caused by: org.dbunit.dataset.DataSetException Caused by: org.xml.sax.SAXNotSupportedException で大量のエラーが発生しています。

f:id:ksby:20200808202608p:plain

Project Tool Window で src/test でコンテキストメニューを表示して「Run 'All Tests'」を選択してテストを実行し、もう少し詳しいエラーメッセージを見てみると、以下のエラーメッセージが出力されていました。

f:id:ksby:20200808203335p:plain

java.lang.RuntimeException: org.dbunit.dataset.DataSetException: not supported setting property http://xml.org/sax/properties/lexical-handler

    at ksbysample.common.test.extension.db.TestDataExtension.after(TestDataExtension.java:129)
    at ksbysample.common.test.extension.db.TestDataExtension.afterEach(TestDataExtension.java:75)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAfterEachCallbacks$11(TestMethodTestDescriptor.java:245)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAllAfterMethodsOrCallbacks$12(TestMethodTestDescriptor.java:256)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAllAfterMethodsOrCallbacks$13(TestMethodTestDescriptor.java:256)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeAllAfterMethodsOrCallbacks(TestMethodTestDescriptor.java:255)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeAfterEachCallbacks(TestMethodTestDescriptor.java:244)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:141)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229)
    at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197)
    at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Caused by: org.dbunit.dataset.DataSetException: not supported setting property http://xml.org/sax/properties/lexical-handler
    at org.dbunit.dataset.xml.XmlProducer.buildException(XmlProducer.java:182)
    at org.dbunit.dataset.xml.FlatXmlProducer.produce(FlatXmlProducer.java:373)
    at org.dbunit.dataset.CachedDataSet.<init>(CachedDataSet.java:80)
    at org.dbunit.dataset.xml.FlatXmlDataSet.<init>(FlatXmlDataSet.java:110)
    at org.dbunit.dataset.xml.FlatXmlDataSetBuilder.buildInternal(FlatXmlDataSetBuilder.java:264)
    at org.dbunit.dataset.xml.FlatXmlDataSetBuilder.build(FlatXmlDataSetBuilder.java:111)
    at ksbysample.common.test.extension.db.TestDataExtension.restoreDb(TestDataExtension.java:207)
    at ksbysample.common.test.extension.db.TestDataExtension.after(TestDataExtension.java:126)
    ... 60 more
Caused by: org.xml.sax.SAXNotSupportedException: not supported setting property http://xml.org/sax/properties/lexical-handler
    at org.gjt.xpp.sax2.Driver.setProperty(Driver.java:204)
    at org.dbunit.dataset.xml.FlatDtdProducer.setLexicalHandler(FlatDtdProducer.java:132)
    at org.dbunit.dataset.xml.FlatXmlProducer.produce(FlatXmlProducer.java:358)
    ... 66 more

build.gradle で testImplementation("org.dbunit:dbunit:2.6.0")testImplementation("org.dbunit:dbunit:2.7.0") にバージョンアップしても状況は変わりません(一旦 2.6.0 に戻しました)。

Web で調べると The SAX parser pull-parser conflicts with Tomcat SAX parser という Issue が見つかりました。同じエラーメッセージが出ていますが、pull-parser-2.jar を削除すると解消したとのこと。Error Parsing /index.xhtml: not supported setting property http://xml.org/sax/properties/lexical-handler の記事も見つけました。dom4j の optional なので外しても問題ないようです。

gradlew dependencies を実行して pull-parser-2.jar に依存しているモジュールを探すと com.github.spotbugs:spotbugs:4.0.0-beta4 がヒットしました。

..........
+--- com.github.spotbugs:spotbugs:4.0.0-beta4
|    +--- org.ow2.asm:asm:7.1
|    +--- org.ow2.asm:asm-analysis:7.1
|    |    \--- org.ow2.asm:asm-tree:7.1
|    |         \--- org.ow2.asm:asm:7.1
|    +--- org.ow2.asm:asm-commons:7.1
|    |    +--- org.ow2.asm:asm:7.1
|    |    +--- org.ow2.asm:asm-tree:7.1 (*)
|    |    \--- org.ow2.asm:asm-analysis:7.1 (*)
|    +--- org.ow2.asm:asm-tree:7.1 (*)
|    +--- org.ow2.asm:asm-util:7.1
|    |    +--- org.ow2.asm:asm:7.1
|    |    +--- org.ow2.asm:asm-tree:7.1 (*)
|    |    \--- org.ow2.asm:asm-analysis:7.1 (*)
|    +--- org.apache.bcel:bcel:6.3.1
|    +--- net.jcip:jcip-annotations:1.0
|    +--- org.dom4j:dom4j:2.1.1 -> 2.1.3
|    |    +--- jaxen:jaxen:1.1.6 -> 1.2.0
|    |    +--- javax.xml.stream:stax-api:1.0-2
|    |    +--- net.java.dev.msv:xsdlib:2013.6.1
|    |    |    \--- relaxngDatatype:relaxngDatatype:20020414
|    |    +--- javax.xml.bind:jaxb-api:2.2.12 -> 2.3.1
|    |    |    \--- javax.activation:javax.activation-api:1.2.0
|    |    +--- pull-parser:pull-parser:2
|    |    \--- xpp3:xpp3:1.1.4c
|    +--- jaxen:jaxen:1.1.6 -> 1.2.0
|    +--- commons-lang:commons-lang:2.6
|    +--- org.slf4j:slf4j-api:1.8.0-beta4 -> 1.7.30
|    +--- net.sf.saxon:Saxon-HE:9.9.1-2
|    |    \--- com.ibm.icu:icu4j:63.1
|    \--- com.github.spotbugs:spotbugs-annotations:4.0.0-beta4
|         \--- com.google.code.findbugs:jsr305:3.0.2
..........

build.gradle で com.github.spotbugs:spotbugs:4.0.0-beta4 の依存関係から pull-parser:pull-parser:2 を除外してみます。

dependencies {
    ..........

    // for SpotBugs
    compileOnly("com.github.spotbugs:spotbugs:${spotbugsVersion}") {
        exclude group: "pull-parser", module: "pull-parser"
    }
    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")
}
  • exclude group: "pull-parser", module: "pull-parser" を追加します。

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新してから、再び clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると今度は無事 "BUILD SUCCESSFUL" のメッセージが出力されました。

f:id:ksby:20200808222943p:plain

Gradle を 6.0.1 → 6.5.1 にバージョンアップする

build.gradle の wrapper タスクの記述を以下のように変更します。

wrapper {
    gradleVersion = "6.5.1"
    distributionType = Wrapper.DistributionType.ALL
}
  • gradleVersion = "6.0.1"gradleVersion = "6.5.1" に変更します。

コマンドプロンプトから gradlew wrapper --gradle-version 6.5.1gradlew --version コマンドを実行します。

f:id:ksby:20200808232307p:plain

gradle/wrapper/gradle-wrapper.properties は以下の内容になります。

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

JVM を呼び出す時のメモリ割り当ての記述が元に戻るので、gradlew.bat 内の記述を set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"set DEFAULT_JVM_OPTS="-Xmx4096m" に変更します(gradlew も同じような変更をします)。

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します(少し時間がかかります)。

clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、spotbugsMain タスクでエラーになりました。'org.gradle.process.internal.worker.SingleRequestWorkerProcessBuilder org.gradle.process.internal.worker.WorkerProcessFactory.singleRequestWorker(java.lang.Class, java.lang.Class)' というエラーメッセージが出力されています。

f:id:ksby:20200809104256p:plain

SpotBugs は後でバージョンアップする予定なので今回はコメントアウトすることにします。

build.gradle で SpotBugs の記述をコメントアウトします。 ただしソース内で SpotBug s のアノテーションを記述しているところがあるので compileOnly("com.github.spotbugs:spotbugs-annotations:${spotbugsVersion}") だけは残します。

..........

plugins {
    id "java"
    id "eclipse"
    id "idea"
    id "org.springframework.boot" version "2.2.9.RELEASE"
    id "io.spring.dependency-management" version "1.0.9.RELEASE"
    id "groovy"
    id "checkstyle"
//    id "com.github.spotbugs" version "3.0.0"
    id "pmd"
    id "net.ltgt.errorprone" version "1.1.1"
    id "com.gorylenko.gradle-git-properties" version "2.2.0"
}

..........

//spotbugs {
//    toolVersion = "4.0.0-beta4"
//    ignoreFailures = true
//    effort = "max"
//    spotbugsTest.enabled = false
//}
//tasks.withType(com.github.spotbugs.SpotBugsTask) {
//    reports {
//        xml.enabled = false
//        html.enabled = true
//        html.stylesheet = resources.text.fromArchiveEntry(configurations.spotbugsStylesheets, "color.xsl")
//    }
//}

..........

dependencies {
    ..........

    // for SpotBugs
//    compileOnly("com.github.spotbugs:spotbugs:${spotbugsVersion}") {
//        exclude group: "pull-parser", module: "pull-parser"
//    }
//    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")
}

再び clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると今度は無事 "BUILD SUCCESSFUL" のメッセージが出力されました。

f:id:ksby:20200809105026p:plain

履歴

2020/08/09
初版発行。

Spring Boot 2.2.x の Web アプリを 2.3.x へバージョンアップする ( その1 )( 概要 )

概要

記事一覧はこちらです。

  • 「Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る」で作成した Web アプリケーション ( ksbysample-webapp-lending ) の Spring Boot のバージョンを 2.2.2 → 2.3.x へバージョンアップします。
  • 進め方は以下の方針とします。
    • Git のブランチは 2.3.x を作成して、そちらで作業します。Spring Boot のバージョンと合わせます。
    • Spring Boot のバージョンを 2.2 系の最新バージョンである 2.2.9 へ、Gradle のバージョンを 6.x 系の最新バージョンである 6.5.1 に上げて build できることを確認します。この時点ではライブラリはバージョンアップしません。
    • Spring Boot のバージョン番号を 2.3.x にします。
      • Spring Initializr で 2.3.x のプロジェクトを作成して、修正した方がよさそうな点があれば反映します。
      • ライブラリは最新バージョンにアップデートします。ただし、この時点では checkstyle, spotbugs, pmd, Error Prone のバージョンは上げません。
    • プロジェクトを build し直してエラーが出る点があれば修正し、まずはここまでで動くようにします。
    • その後で 2.3 系ではこう書くべきという点があるか確認し、変更した方がよいところを変更します。
    • checkstyle, spotbugs, pmd, Error Prone を1つずつ最新バージョンに上げます。変更した方がよいところがあれば変更します。
    • docker-compose で使用している image を最新バージョンに上げます。  
       

2.3 の Release Notes はこちらです。

Spring Boot 2.3 Release Notes
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.3-Release-Notes

履歴

2020/08/08
初版発行。

Spring Boot 2.2.x の Web アプリを 2.3.x へバージョンアップする ( 大目次 )

  1. その1 ( 概要 )
  2. その2 ( Spring Boot を 2.2.2 → 2.2.9 へ、Gradle を 6.0.1 → 6.5.1 へバージョンアップする )
  3. その3 ( Spring Boot を 2.2.9 → 2.3.2 へバージョンアップする )
  4. その4 ( Release Notes を見て必要な箇所を変更する )
  5. その5 ( Checkstyle を 8.28 → 8.35 へバージョンアップする )
  6. その6 ( PMD を 6.20.0 → 6.26.0 へバージョンアップする )
  7. その7 ( Error Prone を 2.3.4 → 2.4.0 へバージョンアップする )
  8. その8 ( SpotBugs を 4.0.0-beta4 → 4.1.1 へバージョンアップする )

AdoptOpenJDK を 11.0.7+10.2 → 11.0.8+10 へ、IntelliJ IDEA を 2020.1.3 → 2020.1.4 へ、Git for Windows を 2.27.0 → 2.28.0 へバージョンアップ

AdoptOpenJDK を 11.0.7+10.2 → 11.0.8+10 へバージョンアップする

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. https://adoptopenjdk.net/?variant=openjdk11&jvmVariant=hotspot を見ると 11.0.8+10 がダウンロードできるようになっていましたので、11.0.8+10 へバージョンアップします。

    f:id:ksby:20200808100602p:plain

  2. インストール時に削除されるかもしれないので D:\Java\jdk-11.0.7.10-hotspot → D:\Java\jdk-11.0.7.10-hotspotx にリネームします。

  3. OpenJDK11U-jdk_x64_windows_hotspot_11.0.8_10.msi をダウンロードして D:\Java\jdk-11.0.8.10-hotspot へインストールした後、環境変数 JAVA_HOME のパスを D:\Java\jdk-11.0.8.10-hotspot へ変更します。

    コマンドプロンプトから java -version を実行し、11.0.8 に変更されていることを確認します。

    f:id:ksby:20200808101533p:plain

  4. D:\Java\jdk-11.0.7.10-hotspotx → D:\Java\jdk-11.0.7.10-hotspot に戻します。

  5. IntelliJ IDEA を再起動した後、プロジェクトで使用する JDK を 11.0.8+10 へ変更します。

  6. ダイアログ下部の「Configure」-「Structure for New Projects」を選択します。

    f:id:ksby:20200808102411p:plain

  7. 「Project Structure for New Projects」ダイアログが表示されます。画面左側で「Project Settings」-「Project」を選択後、画面右側の「Project SDK」の「New...」ボタン。。。がいつの間にか消えていますね。ドロップダウンリストを表示すると D:\Java\jdk-11.0.8.10-hotspot が検知されて表示されていたので選択します。

    f:id:ksby:20200808103006p:plain

  8. 「Project SDK」の「Edit」ボタンをクリックします。

    f:id:ksby:20200808103421p:plain

  9. 画面左側で「Platform Settings」-「SDKs」が選択された状態になるので、画面右上の入力フィールドで "11 (2)" → "11.0.8.10" へ変更します。

    f:id:ksby:20200808103652p:plain

  10. 次に中央のリストから「11.0.7.10」を選択した後、リストの上の「-」ボタンをクリックして削除します。「11」も不要なので削除します。

    f:id:ksby:20200808104003p:plain

  11. 「OK」ボタンをクリックして「Project Structure for New Projects」ダイアログを閉じます。

  12. 「Welcome to IntelliJ IDEA」ダイアログに戻ったら、ksbysample-webapp-lending プロジェクトを開きます。

  13. IntelliJ IDEA のメイン画面が開いたら、メニューから「File」-「Project Structure...」を選択します。

  14. 「Project Structure」ダイアログが表示されます。以下の画像の状態になっているので、

    f:id:ksby:20200808104453p:plain

    「Project SDK」を選択し直します。「Project SDK」を「11.0.8.10」に変更すると「Project language level」も自動で「SDK default (11 - Local variable syntax for lambda param」が選択されました。

    f:id:ksby:20200808104603p:plain

  15. 「OK」ボタンをクリックして「Project Structure」ダイアログを閉じます。

  16. メイン画面に戻ると画面右下に「Indexing...」の表示が出るので、終了するまで待ちます。

  17. Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

  18. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20200808105827p:plain

  19. Project Tool Window で src/test でコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20200808110528p:plain

  20. 特に問題は発生しませんでした。11.0.8+10 で開発を進めます。

IntelliJ IDEA を 2020.1.3 → 2020.1.4 へバージョンアップする

IntelliJ IDEA の 2020.1.4 がリリースされているのでバージョンアップします。2020.2 もリリースされていますが、今回はバージョンアップしません。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  2. IDE and Plugin Updates」ダイアログが表示されます。右下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20200808114455p:plain

  3. Plugin の update も表示されました。このまま「Update and Restart」ボタンをクリックします。

    f:id:ksby:20200808114540p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20200808115109p:plain

  6. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2020.1.4 へバージョンアップされていることを確認します。

  7. Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

  8. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20200808120149p:plain

  9. Project Tool Window で src/test でコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20200808120921p:plain

Git for Windows を 2.27.0 → 2.28.0 へバージョンアップする

Git for Windows の 2.28.0 がリリースされていたのでバージョンアップします。

  1. https://gitforwindows.org/ の「Download」ボタンをクリックして Git-2.28.0-64-bit.exe をダウンロードします。

  2. Git-2.28.0-64-bit.exe を実行します。

  3. 「Git 2.28.0 Setup」ダイアログが表示されます。インストーラーの画面を一通り見たいので「Only show new options」のチェックを外してから [Next >] ボタンをクリックします。

  4. 「Select Components」画面が表示されます。「Git LFS(Large File Support)」だけチェックした状態で [Next >]ボタンをクリックします。

  5. 「Choosing the default editor used by Git」画面が表示されます。「Use Vim (the ubiquitous text editor) as Git's default editor」が選択された状態で [Next >]ボタンをクリックします。

  6. 「Adjusting your PATH environment」画面が表示されます。中央の「Git from the command line and also from 3rd-party software」が選択されていることを確認後、[Next >]ボタンをクリックします。

  7. 「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。

  8. 「Configuring the line ending conversions」画面が表示されます。一番上の「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  9. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  10. 「Choose the default behavior of git pull」画面が表示されます(新画面)。「Default (fast-forward or merge)」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  11. 「Choose a credential helper」画面が表示されます。前回のバージョンアップではなかった画面です。「None」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  12. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Next >]ボタンをクリックします。

  13. 「Configuring experimental options」画面が表示されます。何もチェックせずに [Install]ボタンをクリックします。

  14. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View Release Notes」のチェックを外した後、[Next >]ボタンをクリックしてインストーラーを終了します。

  15. コマンドプロンプトを起動して git --version を実行し、git のバージョンが git version 2.28.0.windows.1 になっていることを確認します。

    f:id:ksby:20200808122820p:plain

  16. 特に問題はないようですので、2.28.0 で作業を進めたいと思います。

Serverless Framework で deploy 用ディレクトリへ移動→環境変数を設定する方法で deploy する環境を切り替える(その3、CircleCI に deploy する)

概要

記事一覧はこちらです。

Serverless Framework で deploy 用ディレクトリへ移動→環境変数を設定する方法で deploy する環境を切り替える(その2) の続きです。

参照したサイト・書籍

  1. Making Serverless CI/CD Easier with CircleCI and Serverless Framework
    https://aws.amazon.com/jp/blogs/apn/making-serverless-ci-cd-easier-with-circleci-and-serverless-framework/

  2. Welcome to CircleCI Documentation
    https://circleci.com/docs/ja/2.0/

    • CircleCI のドキュメントの日本語版。
  3. Configuring CircleCI
    https://circleci.com/docs/2.0/configuration-reference/

  4. あなたがnpm installをしてはいけない時
    https://blog.minimalcorp.com/users/jigen/posts/6f325dc9b8a00370b6aedf47a34cb3ce

  5. npm ciを使おう あるいはより速く
    https://qiita.com/mstssk/items/8759c71f328cab802670

  6. まさかPushデバッグしてないよね? よく使うCircleCIのデバッグ方法
    https://blog.vtryo.me/entry/circleci-debug-method

  7. いまさらだけどCircleCIに入門したので分かりやすくまとめてみた
    https://qiita.com/gold-kou/items/4c7e62434af455e977c2

目次

  1. .circleci/config.yml を作成する
  2. CircleCI で Project の設定をする
  3. AWS マネジメントコンソールで circle-ci ユーザーを作成し、アクセスキーを作成する
  4. CircleCI の管理画面から AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY を設定する
  5. .circleci/config.yml で NODE_ENV=ci npm run deploy:all を実行するよう変更する
  6. commit&push して CircleCI から deploy する
  7. AWS に作成したリソースを確認する
  8. 動作確認する
  9. 何も変更せずに deploy し直すと cache の効果でどのくらい時間が短縮されるのか?
  10. stages/prod/custom-shared-package-layer.yml の説明
  11. CircleCI で deploy したリソース一式を削除する

手順

.circleci/config.yml を作成する

.circleci/config.yml を新規作成し、まずは checkout だけ記述します。

version: 2.1
jobs:
  deploy:
    docker:
      - image: lambci/lambda:build-python3.8
    steps:
      - checkout
workflows:
  version: 2
  deploy:
    jobs:
      - deploy:
          filters:
            branches:
              only: master

Docker Image は serverless-python-requirements プラグインが使用している lambci/lambda:build-python3.8 を使用します。以下の理由で使いやすかったからです。

CircleCI で Project の設定をする

CircleCI の管理画面から「Proejcts」の画面を表示し、ksbysample-serverless-deploy の「Set Up Project」ボタンをクリックします。

f:id:ksby:20200718124240p:plain

.circleci/config.yml は追加してあるので「Add Manually」ボタンをクリックします。

f:id:ksby:20200718124458p:plain

下の画像のダイアログが表示されるので「Start Building」ボタンをクリックします。

f:id:ksby:20200718124738p:plain:w300

workflow が実行されて SUCCESS になります。

f:id:ksby:20200718143749p:plain

AWS マネジメントコンソールで circle-ci ユーザーを作成し、アクセスキーを作成する

本番環境(prod)を deploy するアカウントで circle-ci ユーザーを作成し、アクセスキーも作成します。アクセス権限は AdministratorAccess ポリシーをアタッチします。

f:id:ksby:20200718145522p:plain f:id:ksby:20200718171333p:plain

CircleCI の管理画面から AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY を設定する

CircleCI の ksbysample-serverless-deploy の画面右上の「Project Settings」ボタンをクリックします。

f:id:ksby:20200718172144p:plain

Project Settings 画面が表示されたら左側の「Environment Variables」を選択した後、右側の「Add Environment Variable」ボタンをクリックして AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY を追加します。

f:id:ksby:20200718172415p:plain f:id:ksby:20200718173018p:plain

.circleci/config.yml で NODE_ENV=ci npm run deploy:all を実行するよう変更する

.circleci/config.yml を以下のように変更します。

version: 2.1
jobs:
  deploy:
    docker:
      - image: lambci/lambda:build-python3.8
    environment:
      AWS_DEFAULT_REGION: ap-northeast-1
      STAGE: prod
      LOG_LEVEL: ERROR
      POWERTOOLS_TRACE_DISABLED: true
      NODE_PATH: /opt/nodejs/node12/node_modules:/opt/nodejs/node_modules:/var/runtime/node_modules
    steps:
      - checkout
      - run:
          name: install nodejs12.x
          command: curl https://lambci.s3.amazonaws.com/fs/nodejs12.x.tgz | tar -zx -C /
      - run: node -v
      - run: npm -v

      # Caching Dependencies
      # https://circleci.com/docs/2.0/caching/
      - restore_cache:
          keys:
            - asset-cache-v1-{{ arch }}-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
            - asset-cache-v1-{{ arch }}-{{ .Branch }}
            - asset-cache-v1
      - run: npm ci
      - run: pip install -r requirements.txt
      - run: python -m unittest -v
      - run: NODE_ENV=ci npm run deploy:all
      - save_cache:
          key: asset-cache-v1-{{ arch }}-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
          paths:
            # https://docs.npmjs.com/cli/ci#example
            - $HOME/.npm
            - /var/lang/lib/python3.8
            - /root/project/.cache
workflows:
  version: 2
  deploy:
    jobs:
      - deploy:
          filters:
            branches:
              only: master

commit&push して CircleCI から deploy する

master ブランチに commit&push すると worflow が動作して無事 SUCCESS になりました。

f:id:ksby:20200718184952p:plain

各 step にかかった時間は以下の通りです。

f:id:ksby:20200718185126p:plain f:id:ksby:20200718185228p:plain

NODE_ENV=ci npm run deploy:all の状況を見ると per-env の後に cross-env-shell "cd $SERVICE_DIR && npx sls deploy -v" が実行されており、npm-scripts の deploy-service:ci が実行されていることが分かります。

f:id:ksby:20200718185517p:plain f:id:ksby:20200718185625p:plain f:id:ksby:20200718190502p:plain f:id:ksby:20200718185839p:plain f:id:ksby:20200718185957p:plain f:id:ksby:20200718190110p:plain f:id:ksby:20200718190223p:plain f:id:ksby:20200718190316p:plain

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

作成されたリソースを確認すると全てに STAGE 名である prod が入っています。

■Lambda 関数 f:id:ksby:20200718192438p:plain

■Lambda レイヤー f:id:ksby:20200718192550p:plain

■IAM ロール f:id:ksby:20200718192700p:plain

■CloudWatch ロググループ f:id:ksby:20200718192814p:plain

■S3 バケット f:id:ksby:20200718192956p:plain

■CloudFormation スタック f:id:ksby:20200718193108p:plain

API Gateway f:id:ksby:20200718193205p:plain

■SQS キュー f:id:ksby:20200718193334p:plain

■DynamoDB テーブル f:id:ksby:20200718193442p:plain

動作確認する

Postman から API Gateway の endpoint にアクセスすると 200 OK が返ってきて、

f:id:ksby:20200718204452p:plain

DynamoDB の sample-table-prod テーブルにデータが保存されました。

f:id:ksby:20200718204559p:plain

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

f:id:ksby:20200718204715p:plain f:id:ksby:20200718204822p:plain

ksbysample-resize-bucket-prod バケットに sample_thumb.jpg が生成されました。

f:id:ksby:20200718204929p:plain f:id:ksby:20200718205012p:plain

Pillow を使用したリサイズ処理も問題なく動作しています。

何も変更せずに deploy し直すと cache の効果でどのくらい時間が短縮されるのか?

.circleci/config.yml 内で1行改行してから revert して master ブランチに commit&push してみると、前回と比較して deploy 時間が約半分になりました。

f:id:ksby:20200718210014p:plain f:id:ksby:20200718210117p:plain

ただし、この時気づきましたが Lambda Layer の shared_package_layer のバージョンが1つ上がっていました。少し試してみたのですが deploy すると変更点がなくてもバージョンが1つ上がってしまうようです。変更がなければバージョンが上がらないようにしたいのですが、CI/CD 前提ならば Lambda Layer は別レポジトリに分けた方が良いのかもしれません。。。

stages/prod/custom-shared-package-layer.yml の説明

stages/prod/custom-shared-package-layer.yml に以下のように記述しましたが、

pythonRequirements:
  # dockerizePip: true
  fileName: ../../requirements.txt
  noDeploy:
    - aws-lambda-context
    - boto3
    - moto
  useStaticCache: true
  useDownloadCache: true
  cacheLocation: /root/project/.cache
  staticCacheMaxVersions: 3
  layer:
    name: "shared-package-layer-${env:STAGE}"
    description: 共通パッケージ用 Lambda Layer
  • デフォルトの cacheLocation ではエラーになるため、CircleCI の working_directory の下のディレクトリを指定します。
  • useStaticCache、useDownloadCache を true にして cache を使うよう設定します。
  • serverless-python-requirements プラグインの cache を CircleCI の cache 機能で cache するので、staticCacheMaxVersions で履歴数を少なめに指定してサイズが大きくならないようにします。

CircleCI で deploy したリソース一式を削除する

stages/prod/.envrc で AWS_PROFILE に削除可能な profile を設定しておいてから、以下のコマンドを実行すれば削除されます。

$ cd stages/prod
$ npm run remove:all

f:id:ksby:20200719004105p:plain f:id:ksby:20200719004157p:plain f:id:ksby:20200719004308p:plain

履歴

2020/07/19
初版発行。

Serverless Framework で deploy 用ディレクトリへ移動→環境変数を設定する方法で deploy する環境を切り替える(その2)

概要

記事一覧はこちらです。

Serverless Framework で deploy 用ディレクトリへ移動→環境変数を設定する方法で deploy する環境を切り替える(その1) の続きです。

参照したサイト・書籍

目次

  1. deploy/remove 用の npm-scripts を記述する
  2. stages/dev に移動して開発環境(dev)に deploy する
  3. 動作確認する(dev)
  4. stages/stg に移動してステージング環境(stg)に deploy する
  5. 動作確認する(stg)

手順

deploy/remove 用の npm-scripts を記述する

package.json に deploy/remove 用の npm-scripts を記述します。

{
  "name": "ksbysample-serverless-deploy",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "deploy-service": "per-env",
    "deploy-service:development": "cross-env-shell \"cd $SERVICE_DIR && aws-vault exec $AWS_PROFILE -- bash -c \\\"npx sls deploy -v\\\"\"",
    "deploy-service:ci": "cross-env-shell \"cd $SERVICE_DIR && npx sls deploy -v\"",
    "remove-service": "per-env",
    "remove-service:development": "cross-env-shell \"cd $SERVICE_DIR && aws-vault exec $AWS_PROFILE -- bash -c \\\"npx sls remove -v\\\"\"",
    "remove-service:ci": "cross-env-shell \"cd $SERVICE_DIR && npx sls remove -v\"",
    "deploy:shared-package-layer": "cross-env SERVICE_DIR=layers/shared_package_layer run-s deploy-service",
    "remove:shared-package-layer": "cross-env SERVICE_DIR=layers/shared_package_layer run-s remove-service",
    "deploy:image-service": "cross-env SERVICE_DIR=services/image_service run-s deploy-service",
    "remove:image-service": "cross-env SERVICE_DIR=services/image_service run-s remove-service",
    "deploy:sample-service": "cross-env SERVICE_DIR=services/sample_service run-s deploy-service",
    "remove:sample-service": "cross-env SERVICE_DIR=services/sample_service run-s remove-service",
    "deploy:layers": "run-p deploy:shared-package-layer",
    "remove:layers": "run-p remove:shared-package-layer",
    "deploy:services": "run-s deploy:image-service deploy:sample-service",
    "remove:services": "run-s remove:image-service remove:sample-service",
    "deploy:all": "run-s deploy:layers deploy:services",
    "remove:all": "run-s remove:services remove:layers"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ksby/ksbysample-serverless-deploy.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/ksby/ksbysample-serverless-deploy/issues"
  },
  "homepage": "https://github.com/ksby/ksbysample-serverless-deploy#readme",
  "devDependencies": {
    "cross-env": "^7.0.2",
    "npm-run-all": "^4.1.5",
    "per-env": "^1.0.2",
    "serverless": "^1.74.1",
    "serverless-python-requirements": "^5.1.0"
  }
}

deploy:all タスクのフローは以下のようになります。

f:id:ksby:20200714231201p:plain

  • eploy:shared-package-layerdeploy:image-servicedeploy:sample-service では cross-env で環境変数 SERVICE_DIR に移動先のディレクトリのパスをセットして deploy-service タスクを呼び出します。

    ポイントとしては npm-scripts は stages/dev の下に cd してから実行しても実行時のディレクトリはプロジェクトのルートディレクトリ(node_modules のあるディレクトリ)になるので、環境変数 SERVICE_DIR に設定するパスはそこからの相対パスにする必要があるという点です。

    試しに deploy-service:development タスクを "deploy-service:development": "cross-env-shell \"ls -l\"" に変更してから npm run deploy:all を実行してみると、deploy-service:development タスクが呼び出される度にプロジェクトのルートディレクトリにある node_modules ディレクトリが表示されています。

    f:id:ksby:20200714232915p:plain f:id:ksby:20200714233026p:plain

  • deploy-service タスクが呼び出されると per-env により NODE_ENV の値が未セットの場合(deployment になる)には deploy-service:development タスクが、NODE_ENV=ci がセットされている場合には deploy-service:ci タスクが呼び出されます。

  • deploy-service:development 及び deploy-service:ci タスクでは cross-env-shell(cross-env に含まれている)で cd 及び sls deploy を実行します。cross-env-shell で実行することで git-bash や cross-env で設定された環境変数cd 及び sls deploy 側で利用できるようになります。

stages/dev に移動して開発環境(dev)に deploy する

stages/dev に移動してから npm run deploy:all を実行します。

f:id:ksby:20200716232617p:plain f:id:ksby:20200716232826p:plain f:id:ksby:20200716232925p:plain f:id:ksby:20200716233017p:plain f:id:ksby:20200716233118p:plain f:id:ksby:20200716233224p:plain

作成されたリソースを確認すると全てに STAGE 名である dev が入っています。

■Lambda 関数 f:id:ksby:20200716233907p:plain

■Lambda レイヤー f:id:ksby:20200716234028p:plain

■IAM ロール f:id:ksby:20200716234209p:plain

■CloudWatch ロググループ f:id:ksby:20200716234315p:plain

■S3 バケット f:id:ksby:20200716234452p:plain f:id:ksby:20200716234605p:plain

■CloudFormation スタック f:id:ksby:20200716234745p:plain

API Gateway f:id:ksby:20200716234840p:plain

■SQS キュー f:id:ksby:20200716235001p:plain

■DynamoDB テーブル f:id:ksby:20200716235124p:plain

動作確認する(dev)

Postman から API Gateway の endpoint にアクセスすると 200 OK が返ってきて、

f:id:ksby:20200716235549p:plain

DynamoDB の sample-table-dev テーブルにデータが保存されました。

f:id:ksby:20200716235723p:plain

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

f:id:ksby:20200717000041p:plain f:id:ksby:20200717000134p:plain

ksbysample-resize-bucket-dev バケットに sample_thumb.jpg が生成されました。

f:id:ksby:20200717000245p:plain f:id:ksby:20200717000323p:plain

S3 --> Lambda --> S3 の処理をする時の X-Ray を見てみると以下のように表示されていました。

f:id:ksby:20200717000955p:plain

問題なく動作しています。

stages/stg に移動してステージング環境(stg)に deploy する

stages/dev で deploy したリソースは残したままの状態で stages/stg に移動して deploy します。

f:id:ksby:20200717064054p:plain (..........途中省略..........) f:id:ksby:20200717064155p:plain

作成されたリソースを確認すると全てに STAGE 名である stg が入っていました(キャプチャは省略)。

動作確認する(stg

Postman から API Gateway の endpoint にアクセスすると 200 OK が返ってきて、

f:id:ksby:20200717064948p:plain

DynamoDB の sample-table-stg テーブルにデータが保存されました。

f:id:ksby:20200717065104p:plain

ksbysample-upload-bucket-stg バケットに sample2.jpg をアップロードすると、

f:id:ksby:20200717065229p:plain f:id:ksby:20200717065309p:plain

ksbysample-resize-bucket-stg バケットに sample2_thumb.jpg が生成されました。

f:id:ksby:20200717065412p:plain f:id:ksby:20200717065455p:plain

dev のリソースがある状態でも stg も問題なく動作しました。

履歴

2020/07/19
初版発行。

Serverless Framework で deploy 用ディレクトリへ移動→環境変数を設定する方法で deploy する環境を切り替える(その1)

概要

記事一覧はこちらです。

開発環境(dev)、ステージング環境(stg)、本番環境(prod)を異なるアカウント、あるいは同一アカウント内でリソース名を切り替えて deploy する方法を記述します。

Terraform の場合、適用先の環境用ディレクトリに移動してから terraform apply コマンドを実行するようにしていて(direnv を利用して .envrc に記述した AWS_PROFILE 等の環境変数を設定します)、

root_directory
└ stages
  ├ dev   ← ここに移動してから terraform apply すれば開発環境に適用される
  │  └ .envrc
  ├ stg   ← ここに移動してから terraform apply すればステージング環境に適用される
  │  └ .envrc
  └ prod  ← ここに移動してから terraform apply すれば本番環境に適用される
      └ .envrc

個人的にこの方法がやりやすいので、Serverless Framework でも同じ方法で deploy できないかと思い考えてみました。

今回は CircleCI から deploy する方法も記述するので、新規の Repository を作成します。 https://github.com/ksby/ksbysample-serverless-deploy

ローカルPC から開発環境(dev)、ステージング環境(stg)を deploy する方法を2回に分けて記述し、CircleCI から本番環境(prod)に deploy する方法をその後に1回で記述します。

参照したサイト・書籍

  1. AWS - Credentials
    https://www.serverless.com/framework/docs/providers/aws/guide/credentials/

  2. cross-env
    https://www.npmjs.com/package/cross-env

  3. npm-run-all
    https://www.npmjs.com/package/npm-run-all

  4. per-env
    https://www.npmjs.com/package/per-env

  5. aws-lambda-context
    https://pypi.org/project/aws-lambda-context/

目次

  1. 概要
  2. clone してプロジェクトを設定する
  3. npm から cross-env、npm-run-all、per-env をインストールする
  4. Python で使用するモジュールを pip でインストールし、requirements.txt を作成する
  5. リリース用フォルダ(stages/dev、stages/stg、stages/prod)を作成し .envrc と serverless.yml の custom セクション用 yaml ファイルを作成する
  6. Lambda Layer 用の layers/shared_package_layer を作成し serverless.yml を変更する
  7. services/image_service を作成し serverless.yml を変更する
  8. services/sample_service を作成し serverless.yml を変更する
  9. ユニットテストを作成する
  10. 続く。。。

手順

概要

  • deploy 先の環境用ディレクトリへ移動してから npm-scripts(内部で sls deploy を呼び出す)を実行することで deploy する。例えば開発環境(dev)へ deploy する場合には以下のコマンドを実行する。
> cd stages/dev
  ※direnv により環境変数 AWS_PROFILE、AWS_DEFAULT_REGION、STAGE が設定される。
> npm run deploy:all
  • CircleCI から deploy する場合には管理画面か .circleci/config.yml で環境変数 AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_DEFAULT_REGION、STAGE を設定しておいて、以下のコマンドを実行する。
> NODE_ENV=ci npm run deploy:all
  • STAGE は開発環境(dev)、ステージング環境(stg)、本番環境(prod)の3つとする。
  • 開発環境(dev)、ステージング環境(stg)はローカルPC から、本番環境(prod)は CircleCI から deploy する。
  • ローカルPC から deploy する時には aws-vault exec $AWS_PROFILE -- bash -c "npx sls deploy -v" コマンドを、CircleCI から deploy する時には npx sls deploy -v コマンドを実行したいので、NODE_ENV の値を見て npm-scripts を切り替えられる per-env を使用する。
  • serverless.yml の provider.profile は記述しない。sls deploy--aws-profile オプションも使用しない。
  • Lambda Layer x 1、Service x 2(sample-service、image-service)を作成する。以下の目的である。
    • Lambda Layer を利用する複数の Service を deploy するサンプルを作成する。
    • OS 依存のバイナリがある Pillow を含む deploy をしても正常に動作するサンプルを作成する。

f:id:ksby:20200713234022p:plain:w450

  • Python で使用するモジュール(aws-lambda-powertools、Pillow)は全て Lambda Layer にインストールする。全ての Lambda 関数はこの Lambda Layer を使用する。
  • deploy は直接 sls deploy コマンドを実行するのではなく npm-scripts 経由で実行する。
  • git-bash で設定された環境変数を npm-scripts から呼び出された sls deploy で利用できるようにするために cross-env を使用する。
  • Lambda 関数の実装で aws-lambda-powertools を使用する。
  • ユニットテストも作成する。
  • aws-lambda-powertools を利用した実装でユニットテストを作成すると引数 context に必要な値がセットされたオブジェクトを渡す必要があるので、aws-lambda-context を使用する。
  • requirements.txt は Lambda Layer や Service 用のディレクトリの下ではなくプロジェクトのルートディレクトリ直下に作成する。

clone してプロジェクトを設定する

https://github.com/ksby/ksbysample-serverless-deploy の repository を clone して必要な設定を行います。

具体的な手順は IntelliJ IDEA+Node.js+npm+serverless framework+Python の組み合わせで開発環境を構築して AWS Lambda を作成してみる 参照。

  • .gitignore を https://github.com/ksby/ksbysample-serverless からコピーする。
  • Python の仮想環境を作成する。
  • Serverless Framework をインストールする。
    • npm init -y
    • npm install --save-dev serverless
  • serverless-python-requirements をインストールする。
    • npm install --save-dev serverless-python-requirements

npm から cross-env、npm-run-all、per-env をインストールする

npm で今回使用する cross-envnpm-run-allper-env のパッケージをインストールします。

cross-env は npm-scripts 内で環境変数を設定したり、git-bash に設定されている環境変数を npm-scripts に渡すために使用します。

npm-run-all は npm-scripts を sequential あるいは parallel に実行するために使用します。ただし run-p を使って service の deploy を parallel に実行しようと思ったのですがエラーが出てダメでした。。。

per-env は npm-scripts を実行する時に設定されている NODE_ENV(何も設定していなければ development になる)の値により実際に実行される npm-scripts を切り替えるために使用します。

  • npm install --save-dev cross-env
  • npm install --save-dev npm-run-all
  • npm install --save-dev per-env

f:id:ksby:20200712112110p:plain f:id:ksby:20200712112306p:plain f:id:ksby:20200712112401p:plain

Python で使用するモジュールを pip でインストールし、requirements.txt を作成する

実装に必要なモジュールとして aws-lambda-powertools、boto3、Pillow をインストールします。

  • pip install aws-lambda-powertools
  • pip install boto3
  • pip install Pillow

ユニットテストで使用するモジュールとして aws-lambda-context、moto をインストールします。

  • pip install aws-lambda-context
  • pip install moto

最後にプロジェクトのルートディレクトリ直下に requirements.txt を作成し、インストールしたモジュールを全て記述します。

aws-lambda-context==1.1.0
aws-lambda-powertools==1.0.1
boto3==1.14.20
moto==1.3.14
Pillow==7.2.0

リリース用フォルダ(stages/dev、stages/stg、stages/prod)を作成し .envrc と serverless.yml の custom セクション用 yaml ファイルを作成する

プロジェクトのルートディレクトリ直下に stages ディレクトリを作成し、その下に dev、stg、prod ディレクトリを作成します。

f:id:ksby:20200712160802p:plain:w450

ディレクトリの下に .envrc、custom-services.yml、custom-shared-package-layer.yml を作成します。

  • .envrc
    • direnv が参照するファイル。設定する環境変数を記述する。
  • custom-services.yml
    • services ディレクトリの下に作成する各サービスの serverless.yml の custom セクションに読み込ませるファイル。
    • 今回は内容は全て同じ。環境毎に custom セクションに読み込ませる値を変更できることを示すためにわざと分けて書いている。
  • custom-shared-package-layer.yml
    • layers/shared_package_layer の下の serverless.yml の custom セクションに読み込ませるファイル。
    • ローカルPC から deploy する開発環境(dev)、ステージング環境(stg)と、CircleCI から deploy する本番環境(prod)で設定が異なる。

dev ディレクトリの下に .envrc、custom-services.yml、custom-shared-package-layer.yml を作成し、以下の内容を記述します。

■.envrc

export AWS_PROFILE=<deployで使用するprofile名>
export AWS_DEFAULT_REGION=ap-northeast-1
export STAGE=dev

■custom-services.yml

queueName: "sample-queue-${env:STAGE}"
tableName: "sample-table-${env:STAGE}"
uploadBucketName: "ksbysample-upload-bucket-${env:STAGE}"
resizeBucketName: "ksbysample-resize-bucket-${env:STAGE}"

■custom-shared-package-layer.yml

pythonRequirements:
  dockerizePip: true
  fileName: ../../requirements.txt
  noDeploy:
    - aws-lambda-context
    - boto3
    - moto
  layer:
    name: "shared-package-layer-${env:STAGE}"
    description: 共通パッケージ用 Lambda Layer

stg ディレクトリの下に .envrc、custom-services.yml、custom-shared-package-layer.yml を作成し、以下の内容を記述します。

■.envrc

export AWS_PROFILE=<deployで使用するprofile名>
export AWS_DEFAULT_REGION=ap-northeast-1
export STAGE=stg

■custom-services.yml
※dev と同じ。

■custom-shared-package-layer.yml
※dev と同じ。

prod ディレクトリの下に .envrc、custom-services.yml、custom-shared-package-layer.yml を作成し、以下の内容を記述します。custom-shared-package-layer.yml の dev、stg ディレクトリ版との違いは CircleCI の deploy を記述する回で説明します。

■.envrc

export AWS_PROFILE=<deployで使用するprofile名>
export AWS_DEFAULT_REGION=ap-northeast-1
export STAGE=prod

■custom-services.yml
※dev と同じ。

■custom-shared-package-layer.yml

pythonRequirements:
  # dockerizePip: true
  fileName: ../../requirements.txt
  noDeploy:
    - aws-lambda-context
    - boto3
    - moto
  useStaticCache: true
  useDownloadCache: true
  cacheLocation: /root/project/.cache
  staticCacheMaxVersions: 3
  layer:
    name: "shared-package-layer-${env:STAGE}"
    description: 共通パッケージ用 Lambda Layer

Lambda Layer 用の layers/shared_package_layer を作成し serverless.yml を変更する

プロジェクトのルートディレクトリ直下に layers ディレクトリを作成し、git-bash から layers ディレクトリの下へ移動した後 npx sls create --template aws-python3 --path shared_package_layer を実行します。

f:id:ksby:20200712163134p:plain

handler.py は不要なので削除します。

serverless.yml には以下の内容を記述します。

service: shared-package-layer

plugins:
  - serverless-python-requirements

custom: ${file(../../stages/${env:STAGE}/custom-shared-package-layer.yml)}

provider:
  name: aws
  runtime: python3.8
  stage: ${env:STAGE}
  region: ${env:AWS_DEFAULT_REGION}

resources:
  Outputs:
    # 他の Stack から Lambda Layer を参照できるようにする
    # Value に記載している "PythonRequirementsLambdaLayer" はこの文字列固定である
    SharedPackageLayer:
      Value:
        Ref: PythonRequirementsLambdaLayer

services/image_service を作成し serverless.yml を変更する

プロジェクトのルートディレクトリ直下に services ディレクトリを作成します。

git-bash から services ディレクトリの下へ移動した後 npx sls create --template aws-python3 --path image_service を実行します。

f:id:ksby:20200712163951p:plain

handler.py → s3_handler.py にリネームした後、https://github.com/ksby/ksbysample-serverless-deploy/blob/master/services/image_service/s3_handler.py の内容を記述します(今回の本題ではないのでコードは載せません)。

serverless.yml には以下の内容を記述します。

service: image-service

custom: ${file(../../stages/${env:STAGE}/custom-services.yml)}

provider:
  name: aws
  runtime: python3.8
  stage: ${env:STAGE}
  region: ${env:AWS_DEFAULT_REGION}
  environment:
    # aws-lambda-powertools 用環境変数
    LOG_LEVEL: DEBUG
    POWERTOOLS_LOGGER_LOG_EVENT: false
    POWERTOOLS_METRICS_NAMESPACE: serverless-deploytest-project
    POWERTOOLS_SERVICE_NAME: image-service
  tracing:
    lambda: true

  iamRoleStatements:
    - Effect: Allow
      Action:
        - s3:GetObject
      Resource:
        - "arn:aws:s3:::${self:custom.uploadBucketName}/*"
    - Effect: Allow
      Action:
        - s3:PutObject
      Resource:
        - "arn:aws:s3:::${self:custom.resizeBucketName}/*"

functions:
  resize:
    handler: s3_handler.resize
    environment:
      RESIZE_BUCKET_NAME: ${self:custom.resizeBucketName}
    events:
      - s3: ${self:custom.uploadBucketName}
    layers:
      - ${cf:shared-package-layer-${env:STAGE}.SharedPackageLayer}

resources:
  Resources:
    KsbysampleResizeBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.resizeBucketName}

services/sample_service を作成し serverless.yml を変更する

git-bash から services ディレクトリの下へ移動した後 npx sls create --template aws-python3 --path sample_service を実行します。

f:id:ksby:20200712193847p:plain

handler.py → apigw_handler.py にリネームした後、https://github.com/ksby/ksbysample-serverless-deploy/blob/master/services/sample_service/apigw_handler.py の内容を記述します。

sqs_handler.py を作成した後、https://github.com/ksby/ksbysample-serverless-deploy/blob/master/services/sample_service/sqs_handler.py の内容を記述します。

serverless.yml には以下の内容を記述します。

service: sample-service

custom: ${file(../../stages/${env:STAGE}/custom-services.yml)}

provider:
  name: aws
  runtime: python3.8
  stage: ${env:STAGE}
  region: ${env:AWS_DEFAULT_REGION}
  environment:
    # aws-lambda-powertools 用環境変数
    LOG_LEVEL: INFO
    POWERTOOLS_LOGGER_LOG_EVENT: false
    POWERTOOLS_METRICS_NAMESPACE: serverless-deploytest-project
    POWERTOOLS_SERVICE_NAME: sample-service
    # service 固有の設定
    QUEUE_URL: !Ref SampleQueue
    TABLE_NAME: ${self:custom.tableName}
  tracing:
    apiGateway: true
    lambda: true

  iamRoleStatements:
    - Effect: Allow
      Action:
        - sqs:*
      Resource:
        - Fn::GetAtt: [ SampleQueue, Arn ]
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - Fn::GetAtt: [ SampleTable, Arn ]

functions:
  hello:
    handler: apigw_handler.hello
    events:
      - http:
          path: hello
          method: get
          cors: true
    layers:
      - ${cf:shared-package-layer-${env:STAGE}.SharedPackageLayer}

  saveTable:
    handler: sqs_handler.save_table
    events:
      - sqs:
          arn:
            Fn::GetAtt: [ SampleQueue, Arn ]
    layers:
      - ${cf:shared-package-layer-${env:STAGE}.SharedPackageLayer}

resources:
  Resources:
    SampleQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: "${self:custom.queueName}"

    SampleTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: "${self:custom.tableName}"
        AttributeDefinitions:
          - AttributeName: timestamp
            AttributeType: S
        KeySchema:
          - AttributeName: timestamp
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

ユニットテストを作成する

プロジェクトのルートディレクトリ直下に tests ディレクトリを作成し、その下に common、image_service、sample_service ディレクトリを作成します。最終的には以下のディレクトリ・ファイル構成になります。

f:id:ksby:20200712195957p:plain:w450

プロジェクトのルートディレクトリ直下に .envrc を作成し、以下の内容を記述します。

export LOG_LEVEL=ERROR
export POWERTOOLS_TRACE_DISABLED=true
  • ログの出力を抑制したいので LOG_LEVEL は ERROR に設定します。
  • aws-lambda-powertools の Tracer を使用しているとそのままではユニットテストが失敗するため、POWERTOOLS_TRACE_DISABLED=true を設定します。

__init__.py を tests、tests/image_service、tests/sample_service の下に作成します。

tests/common の下に test_utils.py を作成し、以下の内容を記述します。aws_lambda_context を使用して context の mock を作成するメソッドです。

from aws_lambda_context import LambdaContext


def mock_context():
    context = LambdaContext()
    context.function_name = 'test'
    context.function_version = 'test'
    context.invoked_function_arn = 'test'
    context.memory_limit_in_mb = 'test'
    context.aws_request_id = 'test'
    context.log_group_name = 'test'
    context.log_stream_name = 'test'
    return context

tests/common の下に aws_resource.py も作成し、以下の内容を記述します。setUp、tearDown で AWS リソースの mock を作成しますが、都度ユニットテストのクラスに記述するのは冗長になるのでこのファイルに必要なメソッドを記述するようにしました。

import os

import boto3

UPLOAD_BUCKET_NAME = 'ksbysample-upload-bucket'
RESIZE_BUCKET_NAME = 'ksbysample-resize-bucket'
QUEUE_NAME = 'sample-queue-test'
TABLE_NAME = 'sample-table-test'


def create_s3_bucket(self):
    s3_client = boto3.client('s3')
    s3_client.create_bucket(Bucket=UPLOAD_BUCKET_NAME)
    s3_client.create_bucket(Bucket=RESIZE_BUCKET_NAME)
    self._upload_bucket_name = UPLOAD_BUCKET_NAME
    self._resize_bucket_name = RESIZE_BUCKET_NAME


def create_sqs_queue(self):
    sqs_client = boto3.client('sqs')
    response = sqs_client.create_queue(QueueName=QUEUE_NAME)
    self._queue_url = response['QueueUrl']


def create_dynamodb_table(self):
    dynamodb_client = boto3.client('dynamodb')
    dynamodb_client.create_table(
        TableName=TABLE_NAME,
        AttributeDefinitions=[
            {
                "AttributeName": "timestamp", "AttributeType": "S"
            }
        ],
        KeySchema=[
            {
                "AttributeName": "timestamp", "KeyType": "HASH"
            }
        ],
        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
    )

    self._table_name = TABLE_NAME


def delete_s3_bucket(self):
    s3 = boto3.resource('s3')
    upload_bucket = s3.Bucket(UPLOAD_BUCKET_NAME)
    upload_bucket.objects.all().delete()
    upload_bucket.delete()
    resize_bucket = s3.Bucket(RESIZE_BUCKET_NAME)
    resize_bucket.objects.all().delete()
    resize_bucket.delete()


def delete_sqs_queue(self):
    with self.env:
        sqs_client = boto3.client('sqs')
        sqs_client.delete_queue(
            QueueUrl=os.environ['QUEUE_URL']
        )


def delete_dynamodb_table(self):
    with self.env:
        dynamodb_client = boto3.client('dynamodb')
        dynamodb_client.delete_table(TableName=os.environ['TABLE_NAME'])

tests/image_service の下に test_resize.py を作成し、以下の内容を記述します。

import json
import os
import unittest
from unittest.mock import patch

import boto3
from moto import mock_s3

from tests.common import aws_resource, test_utils


@mock_s3
class TestResizeService(unittest.TestCase):

    def setUp(self):
        aws_resource.create_s3_bucket(self)
        self.env = patch.dict('os.environ', {
            'UPLOAD_BUCKET_NAME': self._upload_bucket_name,
            'RESIZE_BUCKET_NAME': self._resize_bucket_name
        })

    def tearDown(self):
        aws_resource.delete_s3_bucket(self)

    def test_resize(self):
        with self.env:
            from services.image_service import s3_handler

            s3_client = boto3.client('s3')
            s3_client.upload_file('tests/image_service/sample.jpg',
                                  os.environ['UPLOAD_BUCKET_NAME'], 'sample.jpg')

            with open('tests/image_service/s3_event.json', 'r') as f:
                event = json.load(f)

            s3_handler.resize(event, test_utils.mock_context())

            thumb_object = s3_client.get_object(Bucket=os.environ['RESIZE_BUCKET_NAME'],
                                                Key='sample_thumb.jpg')
            self.assertEqual(thumb_object['ResponseMetadata']['HTTPStatusCode'], 200)
            self.assertGreater(int(thumb_object['ResponseMetadata']['HTTPHeaders']['content-length']), 0)

            # 生成されたサムネイル画像をダウンロードすることも出来る(実際に作成される)
            # s3_client.download_file(TestResizeService.RESIZE_BUCKET, 'sample_thumb.jpg',
            #                         'tests/sample_thumb.jpg')

tests/sample_service の下に test_apigw_handler.py を作成し、以下の内容を記述します。

import json
import os
import unittest
from unittest.mock import patch

import boto3
from moto import mock_sqs

from tests.common import aws_resource, test_utils


@mock_sqs
class TestApigwHandler(unittest.TestCase):
    def setUp(self):
        aws_resource.create_sqs_queue(self)
        self.env = patch.dict('os.environ', {
            'QUEUE_URL': self._queue_url,
        })

    def tearDown(self):
        aws_resource.delete_sqs_queue(self)

    def test_hello(self):
        with self.env:
            from services.sample_service import apigw_handler

            sqs_resource = boto3.resource('sqs')
            queue = sqs_resource.Queue(os.environ['QUEUE_URL'])

            with open('tests/sample_service/apigw_event.json', encoding='utf-8', mode='r') as f:
                apigw_event = json.load(f)

            response = apigw_handler.hello(apigw_event, test_utils.mock_context())
            self.assertEqual(response['statusCode'], 200)

            messages = queue.receive_messages(QueueUrl=os.environ['QUEUE_URL'])
            self.assertEqual(len(messages), 1)
            self.assertEqual(messages[0].body, "これはテストです")

tests/sample_service の下に test_sqs_handler.py を作成し、以下の内容を記述します。

import json
import os
import unittest
from unittest.mock import patch

import boto3
from moto import mock_sqs, mock_dynamodb2

from tests.common import aws_resource, test_utils


@mock_sqs
@mock_dynamodb2
class TestSqsHandler(unittest.TestCase):
    def setUp(self):
        aws_resource.create_sqs_queue(self)
        aws_resource.create_dynamodb_table(self)
        self.env = patch.dict('os.environ', {
            'QUEUE_URL': self._queue_url,
            'TABLE_NAME': self._table_name,
        })

    def tearDown(self):
        aws_resource.delete_sqs_queue(self)
        aws_resource.delete_dynamodb_table(self)

    def test_save_table(self):
        with self.env:
            from services.sample_service import sqs_handler

            dynamodb_sample_table_tbl = boto3.resource('dynamodb').Table(os.environ['TABLE_NAME'])

            with open('tests/sample_service/sqs_event.json', encoding='utf-8', mode='r') as f:
                sqs_event = json.load(f)

            sqs_handler.save_table(sqs_event, test_utils.mock_context())

            items = dynamodb_sample_table_tbl.scan()['Items']
            self.assertEqual(len(items), 1)
            self.assertEqual(items[0]['message'], "これはテストです")

python -m unittest -v を実行してユニットテストが成功することを確認します。.envrc でユニットテストに必要な環境変数を設定しているので git-bash 上で実行します。

f:id:ksby:20200712201038p:plain

IntelliJ IDEA 上でもユニットテストが成功するようにします。メインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示した後、「Templates」-「Python tests」-「Unittests」を選択して以下の画像の赤枠の部分を設定します。

f:id:ksby:20200712201942p:plain

Project Tool Window 上で tests ディレクトリを選択してコンテキストメニューを表示してから「Run 'Unittests in tests'」を選択してユニットテストを実行し、成功することを確認します。

f:id:ksby:20200712202132p:plain

続く。。。

次回は npm-scripts を定義した後、ローカルPC から開発環境(dev)、ステージング環境(stg)を deploy して動作確認します。

履歴

2020/07/19
初版発行。