Thursday, March 14, 2019

Google Cloud Platform



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

https://start.spring.io/  ß Use Full Version and choose “War” from Packaging option.
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


Run app from

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