Install App Engine extension for Java:
SJCMACJ15JHTD8:~
jzeng$ gcloud components install app-engine-java
Install App Engine extension for Python:
SJCMACJ15JHTD8:~
jzeng$ gcloud components install app-engine-python
Before using IntelliJ IDEA, use following link to go through
all command line exercises to make sure environment is correct:
There is a Spring Boot version of Hello World from above link. To make it work, need following two manual
works:
1. add following section to pom.xml:
<build>
<plugins>
<plugin>
<groupId>com.google.appengine</groupId>
<artifactId>appengine-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2. add appengine-web.xml under WEB-INFO folder with
following content:
SJCMACJ15JHTD8:demoGAESpringBoot2 jzeng$ cat target/demoGAESpringBoot2-0.0.1-SNAPSHOT/WEB-INF/appengine-web.xml
<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app
xmlns="http://appengine.google.com/ns/1.0">
<application>johnzeng-project</application><!-- unused
for Cloud SDK based tooling -->
<version>alpha-001</version><!-- unused for Cloud SDK
based tooling -->
<threadsafe>true</threadsafe>
<runtime>java8</runtime>
</appengine-web-app>
3. Following dependency is not necessary:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
Some troubleshooting suggestions:
Another good website for creating and deploying Spring Boot
application to GAE standard (although I did not use it):
Create Spring Boot
App project
Run “mvn appengine:run” and expect failure.
Go through above 2 manual steps.
Rerun “mvn appengine:run” and “mvn appengine:deploy”. This should work now.
Run or Deploy from
command window:
Run: mvn appengine:run
Deploy to GAE: mvn appengine:deploy
(When running from command line, the version is controlled
by appengine-web.xml. When running the
same app from IDEA, IDEA controls the version automatically)
Troubleshooting:
If there is some weird error message such as “unable to find
main function”, do a “mvn clean”.
Run or Deploy from
IntelliJ IDEA:
Open the pom.xml file from IDEA.
Run and debug an App Engine Standard App locally:
From IntelliJ IDEA 2017 or higher, Ultimate edition: Install
“Google Cloud Tools” plugin. After
that, you should see “Tools”/”Google Cloud Tools” menu. From it, choose
After that, you can “Deploy to Google App Engine” from
IntelliJ IDEA
Once a project is run from IDEA, it will change the
appengine-web.xml (because IDEA will control the application and version) to:
SJCMACJ15JHTD8:WEB-INF jzeng$ cat
appengine-web.xml
<?xml version="1.0"
encoding="utf-8"?>
<appengine-web-app
xmlns="http://appengine.google.com/ns/1.0">
<threadsafe>true</threadsafe>
<runtime>java8</runtime>
</appengine-web-app>
This means I have to manually change it back if I want to
deply the App from command line.
PyCharm Professional edition has a similar way to push
project to GAE.
Setup credentials to
deploy GAE application to GAE:
GCP
client libraries use a strategy called Application Default Credentials (ADC) to
find your application's credentials. When your code uses a client library, the
strategy checks for your credentials in the following order:
1.
First, ADC checks to see if the environment variable
GOOGLE_APPLICATION_CREDENTIALS
is
set. If the variable is set, ADC uses the service account file that the
variable points to. The next section describes
how to set the environment variable.
2.
If the environment variable isn't set, ADC uses the default
service account that Compute Engine, Kubernetes Engine, App Engine, and Cloud
Functions provide, for applications that run on those services.
3.
If ADC can't use either of the above credentials, an error
occurs.
Need manually to add appengine-web.xml file under WEB-INFO
directory even I used IntelliJ IDEA to create Spring Boot project for Google
App Engine standard:
Configure MySQL for Google Appengine (Standard):
When above link is right on which files to change, the
change to pom.xml is wrong. It says to
add following dependency:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-sql</artifactId>
<version>1.0.0.M1</version>
</dependency>
But it will cause following error when running test case:
java.lang.ClassNotFoundException:
org.springframework.cloud.gcp.core.AbstractCredentialsProperty
Actually, we need to remove it and add these dependencies:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gcp-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud.sql</groupId>
<artifactId>mysql-socket-factory</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!--
protobuf-java:2.6.0 included by MySQL Connector J is not compatible with
Google Cloud Java libraries. -->
<exclusions>
<exclusion>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
</exclusion>
</exclusions>
</dependency>
This is the right link:
Enable Cloud SQL API:
Assign permission to allow serviceaccountjohn to access
Cloud SQL through “IAM & admin” console:
This is the link which guided me to create a google Spring
Boot GAE Standard app with Cloud SQL enabled:
But it does not work neither.
Finally, this configuration works for me:
1.
Add following into applicaton-mysql.properties:
database=mysql
spring.datasource.username=root spring.datasource.password=welcome
# This will automatically create database table
spring.jpa.hibernate.ddl-auto=update
# Uncomment this the first time the app runs spring.datasource.initialization-mode=always
# Following 2 lines are for google cloud spring.cloud.gcp.sql.database-name=johnzeng_db spring.cloud.gcp.sql.instance-connection-name=johnzeng-project:us-west2:johnzeng-mysql
2.
Add following into application.properties:
# To use mysql spring.profiles.active=mysql
# Following properties are for logging only
#show sql statement logging.level.org.hibernate.SQL=debug #show sql values logging.level.org.hibernate.type.descriptor.sql=trace logging.level.org.springframework.web=DEBUG logging.level.org.hibernate=DEBUG
3.
Manually copy appengine-web.xml from an existing
project to:
~/spring-petclinic/target/spring-petclinic-2.1.0.BUILD-SNAPSHOT/WEB-INF/.
~/spring-petclinic/src/main/webapp/WEB-INFO/
<?xml version="1.0" encoding="utf-8"?> <appengine-web-app xmlns="http://appengine.google.com/ns/1.0"> <threadsafe>true</threadsafe> <runtime>java8</runtime> </appengine-web-app>
4.
pom.xml
<?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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>demoGAESpringBoot2</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <name>demoGAESpringBoot2</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-gcp-starter</artifactId> </dependency> <!-- Following two dependencies are from https://docs.spring.io/spring-cloud-gcp/docs/1.0.0.RELEASE/reference/htmlsingle/#_dependency_management --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-gcp-dependencies</artifactId> <version>1.0.0.RELEASE</version> <type>pom</type> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-gcp-starter-sql-mysql</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>4.2.0.Final</version> </dependency> --> <!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.google.cloud.sql</groupId> <artifactId>mysql-socket-factory</artifactId> </dependency> --> <!-- Following dependency caused classNotFound exception <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-gcp-starter-sql</artifactId> <version>1.0.0.M1</version> </dependency> --> <!-- <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> protobuf-java:2.6.0 included by MySQL Connector J is not compatible with Google Cloud Java libraries. <exclusions> <exclusion> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> </exclusion> </exclusions> </dependency>--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- <dependency> <groupId>com.google.cloud</groupId> <artifactId>google-cloud-logging</artifactId> </dependency> --> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.google.cloud</groupId> <artifactId>google-cloud-bom</artifactId> <version>0.80.0-alpha</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>com.google.appengine</groupId> <artifactId>appengine-maven-plugin</artifactId> <version>1.9.71</version> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories> </project>
The working code includes 4 java classes:
UserRepository.java:
package com.example.demoGAESpringBoot2; // This will be AUTO IMPLEMENTED by Spring into a Bean called userRepository // CRUD refers Create, Read, Update, Delete // import org.springframework.data.repository.CrudRepository; public interface UserRepository extends CrudRepository<User, Integer> { }
User.java:
package com.example.demoGAESpringBoot2; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity // This tells Hibernate to make a table out of this class public class User { @Id @GeneratedValue(strategy= GenerationType.IDENTITY) private Integer id; private String name; private String email; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
ServletInitializer.java:
package com.example.demoGAESpringBoot2; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(DemoGaeSpringBoot2Application.class); } }
DemoGaeSpringBoot2Application.java:
package com.example.demoGAESpringBoot2; import com.google.common.base.Stopwatch; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import java.sql.PreparedStatement; import static com.oracle.jrockit.jfr.ContentType.Timestamp; @SpringBootApplication @RestController @RequestMapping("/") public class DemoGaeSpringBoot2Application { /** * Read parameters from application.properties file */ @Value("${spring.cloud.gcp.sql.database-name}") private String dbName; @Value("{spring.cloud.gcp.sql.instance-connection-name}") private String instanceConnName; @Value("spring.cloud.gcp.sql.database-type") private String dbType; @Value("{spring.datasource.username}") private String dbUser; @Value("{spring.datasource.password}") private String dbPassword; /** * Get the bean called userRepository which is auto-generated by Spring. * We will use it to handle the data. */ @Autowired private UserRepository userRepository; /** * Endpoints: */ @RequestMapping(value = "/john") @ResponseBody public String helloJohn() { return "Hello John!"; } @RequestMapping(value = {"", "default", "/default*"}, produces = "text/plain;charset=UTF-8") @ResponseBody public String home() { return "Hello Stranger! Please do one of following operations \n" + "1. Greating your self: /name?name={your_name} \n" + "2. Add a user to Cloud SQL table: /add?name={user_name}&email={user_email} \n" + "3. Read all users from the Cloud SQL table: /getAll"; } @RequestMapping(value = "name") public String name(@RequestParam("name") String name) { return "Your name is " + name; } @RequestMapping(value = "db") public String db() { return "dbName is " + dbName; } @RequestMapping(value = "add") public String addUser(@RequestParam String name, @RequestParam String email) { // return "name=" + name + ", email=" + email; User user = new User(); user.setName(name); user.setEmail(email); userRepository.save(user); return "User " + name + " is saved"; } @RequestMapping(value = "getAll") public @ResponseBody Iterable<User> getAllUsers() { return userRepository.findAll(); } /** * (Optional) App Engine health check endpoint mapping. * @see <a href="https://cloud.google.com/appengine/docs/flexible/java/how-instances-are-managed#health_checking"></a> * If your app does not handle health checks, a HTTP 404 response is interpreted * as a successful reply. */ @RequestMapping(value = "/_ah/health") public String healthy() { // Message body required though ignored return "Still surviving..."; } public static void main(String[] args) { SpringApplication.run(DemoGaeSpringBoot2Application.class, args); } }
To support Spring Security:
Add following to appengine-web.xml:
<sessions-enabled>true</sessions-enabled>
Add following to pom.xml:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>
Add following to application.properties:
# Default user name. spring.security.user.name=user # Password for the default user name. spring.security.user.password=password
After above changes, a Form will be shown to enter user/password. This is the default behavior: basic
authentication.
We may have to rebuild the project if there is a wired error
message in log such as:
Error creating
bean with name 'entityManagerFactory' defined in class path resource
[org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]:
Invocation of init method failed; nested exception is
org.hibernate.service.spi.ServiceException: Unable to create requested service
[org.hibernate.engine.jdbc.env.spi.JdbcEnvironment]
All properties we can use in application.properties file:
To disable the basic authentication, set following property
to ‘false’
security.basic.enabled=true
1.
Add following into application-mysql.properties:
spring.datasource.url=jdbc:mysql://{Cloud_SQL_Instance_public_IP}/{db_name} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
2.
Add and Save the local ip address into Cloud
SQL’s instance:
When moving laptop to different network (such as home
network), need to add its IP to above list.
Some code from web to configure customized non basic
authentication behavior, but I do not use yet:
WebSecurityConfig.java:
package com.example.demoGAESpringBoot2; //import org.springframework.beans.factory.annotation.Autowired; //import org.springframework.context.annotation.Configuration; //import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; //import org.springframework.security.config.annotation.web.builders.HttpSecurity; //import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; //import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; //import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.authentication.builders.*; import org.springframework.security.config.annotation.web.configuration.*; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @EnableWebSecurity public class WebSecurityConfig implements WebMvcConfigurer { @Bean public UserDetailsService userDetailsService() throws Exception { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build()); return manager; } }
Webhook on GCP:
Can be implemented using
Google Cloud Function + HTTP Trigger:
gcloud init: authenticate SDK tools to access GCP.
gcloud info: get configuration info
Following command will update .kube/config file on local
machine. If there is a local K8s
cluster, this will change the default cluster name and add GKE cluster info
into the same ‘config’ file. The command itself is to update .kube/config file
so ‘kubectl’ will connect to GKE to use the cluster there (i.e. authenticate
for the cluster).
gcloud
container clusters get-credentials cluster-1 --zone us-west1-a --project
panw-appservices-dev
GAE (Flex) vs GKE:
If you’re primarily focused on delivering HTTP(REST|web)-based
solutions, you would prefer to have Google manage your deployed apps, and you
don’t anticipate needing more control over ‘Pod’ deployments, App Engine is an
outstanding choice.
If you’re delivering a mixture of services that may require
non-REST|web protocols (currently also gRPC), you are willing to take more
control of your deployed apps, you require more control over ‘Pod’ deployments,
you want as-is deployments across platforms (e.g. GKE, AKS, EKS, on-premises),
you want Kubernetes faster innovation cycle, then Kubernetes is an outstanding
choice.
GAE Standard vs Flex:
Standard GAE consist of
SDK which offers possibility to use services like Datastore, Taskqueues,
Memcache, Cron etc. which are built in Standard GAE. Usage is limited to
following programming languages: Python, Go, Java and PHP and each language has
restriction of libraries you can use on GAE. Standard GAE also provides
autoscaling (creating/shutting down server instances based on traffic to your
app).
Flexible GAE provides
but also takes functionalities compared to Standard GAE:
·
no GAE SDK. you need to use client libraries for Datastore,
Taskqueues… or some other products/libraries to supplement Memcache etc.
·
you can use what ever language you want (application just needs
to listen on port 8080), application is deployed as Docker image
·
Google provides runtimes for several languages like in Standard
+ Node.js and Ruby. Languages from Standard GAE doesn’t have restriction
libraries which you can use
·
You have wider possibility of instance type selection (under the
hood those are Google Compute Engine machines) than for Standard
·
Since Flex server instances are actually GCE machines, boot time
is longer comparing to Standard
·
There has to be always minimum 1 instance live even though there
are no requests
·
Autoscaling works also
·
Depending on the application, transition from Standard to Flex
can be very painful or not worthy (mainly due to 1st point)
·
It’s cheaper
GAE standard uses webapp/WEB-INFO/appengine-web.xml
GAE flexible uses appengine/app.yaml
No comments:
Post a Comment