1. Overview
This series of codelabs (self-paced, hands-on tutorials) aims to help Google App Engine (Standard) Java developers modernize their apps by guiding them through a series of migrations. By following these steps, you can update your app to be more portable and decide to containerize them for Cloud Run, Google Cloud's container-hosting sister service to App Engine, and other container-hosting services.
This tutorial teaches you how to containerize an App Engine app for deploying to the Cloud Run fully-managed service by using Jib. With Jib, you can create Docker images, a well-known platform in industry for developing, shipping, and running applications in containers.
In addition to teaching you the required steps to move from App Engine to Cloud Run, you will also learn how to upgrade a Java 8 App Engine app to Java 17.
If your application makes heavy use of App Engine legacy bundled services or other App Engine features, we recommend migrating off of those bundled services or replacing those features before moving to Cloud Run. If you need more time to investigate your migration options or want to continue using the legacy bundled services for the time being, you can continue to access App Engine bundled services for Java 11/17 when upgrading to a newer runtime. When your app is more portable, come back to this codelab to learn how to apply the instructions to your app.
You'll learn how to
- Use Cloud Shell
- Enable the Cloud Run, Artifact Registry, and Cloud Build APIs
- Containerize your app using Jib, and Cloud Build
- Deploy your container images to Cloud Run
What you'll need
- A Google Cloud Platform project with an active GCP billing account and App Engine enabled
- Working knowledge of common Linux commands
- Basic knowledge of developing and deploying App Engine apps
- A Java 8 servlet app that you'd like to migrate to Java 17 and deploy to Cloud Run (this can be an app on App Engine or just the source)
Survey
How will you use this tutorial?
How would you rate your experience with Java?
How would you rate your experience with using Google Cloud services?
2. Background
Platform as a Service (PaaS) systems like App Engine and Cloud Functions provide many conveniences for your team and application, such as enabling SysAdmins and Devops to focus on building solutions. With severless platforms, your app can autoscale up as needed, scale down to zero with pay-per-use billing to help control costs, and use a variety of common development languages.
However, the flexibility of containers is compelling as well. With the ability to choose any language, any library, and any binary, containers give you the best of both worlds: the convenience of serverless along with the flexibility of containers. This is what Cloud Run is all about.
Learning how to use Cloud Run is not within the scope of this codelab; that's covered by the Cloud Run documentation. The goal here is for you to get familiar with how to containerize your App Engine app for Cloud Run (or other container-hosted services). There are a few things you should know before moving forward, primarily that your user experience will be slightly different.
In this codelab, you'll learn how to build and deploy containers. You'll learn how to:
- Containerize your app with Jib
- Migrate away from App Engine configuration
- and, optionally, define build steps for Cloud Build.
This will involve moving away from certain App Engine specific features. If you prefer not to follow this path, you can still upgrade to a Java 11/17 runtime while keeping your apps on App Engine instead.
3. Setup/Prework
1. Setup project
For this tutorial, you will use a sample app from the appengine-java-migration-samples repository on a brand new project. Ensure the project has an active billing account.
If you intend to move an existing App Engine app to Cloud Run, you can use that app to follow along instead.
Run the following command to enable the necessary APIs for your project:
gcloud services enable artifactregistry.googleapis.com cloudbuild.googleapis.com run.googleapis.com
2. Get baseline sample app
Clone the sample app either on your own machine or the Cloud Shell, then navigate to the baseline folder.
The sample is a Java 8, Servlet-based Datastore app intended for deployment on App Engine. Follow the instructions in the README on how to prepare this app for App Engine deployment.
3. (Optional) Deploy baseline app
The following is only necessary if you would like to confirm that the app works on App Engine before we migrate to Cloud Run.
Refer to the steps in the README.md:
- Install/Re-familiarize yourself with the
gcloud
CLI - Initialize the gcloud CLI for your project with
gcloud init
- Create the App Engine project with
gcloud app create
- Deploy the sample app to App Engine
./mvnw package appengine:deploy -Dapp.projectId=$PROJECT_ID
- Confirm the app runs on App Engine without issues
4. Create an Artifact Registry repository
After containerizing your app, you'll need somewhere to push and store your images. The recommended way to go about this on Google Cloud is with Artifact Registry.
Create the repository named migration
with gcloud like so:
gcloud artifacts repositories create migration --repository-format=docker \
--description="Docker repository for the migrated app" \
--location="northamerica-northeast1"
Note that this repository uses the docker
format type, but there are several repository types available.
At this point, you have your baseline App Engine app, and your Google Cloud project is prepared to migrate it to Cloud Run.
4. Modify Application Files
In cases where your app makes heavy use of App Engine's legacy bundled services, configuration, or other App Engine only features, we recommend continuing to access those services while upgrading to the new runtime. This codelab demonstrates a migration path for applications that already use standalone services, or can feasibly be refactored to do so.
1. Upgrading to Java 17
If your app is on Java 8, consider upgrading to a later LTS candidate like 11 or 17 to keep up with security updates and gain access to new language features.
Start by updating the properties in your pom.xml
to include the following:
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
This will set the project version to 17, inform the compiler plugin that you want access to java 17 language features, and would like the compiled classes to be compatible with the Java 17 JVM.
2. Including a web server
There are a number of differences between App Engine and Cloud Run worth considering when moving between them. One difference is that while App Engine's Java 8 runtime provided and managed a Jetty server for the apps it hosted, Cloud Run does not. We'll be using Spring Boot to provide us with a web server and servlet container.
Add the following dependencies:
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.6</version>
<exclusions>
<!-- Exclude the Tomcat dependency -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Use Jetty instead -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
<version>2.6.6</version>
</dependency>
<!-- ... -->
</dependencies>
Spring Boot embeds a Tomcat server by default, but this sample will exclude that artifact and stick with Jetty to minimize differences in default behavior after the migration. We can also configure the Jetty version to match the one that App Engine provides out of the box.
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<jetty.version>9.4.46.v20220331</jetty.version>
</properties>
3. Spring Boot setup
While Spring Boot will be able to re-use your servlets without modification, it will require some configuration for discoverability.
Create the following MigratedServletApplication.java
class in the com.example.appengine
package:
package com.example.appengine;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan
@SpringBootApplication
@EnableAutoConfiguration
public class MigratedServletApplication {
public static void main(String[] args) {
SpringApplication.run(MigratedServletApplication.class, args);
}
}
Note that this includes the @ServletComponentScan
annotation, which will look (in the current package by default) for any @WebServlets
, and make them available as expected.
4. Packaging the app as a JAR
While it is possible to containerize your app with Jib starting from a war, it becomes easier if you package your app as an executable JAR. This won't require much configuration, particularly for projects using Maven as a build tool — as jar packaging is the default behavior.
Remove the packaging
tag in thepom.xml
file:
<packaging>war</packaging>
Next, add the spring-boot-maven-plugin
:
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.6</version>
</plugin>
<!-- ... -->
</plugins>
5. Migrating away from App Engine configuration, services, and dependencies
As mentioned in the beginning of the codelab, Cloud Run and App Engine are designed to offer different user experiences. Certain features that App Engine offers out of the box—like the Cron and Task Queue services—need to be re-created manually and will be covered in more detail in later modules.
The sample app does not make use of legacy bundled services, but users whose apps do can refer to the following guides:
- Migrating from bundled services to find suitable standalone services.
- Migrating XML configuration files to YAML, for users migrating to the Java 11/17 runtimes while remaining on App Engine.
Since you will be deploying to Cloud Run from now on, the appengine-maven-plugin
can be removed:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>appengine-maven-plugin</artifactId>
<version>2.4.1</version>
<configuration>
<!-- can be set w/ -DprojectId=myProjectId on command line -->
<projectId>${app.projectId}</projectId>
<!-- set the GAE version or use "GCLOUD_CONFIG" for an autogenerated GAE version -->
<version>GCLOUD_CONFIG</version>
</configuration>
</plugin>
5. Containerize Application
At this point you could manually deploy your app to Cloud Run directly from your source code. This is an excellent option that uses Cloud Build behind the scenes to provide a hands off deploy experience. We will cover source deploys in more detail in later modules.
Alternatively, if you need more control over the way your app is deployed, you can achieve that by defining a cloudbuild.yaml
file that explicitly lays out your intended build steps:
1. Define a cloudbuild.yaml file
Create the following cloudbuild.yaml
file at the same level as the pom.xml
:
steps:
# Test your build
- name: maven:eclipse-temurin
entrypoint: mvn
args: ["test"]
# Build with Jib
- name: maven:eclipse-temurin
entrypoint: mvn
args: [ "compile", "com.google.cloud.tools:jib-maven-plugin:3.2.1:build", "-Dimage=northamerica-northeast1-docker.pkg.dev/PROJECT_ID/migration/visitors:jib"]
# Deploy to Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args: [ 'run', 'deploy', 'visitors', '--image', 'northamerica-northeast1-docker.pkg.dev/PROJECT_ID/migration/visitors:jib', '--region', 'northamerica-northeast1', '--allow-unauthenticated']
Once we tell Cloud Build to follow these steps, it will:
- Run your tests with
./mvnw test
- Build, push, and tag your image to Artifact registry with Jib
- Deploy your image to Cloud Run with
gcloud run deploy
Note that ‘visitors'
is supplied to Cloud Run as the desired service name. The –allow-unauthenticated
flag enables the users to visit the webapp without requiring authentication. Make sure to replace PROJECT_ID with your project's ID in the cloudbuild.yaml
file.
Next, add the following IAM policy bindings to allow the Cloud Build Service Account to Artifact Registry:
export PROJECT_ID=$(gcloud config list --format 'value(core.project)')
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)" )
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
--role=roles/run.admin \
--project=$PROJECT_ID
gcloud iam service-accounts add-iam-policy-binding $PROJECT_NUMBER-compute@developer.gserviceaccount.com \
--member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
--role roles/iam.serviceAccountUser --project=$PROJECT_ID
2. Run the build process
Now that you've informed Cloud Build on the desired build steps, you're ready for a one-click deploy.
Run the following command:
gcloud builds submit
Once the process is finished, your container image has been built, stored in Artifact Registry, and deployed to Cloud Run.
At the end of this codelab, your app should look the same as the one in the java17-and-cloud-run/finish.
And there you have it! You have successfully migrated a Java 8 App Engine app to Java 17 and Cloud Run, and now have a clearer understanding of the work involved when switching and choosing between hosting options.
6. Summary/Cleanup
Congratulations, you've upgraded, containerized, migrated and your app, which concludes this tutorial!
From here, the next step is to learn more about the CI/CD and Software supply chain security features that are within reach now that you can deploy with Cloud Build:
- Creating custom build steps with Cloud Build
- Creating and managing Build Triggers
- Using On-Demand scanning in your Cloud Build pipeline
Optional: Clean up and/or disable service
If you deployed the sample app on App Engine during this tutorial, remember to disable the app to avoid incurring charges. When you're ready to move to the next codelab, you can re-enable it. While App Engine apps are disabled, they won't get any traffic to incur charges, however Datastore usage may be billable if it exceeds its free quota, so delete enough to fall under that limit.
On the other hand, if you're not going to continue with migrations and want to delete everything completely, you can either delete your service or shutdown your project entirely.
7. Additional resources
App Engine migration module codelabs issues/feedback
If you find any issues with this codelab, please search for your issue first before filing. Links to search and create new issues:
Migration resources
- Migration options for unbundling app engine services
- Setting up Build triggers for Cloud Build
- More information on migrating to Java 11/17
Online resources
Below are online resources which may be relevant for this tutorial:
App Engine
- App Engine documentation
- App Engine pricing and quotas information
- Comparing first & second generation platforms
- Long-term support for legacy runtimes
Other Cloud information
- Google Cloud "Always Free" tier
- Google Cloud CLI (
gcloud
CLI) - All Google Cloud documentation
Videos
- Serverless Migration Station
- Serverless Expeditions
- Subscribe to Google Cloud Tech
- Subscribe to Google Developers
License
This work is licensed under a Creative Commons Attribution 2.0 Generic License.