-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added Cloud SQL MySQL Servlet connectivity sample. #1231
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# Connecting to Cloud SQL - MySQL | ||
|
||
## Before you begin | ||
|
||
1. If you haven't already, set up a Java Development Environment (including google-cloud-sdk and | ||
maven utilities) by following the [java setup guide](https://cloud.google.com/java/docs/setup) and | ||
[create a project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project). | ||
|
||
1. Create a 2nd Gen Cloud SQL Instance by following these | ||
[instructions](https://cloud.google.com/sql/docs/mysql/create-instance). Note the connection string, | ||
database user, and database password that you create. | ||
|
||
1. Create a database for your application by following these | ||
[instructions](https://cloud.google.com/sql/docs/mysql/create-manage-databases). Note the database | ||
name. | ||
|
||
1. Create a service account with the 'Cloud SQL Client' permissions by following these | ||
[instructions](https://cloud.google.com/sql/docs/mysql/connect-external-app#4_if_required_by_your_authentication_method_create_a_service_account). | ||
Download a JSON key to use to authenticate your connection. | ||
|
||
1. Use the information noted in the previous steps: | ||
```bash | ||
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service/account/key.json | ||
export CLOUD_SQL_CONNECTION_NAME='<MY-PROJECT>:<INSTANCE-REGION>:<MY-DATABASE>' | ||
export DB_USER='my-db-user' | ||
export DB_PASS='my-db-pass' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is often bad practice. Letting folks pass it in on the mvn / gradle command line is usually considered better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added a warning to the sections with environment variables that they aren't secure with a link to KMS as a secure solution. I would prefer to avoid using mvn / gradle to configure properties because it's permanent and forces the user to rebuild rather than restart the application. We are planning on adding instructions for GKE/GCE as well, which could mean a lot of repeated steps if they have to rebuild to change instance name. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1; Definitely do not use Maven/Gradle configuration properties for runtime environment values. Those are not idiomatic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might argue that, but I'm ok with it. |
||
export DB_NAME='my_db' | ||
``` | ||
Note: Saving credentials in environment variables is convenient, but not secure - consider a more | ||
secure solution such as [Cloud KMS](https://cloud.google.com/kms/) to help keep secrets safe. | ||
|
||
## Deploying locally | ||
|
||
To run this application locally, run the following command inside the project folder: | ||
|
||
```bash | ||
mvn jetty:run | ||
``` | ||
|
||
Navigate towards `http://127.0.0.1:8080` to verify your application is running correctly. | ||
|
||
## Google App Engine Standard | ||
|
||
To run on GAE-Standard, create an AppEngine project by following the setup for these | ||
[instructions](https://cloud.google.com/appengine/docs/standard/java/quickstart#before-you-begin) | ||
and verify that | ||
[appengine-maven-plugin](https://cloud.google.com/java/docs/setup#optional_install_maven_or_gradle_plugin_for_app_engine) | ||
has been added in your build section as a plugin. | ||
|
||
|
||
### Development Server | ||
|
||
The following command will run the application locally in the the GAE-development server: | ||
```bash | ||
mvn appengine:run | ||
``` | ||
|
||
### Deploy to Google Cloud | ||
|
||
First, update `src/main/webapp/WEB-INF/appengine-web.xml` with the correct values to pass the | ||
environment variables into the runtime. | ||
kurtisvg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Next, the following command will deploy the application to your Google Cloud project: | ||
```bash | ||
mvn appengine:deploy | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
<!-- | ||
Copyright 2018 Google LLC | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
--> | ||
<project> | ||
<modelVersion>4.0.0</modelVersion> | ||
<packaging>war</packaging> | ||
<version>1.0-SNAPSHOT</version> | ||
<groupId>com.example.cloudsql</groupId> | ||
<artifactId>tabs-vs-spaces</artifactId> | ||
|
||
<!-- | ||
The parent pom defines common style checks and testing strategies for our samples. | ||
Removing or replacing it should not affect the execution of the samples in anyway. | ||
--> | ||
<parent> | ||
<groupId>com.google.cloud.samples</groupId> | ||
<artifactId>shared-configuration</artifactId> | ||
<version>1.0.10</version> | ||
</parent> | ||
|
||
<properties> | ||
<maven.compiler.target>1.8</maven.compiler.target> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about Java / JDK 11 ?? |
||
<maven.compiler.source>1.8</maven.compiler.source> | ||
<failOnMissingWebXml>false</failOnMissingWebXml> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>javax.servlet</groupId> | ||
<artifactId>javax.servlet-api</artifactId> | ||
<version>3.1.0</version> | ||
<type>jar</type> | ||
<scope>provided</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>javax.servlet</groupId> | ||
<artifactId>jstl</artifactId> | ||
<version>1.2</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>mysql</groupId> | ||
<artifactId>mysql-connector-java</artifactId> | ||
<version>8.0.11</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.google.cloud.sql</groupId> | ||
<artifactId>mysql-socket-factory-connector-j-8</artifactId> | ||
<version>1.0.11</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.zaxxer</groupId> | ||
<artifactId>HikariCP</artifactId> | ||
<version>3.1.0</version> | ||
</dependency> | ||
</dependencies> | ||
|
||
<build> | ||
<plugins> | ||
<plugin> | ||
<groupId>org.eclipse.jetty</groupId> | ||
<artifactId>jetty-maven-plugin</artifactId> | ||
<version>9.4.10.v20180503</version> | ||
<configuration> | ||
<scanIntervalSeconds>1</scanIntervalSeconds> | ||
</configuration> | ||
</plugin> | ||
<!-- Only required for AppEngine Deployments --> | ||
<plugin> | ||
<groupId>com.google.cloud.tools</groupId> | ||
<artifactId>appengine-maven-plugin</artifactId> | ||
<version>1.3.2</version> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
</project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/* | ||
* Copyright 2018 Google LLC | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.example.cloudsql; | ||
|
||
import com.zaxxer.hikari.HikariConfig; | ||
import com.zaxxer.hikari.HikariDataSource; | ||
import java.sql.Connection; | ||
import java.sql.PreparedStatement; | ||
import java.sql.SQLException; | ||
import java.util.logging.Logger; | ||
import javax.servlet.ServletContextEvent; | ||
import javax.servlet.ServletContextListener; | ||
import javax.servlet.annotation.WebListener; | ||
import javax.sql.DataSource; | ||
|
||
@WebListener("Creates a connection pool that is stored in the Servlet's context for later use.") | ||
public class ConnectionPoolContextListener implements ServletContextListener { | ||
|
||
private static final Logger LOGGER = Logger.getLogger(IndexServlet.class.getName()); | ||
|
||
// Saving credentials in environment variables is convenient, but not secure - consider a more | ||
// secure solution such as https://cloud.google.com/kms/ to help keep secrets safe. | ||
private static final String CLOUD_SQL_INSTANCE_NAME = System.getenv("CLOUD_SQL_INSTANCE_NAME"); | ||
private static final String DB_USER = System.getenv("DB_USER"); | ||
private static final String DB_PASS = System.getenv("DB_PASS"); | ||
kurtisvg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private static final String DB_NAME = System.getenv("DB_NAME"); | ||
|
||
private DataSource createConnectionPool() { | ||
// [START cloud_sql_mysql_connection_pool] | ||
// The configuration object specifies behaviors for the connection pool. | ||
HikariConfig config = new HikariConfig(); | ||
|
||
// Configure which instance and what database user to connect with. | ||
config.setJdbcUrl(String.format("jdbc:mysql:///%s", DB_NAME)); | ||
config.setUsername(DB_USER); // e.g. "root", "postgres" | ||
config.setPassword(DB_PASS); // e.g. "my-password" | ||
|
||
// For Java users, the Cloud SQL JDBC Socket Factory can provide authenticated connections. | ||
// See https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory for details. | ||
config.addDataSourceProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory"); | ||
config.addDataSourceProperty("cloudSqlInstance", CLOUD_SQL_INSTANCE_NAME); | ||
config.addDataSourceProperty("useSSL", "false"); | ||
|
||
// ... Specify additional connection properties here. | ||
|
||
// [START_EXCLUDE] | ||
|
||
// [START cloud_sql_limit_connections] | ||
// maximumPoolSize limits the total number of concurrent connections this pool will keep. Ideal | ||
// values for this setting are highly variable on app design, infrastructure, and database. | ||
config.setMaximumPoolSize(5); | ||
// minimumIdle is the minimum number of idle connections Hikari maintains in the pool. | ||
// Additional connections will be established to meet this value unless the pool is full. | ||
config.setMinimumIdle(5); | ||
// [END cloud_sql_limit_connections] | ||
|
||
// [START cloud_sql_connection_timeout] | ||
// setConnectionTimeout is the maximum number of milliseconds to wait for a connection checkout. | ||
// Any attempt to retrieve a connection from this pool that exceeds the set limit will throw an | ||
// SQLException. | ||
config.setConnectionTimeout(10000); // 10 seconds | ||
// idleTimeout is the maximum amount of time a connection can sit in the pool. Connections that | ||
// sit idle for this many milliseconds are retried if minimumIdle is exceeded. | ||
config.setIdleTimeout(600000); // 10 minutes | ||
// [END cloud_sql_connection_timeout] | ||
|
||
// [START cloud_sql_connection_backoff] | ||
// Hikari automatically delays between failed connection attempts, eventually reaching a | ||
// maximum delay of `connectionTimeout / 2` between attempts. | ||
// [END cloud_sql_connection_backoff] | ||
|
||
// [START cloud_sql_connection_lifetime] | ||
// maxLifetime is the maximum possible lifetime of a connection in the pool. Connections that | ||
// live longer than this many milliseconds will be closed and reestablished between uses. This | ||
// value should be several minutes shorter than the database's timeout value to avoid unexpected | ||
// terminations. | ||
config.setMaxLifetime(1800000); // 30 minutes | ||
kurtisvg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// [END cloud_sql_connection_lifetime] | ||
|
||
// [END_EXCLUDE] | ||
|
||
// Initialize the connection pool using the configuration object. | ||
DataSource pool = new HikariDataSource(config); | ||
// [END cloud_sql_mysql_connection_pool] | ||
return pool; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might you want to create a Shutdown Hook to close any currently inactive instances and mark active ones to be closed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to skip this for now as well - we want to expand the sample to additional run-times soon and I want to avoid any run-time specific options. |
||
|
||
private void createTable(DataSource pool) throws SQLException { | ||
// Safely attempt to create the table schema. | ||
try (Connection conn = pool.getConnection()) { | ||
PreparedStatement createTableStatement = conn.prepareStatement( | ||
"CREATE TABLE IF NOT EXISTS votes ( " | ||
+ "vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, candidate CHAR(6) NOT NULL," | ||
+ " PRIMARY KEY (vote_id) );" | ||
); | ||
createTableStatement.execute(); | ||
} | ||
} | ||
|
||
@Override | ||
public void contextDestroyed(ServletContextEvent event) { | ||
// This function is called when the Servlet is destroyed. | ||
HikariDataSource pool = (HikariDataSource) event.getServletContext().getAttribute("my-pool"); | ||
if (pool != null) { | ||
pool.close(); | ||
} | ||
} | ||
|
||
@Override | ||
public void contextInitialized(ServletContextEvent event) { | ||
// This function is called when the application starts and will safely create a connection pool | ||
// that can be used to connect to. | ||
DataSource pool = (DataSource) event.getServletContext().getAttribute("my-pool"); | ||
if (pool == null) { | ||
pool = createConnectionPool(); | ||
event.getServletContext().setAttribute("my-pool", pool); | ||
} | ||
try { | ||
createTable(pool); | ||
} catch (SQLException ex) { | ||
throw new RuntimeException("Unable to verify table schema. Please double check the steps" | ||
+ "in the README and try again.", ex); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also point them at the create a project doc as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project