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.
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
,
spring.datasource.url: @spring.datasource.url@
spring.datasource.username: @spring.datasource.username@
spring.datasource.password: @spring.datasource.password@
spring.main.banner-mode: OFF
src/main/webapp/WEB-INF/web.xml
,
<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<security-constraint>
<web-resource-collection>
<web-resource-name>HTTPS Redirect</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
</web-app>
and src/main/webapp/WEB-INF/logging.properties
:
# Set the default logging level for all loggers to WARNING
.level = WARNING
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:
<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<application>@project.groupId@</application>
<service>@project.artifactId@</service>
<version>@project.version@</version>
<runtime>java8</runtime>
<basic-scaling>
<max-instances>1</max-instances>
<idle-timeout>5m</idle-timeout>
</basic-scaling>
<instance-class>B1</instance-class>
<threadsafe>true</threadsafe>
<sessions-enabled>true</sessions-enabled>
<env-variables>
<env-var name="SPRING_PROFILES_ACTIVE" value="gcp"/>
</env-variables>
<system-properties>
<property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
</system-properties>
</appengine-web-app>
which has the following features (in addition to specifying scaling):
- Sets the
SPRING_PROFILES_ACTIVE
environment variable togcp
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
:
# Configured from src/main/webapp/WEB-INF/appengine-web.xml
---
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:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<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>
<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>
<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>
<properties>
<start-class>com.example.Launcher</start-class>
</properties>
<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>
<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>
<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>
<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>
<profiles>
<profile>gcp</profile>
</profiles>
</configuration>
</plugin>
</plugins>
</build>
</project>
References
[1] Exercise left to the reader. ↩
Top comments (0)