Google Cloud Platform App Engine Deployment Experience

This article describes some of the challenges and pecularities of deploying an annotated @SpringBootApplication application to Google Cloud Platform.

Set-Up

This article assumes that the Google Cloud SDK has been installed and has been configured. Further, this article assumes that a project has been set-up within the Google Cloud Platform Console (a fictitious www-example-com within this article).

The default Google App Engine service must be created. It may be created from the command line with gcloud app create.

WAR Maven Project

The Maven Project to create the WAR for deployment consists of the following artifacts:

www-example-com-service-default
├── pom.xml
└── src
    └── main
        ├── resources
        │   └── application-gcp.properties
        └── webapp
            └── WEB-INF
                ├── logging.properties
                └── web.xml

For simplicity, this example assumes a Spring Boot application is available as a single Maven dependency. The project POM to deploy the application is identified with groupId corresponding to the Google Cloud PROJECT_ID, artifactId corresponding to the App Engine service name, and version corresponds to the App Engine service version.

<project ...>
  <groupId>www-example-com</groupId>
  <artifactId>default</artifactId>
  <version>2018121801</version>
  <packaging>war</packaging>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.1.1.RELEASE</version>
    <relativePath/>
  </parent>
  ...
  <dependencies>
    <dependency>
      <groupId>com.example</groupId>
      <artifactId>spring-boot-application</artifactId>
      <version>1.0.0</version>
    </dependency>
    ...
  </dependencies>
  ...
</project>

Runtime dependencies should be added, also. For example, the necessary dependencies to connect to a Google mysql SQL instance would include:

  <dependencies>
    ...
    <dependency>
      <groupId>com.google.cloud.sql</groupId>
      <artifactId>mysql-socket-factory</artifactId>
      <version>1.0.11</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.13</version>
      <scope>runtime</scope>
    </dependency>
    ...
  </dependencies>

The necessary plugins to build the WAR and repackage the WAR as a Spring Boot application are:

  ...
  <properties>
    ...
    <start-class>com.example.Launcher</start-class>
    ...
  </properties>
  ...
  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-war-plugin</artifactId>
          <configuration>
            <delimiters>
              <delimiter>@</delimiter>
            </delimiters>
            <useDefaultDelimiters>false</useDefaultDelimiters>
            <webResources>
              <resource>
                <filtering>true</filtering>
                <directory>src/main/webapp</directory>
              </resource>
            </webResources>
          </configuration>
        </plugin>
        <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <executions>
            <execution>
              <goals>
                <goal>build-info</goal>
                <goal>repackage</goal>
              </goals>
            </execution>
          </executions>
          <configuration>
            <mainClass>${start-class}</mainClass>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
    ...
  </build>
  ...

The additional artifacts used in constructing the WAR include src/main/resources/application-gcp.properties,



src/main/webapp/WEB-INF/web.xml,



and src/main/webapp/WEB-INF/logging.properties:



The resulting WAR is expected to be executed with the Spring Profile “gcp” enabled. The secrets in the template may be populated by setting the corresponding Maven properties in the build.1 While the web.xml is not needed by the annotated Spring Boot Application, it is configured to redirect http traffic to https and provide session management.

The WAR may be created by executing mvn clean package.

Adjustments to Deploy to Google App Engine

The “standard” App Engine environment uses Jetty instead of Tomcat so the application dependencies need to be adjusted by adding exclusions:

  <dependencies verbose="true">
    <dependency>
      <groupId>com.example</groupId>
      <artifactId>spring-boot-application</artifactId>
      <version>1.0.0</version>
      <exclusions>
        <exclusion>
          <artifactId>commons-logging</artifactId>
          <groupId>commons-logging</groupId>
        </exclusion>
        <exclusion>
          <groupId>javax.transaction</groupId>
          <artifactId>javax.transaction-api</artifactId>
        </exclusion>
        <exclusion>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    ...
  </dependencies>

Google offers at least two Maven plugins. As of this writing, the two available plugins are:

  <build>
    <pluginManagement>
      ...
      <plugins>
        <plugin>
          <groupId>com.google.appengine</groupId>
          <artifactId>appengine-maven-plugin</artifactId>
          <version>1.9.70</version>
        </plugin>
        <plugin>
          <groupId>com.google.cloud.tools</groupId>
          <artifactId>appengine-maven-plugin</artifactId>
          <version>1.3.2</version>
        </plugin>
      </plugins>
      ...
    </pluginManagement>
    ...
  </build>

Google describes the first as “App Engine SDK-based” and provides a document on this plugin here and the second as “Cloud SDK-based” with a document on this plugin here. The Cloud SDK-based plugin also requires the app-engine-java component be installed with gcloud components install app-engine-java.

Both plugins (as with any deployment to App Engine) require a src/main/webapp/WEB-INF/appengine-web.xml which is either specified and/or generated. This application includes the following:



which has the following features (in addition to specifying scaling):

  • Sets the SPRING_PROFILES_ACTIVE environment variable to gcp to enable the Spring profile
  • Configures java.util.logging

For this demonstration, the com.google.appengine:appengine-maven-plugin is configured in a Maven profile:

  <profiles>
    ...
    <profile>
      <id>com.google.appengine:appengine-maven-plugin</id>
      <activation>
        <file><missing>${basedir}/src/main/appengine/app.yaml</missing></file>
      </activation>
      <build>
        <plugins>
          <plugin>
            <groupId>com.google.appengine</groupId>
            <artifactId>appengine-maven-plugin</artifactId>
          </plugin>
        </plugins>
      </build>
    </profile>
    ...
  </profiles>

The application may be built and deployed with mvn clean package appengine:deploy. Example output:

mvn -B clean package appengine:deploy
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< www-example-com:default >----------------------
[INFO] Building www-example-com default 1
[INFO] --------------------------------[ war ]---------------------------------
...
[INFO] --- appengine-maven-plugin:1.9.70:deploy (default-cli) @ default ---
[INFO]
[INFO] Google App Engine Java SDK - Updating Application
[INFO]
[INFO] Retrieving Google App Engine Java SDK from Maven
[INFO] Downloading from central: https://repo1.maven.org/maven2/com/google/appengine/appengine-java-sdk/1.9.70/appengine-java-sdk-1.9.70.zip
[INFO] Downloaded from central: https://repo1.maven.org/maven2/com/google/appengine/appengine-java-sdk/1.9.70/appengine-java-sdk-1.9.70.zip (183 MB at 15 MB/s)
[INFO] Updating Google App Engine Application
[INFO] Running -A www-example-com -V 1 --oauth2 update /Users/ball/www-example-com-service-default/target/default-1
Reading application configuration data...


Beginning interaction for module default...
0% Created staging directory at: '/var/folders/c5/pzywv1k91gqgvkklp5r2twx00000gn/T/appcfg6118757389943856883.tmp'
5% Scanning for jsp files.
20% Scanning files on local disk.
25% Initiating update.
28% Cloning 162 application files.
40% Uploading 6 files.
52% Uploaded 1 files.
61% Uploaded 2 files.
68% Uploaded 3 files.
73% Uploaded 4 files.
77% Uploaded 5 files.
80% Uploaded 6 files.
82% Sending batch containing 6 file(s) totaling 231KB.
84% Initializing precompilation...
90% Deploying new version.
95% Will check again in 1 seconds.
98% Will check again in 2 seconds.
99% Will check again in 4 seconds.
99% Will check again in 8 seconds.
99% Closing update: new version is ready to start serving.
99% Uploading index definitions.

Update for module default completed successfully.
Success.
Cleaning up temporary files for module default...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  48.730 s
[INFO] Finished at: 2018-12-18T19:34:33-08:00
[INFO] ------------------------------------------------------------------------

This deploys the service but does not switch the load balancer to the new service.

The com.google.cloud.tools:appengine-maven-plugin may be configured with an src/main/appengine/app.yaml:

  <profiles>
    ...
    <profile>
      <id>com.google.cloud.tools:appengine-maven-plugin</id>
      <activation>
        <file><exists>${basedir}/src/main/appengine/app.yaml</exists></file>
      </activation>
      <build>
        <plugins>
          <plugin>
            <groupId>com.google.cloud.tools</groupId>
            <artifactId>appengine-maven-plugin</artifactId>
            <configuration>
              <project>${project.groupId}</project>
              <service>${project.artifactId}</service>
              <version>${project.version}</version>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </profile>
    ...
  </profiles>

Various articles suggest that a minimal app.yaml can replace the appengine-web.xml:

runtime: java8
env: standard

However, it fails with a cryptic error message:

mvn -B clean package appengine:deploy
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< www-example-com:default >----------------------
[INFO] Building www-example-com default 2
[INFO] --------------------------------[ war ]---------------------------------
...
[INFO] --- appengine-maven-plugin:1.3.2:deploy (default-cli) @ default ---
[INFO] Staging the application to: /Users/ball/www-example-com-service-default/target/appengine-staging
[INFO] Detected App Engine flexible environment application.
Dec 18, 2018 8:16:31 PM com.google.cloud.tools.appengine.cloudsdk.CloudSdk logCommand
INFO: submitting command: /usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/gcloud app deploy --version 2 --project www-example-com
[INFO] GCLOUD: Services to deploy:
[INFO] GCLOUD:
[INFO] GCLOUD: descriptor:      [/Users/ball/www-example-com-service-default/target/appengine-staging/app.yaml]
[INFO] GCLOUD: source:          [/Users/ball/www-example-com-service-default/target/appengine-staging]
[INFO] GCLOUD: target project:  [www-example-com]
[INFO] GCLOUD: target service:  [default]
[INFO] GCLOUD: target version:  [2]
[INFO] GCLOUD: target url:      [http://www-example-com.appspot.com]
[INFO] GCLOUD:
[INFO] GCLOUD:
[INFO] GCLOUD: Beginning deployment of service [default]...
[INFO] GCLOUD: ERROR: (gcloud.app.deploy) Cannot upload file [/Users/ball/www-example-com-service-default/target/appengine-staging/default-2.war], which has size [74408302] (greater than maximum allowed size of [33554432]). Please delete the file or add to the skip_files entry in your application .yaml file and try again.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  12.260 s
[INFO] Finished at: 2018-12-18T20:16:34-08:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal com.google.cloud.tools:appengine-maven-plugin:1.3.2:deploy (default-cli) on project default: Execution default-cli of goal com.google.cloud.tools:appengine-maven-plugin:1.3.2:deploy failed: Non zero exit: 1 -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginExecutionException

This author recommends using com.google.cloud.tools:appengine-maven-plugin with a fully defined appengine-web.xml and a trivial app.yaml:



With the corresponding output:

mvn -B clean package appengine:deploy
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< www-example-com:default >-----------------------
[INFO] Building www-example-com default 3
[INFO] --------------------------------[ war ]---------------------------------
...
[INFO] --- appengine-maven-plugin:1.3.2:deploy (default-cli) @ default ---
[INFO] Staging the application to: /Users/ball/www-example-com-service-default/target/appengine-staging
[INFO] Detected App Engine standard environment application.
Dec 18, 2018 8:30:22 PM com.google.cloud.tools.appengine.cloudsdk.CloudSdk logCommand
INFO: submitting command: /Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/jre/bin/java -cp /usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/platform/google_appengine/google/appengine/tools/java/lib/appengine-tools-api.jar com.google.appengine.tools.admin.AppCfg --disable_update_check stage /Users/ball/www-example-com-service-default/target/default-3 /Users/ball/www-example-com-service-default/target/appengine-staging
[INFO] GCLOUD: Reading application configuration data...
[INFO] GCLOUD:
[INFO] GCLOUD:
[INFO] GCLOUD: Beginning interaction for module default...
[INFO] GCLOUD: 0% Scanning for jsp files.
[INFO] GCLOUD: 2018-12-18 20:30:24.932:INFO::main: Logging initialized @234ms to org.eclipse.jetty.util.log.StdErrLog
[INFO] GCLOUD: 2018-12-18 20:30:25.048:INFO:oejs.Server:main: jetty-9.4.14.v20181114; built: 2018-11-14T21:20:31.478Z; git: c4550056e785fb5665914545889f21dc136ad9e6; jvm 1.8.0_192-b12
[INFO] GCLOUD: 2018-12-18 20:30:26.627:INFO:oeja.AnnotationConfiguration:main: Scanning elapsed time=989ms
[INFO] GCLOUD: 2018-12-18 20:30:26.643:INFO:oejq.QuickStartDescriptorGenerator:main: Quickstart generating
[INFO] GCLOUD: 2018-12-18 20:30:26.658:INFO:oejsh.ContextHandler:main: Started o.e.j.q.QuickStartWebApp@685cb137{/,file:///Users/ball/www-example-com-service-default/target/appengine-staging/,AVAILABLE}
[INFO] GCLOUD: 2018-12-18 20:30:26.661:INFO:oejs.Server:main: Started @1964ms
[INFO] GCLOUD: 2018-12-18 20:30:26.666:INFO:oejsh.ContextHandler:main: Stopped o.e.j.q.QuickStartWebApp@685cb137{/,file:///Users/ball/www-example-com-service-default/target/appengine-staging/,UNAVAILABLE}
[INFO] GCLOUD: Success.
[INFO] GCLOUD: Temporary staging for module default directory left in /Users/ball/www-example-com-service-default/target/appengine-staging
Dec 18, 2018 8:30:26 PM com.google.cloud.tools.appengine.cloudsdk.CloudSdk logCommand
INFO: submitting command: /usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/gcloud app deploy --version 3 --project www-example-com
[INFO] GCLOUD: Services to deploy:
[INFO] GCLOUD:
[INFO] GCLOUD: descriptor:      [/Users/ball/www-example-com-service-default/target/appengine-staging/app.yaml]
[INFO] GCLOUD: source:          [/Users/ball/www-example-com-service-default/target/appengine-staging]
[INFO] GCLOUD: target project:  [www-example-com]
[INFO] GCLOUD: target service:  [default]
[INFO] GCLOUD: target version:  [3]
[INFO] GCLOUD: target url:      [https://www-example-com.appspot.com]
[INFO] GCLOUD:
[INFO] GCLOUD:
[INFO] GCLOUD: Beginning deployment of service [default]...
[INFO] GCLOUD: #============================================================#
[INFO] GCLOUD: #= Uploading 3 files to Google Cloud Storage                =#
[INFO] GCLOUD: #============================================================#
[INFO] GCLOUD: File upload done.
[INFO] GCLOUD: Updating service [default]...
[INFO] GCLOUD: ..............done.
[INFO] GCLOUD: Setting traffic split for service [default]...
[INFO] GCLOUD: .......done.
[INFO] GCLOUD: Stopping version [www-example-com/default/2018121803].
[INFO] GCLOUD: Sent request to stop version [www-example-com/default/2018121803]. This operation may take some time to complete. If you would like to verify that it succeeded, run:
[INFO] GCLOUD:   $ gcloud app versions describe -s default 2018121803
[INFO] GCLOUD: until it shows that the version has stopped.
[INFO] GCLOUD: Deployed service [default] to [https://www-example-com.appspot.com]
[INFO] GCLOUD:
[INFO] GCLOUD: You can stream logs from the command line by running:
[INFO] GCLOUD:   $ gcloud app logs tail -s default
[INFO] GCLOUD:
[INFO] GCLOUD: To view your application in the web browser run:
[INFO] GCLOUD:   $ gcloud app browse
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  37.537 s
[INFO] Finished at: 2018-12-18T20:30:51-08:00
[INFO] ------------------------------------------------------------------------

pom.xml

The complete pom.xml used in this example:



References

[1] Exercise left to the reader.