Developing with Cloud Workstations and Cloud Code

1. Overview

This lab demonstrates features and capabilities designed to streamline the development workflow for software engineers tasked with developing Java applications in a containerized environment. Typical container development requires the user to understand details of containers and the container build process. Additionally, developers typically have to break their flow, moving out of their IDE to test and debug their applications in remote environments. With the tools and technologies mentioned in this tutorial, developers can work effectively with containerized applications without leaving their IDE.

What you will learn

In this lab you will learn methods for developing with containers in GCP including:

  • InnerLoop development with Cloud Workstations
  • Creating a new Java starter application
  • Walking through the development process
  • Developing a simple CRUD Rest Service
  • Debugging application on GKE cluster
  • Connecting application to CloudSQL database


2. Setup and Requirements

Self-paced environment setup

  1. Sign-in to the Google Cloud Console and create a new project or reuse an existing one. If you don't already have a Gmail or Google Workspace account, you must create one.




  • The Project name is the display name for this project's participants. It is a character string not used by Google APIs. You can update it at any time.
  • The Project ID is unique across all Google Cloud projects and is immutable (cannot be changed after it has been set). The Cloud Console auto-generates a unique string; usually you don't care what it is. In most codelabs, you'll need to reference the Project ID (it is typically identified as PROJECT_ID). If you don't like the generated ID, you may generate another random one. Alternatively, you can try your own and see if it's available. It cannot be changed after this step and will remain for the duration of the project.
  • For your information, there is a third value, a Project Number which some APIs use. Learn more about all three of these values in the documentation.
  1. Next, you'll need to enable billing in the Cloud Console to use Cloud resources/APIs. Running through this codelab shouldn't cost much, if anything at all. To shut down resources so you don't incur billing beyond this tutorial, you can delete the resources you created or delete the whole project. New users of Google Cloud are eligible for the $300 USD Free Trial program.

Start Cloudshell Editor

This lab was designed and tested for use with Google Cloud Shell Editor. To access the editor,

  1. access your google project at
  2. In the top right corner click on the cloud shell editor icon


  1. A new pane will open in the bottom of your window
  2. Click on the Open Editor button


  1. The editor will open with an explorer on the right and editor in the central area
  2. A terminal pane should also be available in the bottom of the screen
  3. If the terminal is NOT open use the key combination of `ctrl+`` to open a new terminal window

Set up gcloud

In Cloud Shell, set your project ID and the region you want to deploy your application to. Save them as PROJECT_ID and REGION variables.

export REGION=us-central1
export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')

Clone the source code

The source code for this lab is located in the container-developer-workshop in GoogleCloudPlatform on GitHub. Clone it with the command below then change into the directory.

git clone
cd container-developer-workshop/labs/spring-boot

Provision the infrastructure used in this lab

In this lab you will deploy code to GKE and access data stored in a CloudSQL database. The setup script below prepares this infrastructure for you. The provisioning process will take over 25 minutes. Wait for the script to complete before moving to the next section.

./ &

Cloud Workstations Cluster

Open Cloud Workstations in the Cloud Console. Wait for the cluster to be in READY status.


Create Workstations Configuration

If your Cloud Shell session was disconnected, click "Reconnect" and then run the gcloud cli command to set the project ID. Replace sample project id below with your qwiklabs project ID before running the command.

gcloud config set project qwiklabs-gcp-project-id

Run the script below in the terminal to create Cloud Workstations configuration.

cd ~/container-developer-workshop/labs/spring-boot

Verify results under the Configurations section. It will take 2 minutes to transition to READY status.


Open Cloud Workstations in the Console and create new instance.


Change name to my-workstation and select existing configuration: codeoss-java.


Verify results under the Workstations section.


Launch Workstation

Start and launch the workstation.


Allow 3rd party cookies by clicking on the icon in the address bar. 1b8923e2943f9bc4.png


Click "Site not working?".


Click "Allow cookies".


Once the workstation launches you will see Code OSS IDE come up. Click on "Mark Done" on the Getting Started page one the workstation IDE


3. Creating a new Java starter application

In this section you'll create a new Java Spring Boot application from scratch utilizing a sample application provided by Open a new Terminal.


Clone the Sample Application

  1. Create a starter application
curl -d dependencies=web -d type=maven-project -d javaVersion=17 -d packageName=com.example.springboot -o

Click on the Allow button if you see this message, so that you can copy paste into the workstation.


  1. Unzip the application
unzip -d sample-app
  1. Open the "sample-app" folder
cd sample-app && code-oss-cloud-workstations -r --folder-uri="$PWD"

Add spring-boot-devtools & Jib

To enable the Spring Boot DevTools find and open the pom.xml from the explorer in your editor. Next paste the following code after the description line which reads <description>Demo project for Spring Boot</description>

  1. Add spring-boot-devtools in pom.xml

Open the pom.xml in the root of the project. Add the following configuration after the Description entry.


  <!--  Spring profiles-->
  1. Enable jib-maven-plugin in pom.xml

Jib is an open-source Java containerizing tool from Google that lets Java developers build containers using the Java tools they know. Jib is a fast and simple container image builder that handles all the steps of packaging your application into a container image. It does not require you to write a Dockerfile or have docker installed, and it is directly integrated into Maven and Gradle.

Scroll down in the pom.xml file and update the Build section to include the Jib plugin. The build section should match the following when completed.


      <!--  Jib Plugin-->
       <!--  Maven Resources Plugin-->

Generate Manifests

Skaffold provides integrated tools to simplify container development. In this step you will initialize Skaffold which will automatically create base Kubernetes YAML files. The process tries to identify directories with container image definitions, like a Dockerfile, and then creates a deployment and service manifest for each.

Execute the command below in the Terminal to begin the process.


  1. Execute the following command in the terminal
skaffold init --generate-manifests
  1. When prompted:
  • Use the arrows to move your cursor to Jib Maven Plugin
  • Press the spacebar to select the option.
  • Press enter to continue
  1. Enter 8080 for the port
  2. Enter y to save the configuration

Two files are added to the workspace skaffold.yaml and deployment.yaml

Skaffold output:


Update app name

The default values included in the configuration don't currently match the name of your application. Update the files to reference your application name rather than the default values.

  1. Change entries in Skaffold config
  • Open skaffold.yaml
  • Select the image name currently set as pom-xml-image
  • Right click and choose Change All Occurrences
  • Type in the new name as demo-app
  1. Change entries in Kubernetes config
  • Open deployment.yaml file
  • Select the image name currently set as pom-xml-image
  • Right click and choose Change All Occurrences
  • Type in the new name as demo-app

Enable Auto sync mode

To facilitate an optimized hot reload experience you'll utilize the Sync feature provided by Jib. In this step you will configure Skaffold to utilize that feature in the build process.

Note that the "sync" profile you are configuring in the Skaffold configuration leverages the Spring "sync" Profile you have configured in the previous step, where you have enabled support for spring-dev-tools.

  1. Update Skaffold config

In the skaffold.yaml file replace the entire build section of the file with the following specification. Do not alter other sections of the file.


  - image: demo-app
      project: com.example:demo
      type: maven
      - --no-transfer-progress
      - -Psync
      auto: true

Add a default route

Create a file called in /src/main/java/com/example/springboot/ folder.


Paste the following contents in the file to create a default http route.

package com.example.springboot;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Value;

public class HelloController {

    String target;

    public String hello()
        return String.format("Hello from your %s environment!", target);

4. Walking through the development process

In this section you'll walk through a few steps using the Cloud Code plugin to learn the basic processes and to validate the configuration and setup of your starter application.

Cloud Code integrates with Skaffold to streamline your development process. When you deploy to GKE in the following steps, Cloud Code and Skaffold will automatically build your container image, push it to a Container Registry, and then deploy your application to GKE. This happens behind the scenes abstracting the details away from the developer flow. Cloud Code also enhances your development process by providing traditional debug and hotsync capabilities to container based development.

Sign in to Google Cloud

Click on Cloud Code icon and Select "Sign in to Google Cloud":


Click "Proceed to sign in".


Check the output in the Terminal and open the link:


Login with your Qwiklabs students credentials.


Select "Allow":


Copy verification code and return to the Workstation tab.


Paste the verification code and hit Enter.


Add Kubernetes Cluster

  1. Add a Cluster


  1. Select Google Kubernetes Engine:


  1. Select project.


  1. Select "quote-cluster" that was created in the initial setup.



Set current project id using gcloud cli

Copy project ID for this lab from qwiklabs page.


Run the gcloud cli command to set the project ID. Replace sample project id before running the command.

gcloud config set project qwiklabs-gcp-project-id

Sample output:


Debug on Kubernetes

  1. In the left pane at the bottom select Cloud Code.


  1. In the panel that appears under DEVELOPMENT SESSIONS, select Debug on Kubernetes.

Scroll down if the option is not visible.


  1. Select "Yes" to use current context.


  1. Select "quote-cluster" that was created during initial setup.


  1. Select Container Repository.


  1. Select the Output tab in the lower pane to view progress and notifications
  2. Select "Kubernetes: Run/Debug - Detailed" in the channel drop down to the right to view additional details and logs streaming live from the containers


Wait for the application to be deployed.


  1. Review deployed application on GKE in Cloud Console.


  1. Return to the simplified view by selecting "Kubernetes: Run/Debug" from the dropdown on the OUTPUT tab.
  2. When the build and tests are done, the Output tab says: Resource deployment/demo-app status completed successfully, and a url is listed: "Forwarded URL from service demo-app: http://localhost:8080"
  3. In the Cloud Code terminal, hover over the URL in the output (http://localhost:8080), and then in the tool tip that appears select Follow link.


New tab will be opened and you will see output below:


Utilize Breakpoints

  1. Open the application located at /src/main/java/com/example/springboot/
  2. Locate the return statement for the root path which reads return String.format("Hello from your %s environment!", target);
  3. Add a breakpoint to that line by clicking the blank space to the left of the line number. A red indicator will show to note the breakpoint is set


  1. Reload your browser and note the debugger stops the process at the breakpoint and allows you to investigate the variables and state of the application which is running remotely in GKE


  1. Click down into the variables section until you find the "Target" variable.
  2. Observe the current value as "local"


  1. Double click on the variable name "target" and in the popup,

change the value to "Cloud Workstations"


  1. Click the Continue button in the debug control panel


  1. Review the response in your browser which now shows the updated value you just entered.


  1. Remove the breakpoint by clicking the red indicator to the left of the line number. This will prevent your code from stopping execution at this line as you move forward in this lab.

Hot Reload

  1. Change the statement to return a different value such as "Hello from %s Code"
  2. The file is automatically saved and synced into the remote containers in GKE
  3. Refresh your browser to see the updated results.
  4. Stop the debugging session by clicking on the red square in the debug toolbar

a541f928ec8f430e.png c2752bb28d82af86.png

Select "Yes clean up after each run".


5. Developing a simple CRUD Rest Service

At this point your application is fully configured for containerized development and you've walked through the basic development workflow with Cloud Code. In the following sections you practice what you've learned by adding REST service endpoints connecting to a managed database in Google Cloud.

Configure Dependencies

The application code uses a database to persist the rest service data. Ensure the dependencies are available by adding the following in the pom.xl

  1. Open the pom.xml file and add the following into the dependencies section of the config


    <!--  Database dependencies-->

Code REST service

Create a file called in /src/main/java/com/example/springboot/ and copy in the code below. This defines the Entity model for the Quote object used in the application.

package com.example.springboot;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.util.Objects;

@Table(name = "quotes")
public class Quote
    @Column(name = "id")
    private Integer id;

    private String quote;

    private String author;

    public Integer getId() {
        return id;

    public void setId(Integer id) { = id;

    public String getQuote() {
        return quote;

    public void setQuote(String quote) {
        this.quote = quote;

    public String getAuthor() {
        return author;

    public void setAuthor(String author) { = author;

    public boolean equals(Object o) {
      if (this == o) {
        return true;
      if (o == null || getClass() != o.getClass()) {
        return false;
        Quote quote1 = (Quote) o;
        return Objects.equals(id, &&
                Objects.equals(quote, quote1.quote) &&

    public int hashCode() {
        return Objects.hash(id, quote, author);

Create a file called at src/main/java/com/example/springboot and copy in the following code

package com.example.springboot;


public interface QuoteRepository extends JpaRepository<Quote,Integer> {

    @Query( nativeQuery = true, value =
            "SELECT id,quote,author FROM quotes ORDER BY RANDOM() LIMIT 1")
    Quote findRandomQuote();

This code uses JPA for persisting the data. The class extends the Spring JPARepository interface and allows the creation of custom code. In the code you've added a findRandomQuote custom method.

To expose the endpoint for the service, a QuoteController class will provide this functionality.

Create a file called at src/main/java/com/example/springboot and copy in the following contents

package com.example.springboot;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

public class QuoteController {

    private final QuoteRepository quoteRepository;

    public QuoteController(QuoteRepository quoteRepository) {
        this.quoteRepository = quoteRepository;

    public Quote randomQuote()
        return quoteRepository.findRandomQuote();  

    public ResponseEntity<List<Quote>> allQuotes()
        try {
            List<Quote> quotes = new ArrayList<Quote>();

            if (quotes.size()==0 || quotes.isEmpty()) 
                return new ResponseEntity<List<Quote>>(HttpStatus.NO_CONTENT);
            return new ResponseEntity<List<Quote>>(quotes, HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<List<Quote>>(HttpStatus.INTERNAL_SERVER_ERROR);

    public ResponseEntity<Quote> createQuote(@RequestBody Quote quote) {
        try {
            Quote saved =;
            return new ResponseEntity<Quote>(saved, HttpStatus.CREATED);
        } catch (Exception e) {
            return new ResponseEntity<Quote>(HttpStatus.INTERNAL_SERVER_ERROR);

    public ResponseEntity<Quote> updateQuote(@PathVariable("id") Integer id, @RequestBody Quote quote) {
        try {
            Optional<Quote> existingQuote = quoteRepository.findById(id);
                Quote updatedQuote = existingQuote.get();

                return new ResponseEntity<Quote>(updatedQuote, HttpStatus.OK);
            } else {
                return new ResponseEntity<Quote>(HttpStatus.NOT_FOUND);
        } catch (Exception e) {
            return new ResponseEntity<Quote>(HttpStatus.INTERNAL_SERVER_ERROR);

    public ResponseEntity<HttpStatus> deleteQuote(@PathVariable("id") Integer id) {
        Optional<Quote> quote = quoteRepository.findById(id);
        if (quote.isPresent()) {
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } else {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);


Add Database Configurations


Add configuration for the backend database accessed by the service. Edit (or create if not present) the file called application.yaml file under src/main/resources and add a parameterized Spring configuration for the backend.

target: local

      on-profile: cloud-dev
    url: 'jdbc:postgresql://${DB_HOST:}/${DB_NAME:quote_db}'
    username: '${DB_USER:user}'
    password: '${DB_PASS:password}'
            non_contextual_creation: true
        dialect: org.hibernate.dialect.PostgreSQLDialect
      ddl-auto: update

Add Database Migration

Create folders db/migration under src/main/resources

Create a SQL file: V1__create_quotes_table.sql

Paste the following contents into the file


   quote VARCHAR(1024),
   author VARCHAR(256)

INSERT INTO quotes (id,quote,author) VALUES (1,'Never, never, never give up','Winston Churchill');
INSERT INTO quotes (id,quote,author) VALUES (2,'While there''s life, there''s hope','Marcus Tullius Cicero');
INSERT INTO quotes (id,quote,author) VALUES (3,'Failure is success in progress','Anonymous');
INSERT INTO quotes (id,quote,author) VALUES (4,'Success demands singleness of purpose','Vincent Lombardi');
INSERT INTO quotes (id,quote,author) VALUES (5,'The shortest answer is doing','Lord Herbert');

Kubernetes Config

The following additions to the deployment.yaml file allow the application to connect to the CloudSQL instances.

  • TARGET - configures the variable to indicate the environment where the app is executed
  • SPRING_PROFILES_ACTIVE - shows the active Spring profile, which will be configured to cloud-dev
  • DB_HOST - the private IP for the database, which has been noted when the database instance has been created or by clicking SQL in the Navigation Menu of the Google Cloud Console - please change the value !
  • DB_USER and DB_PASS - as set in the CloudSQL instance configuration, stored as a Secret in GCP

Update your deployment.yaml with the contents below.


apiVersion: v1
kind: Service
  name: demo-app
    app: demo-app
  - port: 8080
    protocol: TCP
  clusterIP: None
    app: demo-app
apiVersion: apps/v1
kind: Deployment
  name: demo-app
    app: demo-app
  replicas: 1
      app: demo-app
        app: demo-app
      - name: demo-app
        image: demo-app
          - name: PORT
            value: "8080"
          - name: TARGET
            value: "Local Dev - CloudSQL Database - K8s Cluster"
          - name: SPRING_PROFILES_ACTIVE
            value: cloud-dev
          - name: DB_HOST
            value: ${DB_INSTANCE_IP}   
          - name: DB_PORT
            value: "5432"  
          - name: DB_USER
                name: gke-cloud-sql-secrets
                key: username
          - name: DB_PASS
                name: gke-cloud-sql-secrets
                key: password
          - name: DB_NAME
                name: gke-cloud-sql-secrets
                key: database

Replace the DB_HOST value with the address of your Database by running the commands below in the terminal:

export DB_INSTANCE_IP=$(gcloud sql instances describe quote-db-instance \
    --format=json | jq \
    --raw-output ".ipAddresses[].ipAddress")

envsubst < deployment.yaml > && mv deployment.yaml

Open deployment.yaml and verify that DB_HOST value was updated with Instance IP.


Deploy and Validate Application

  1. In the pane at the bottom of Cloud Shell Editor, select Cloud Code then select Debug on Kubernetes at the top of the screen.


  1. When the build and tests are done, the Output tab says: Resource deployment/demo-app status completed successfully, and a url is listed: "Forwarded URL from service demo-app: http://localhost:8080". Note that sometimes the port may be different like 8081. If so set the appropriate value. Set the value of URL in the terminal
export URL=localhost:8080
  1. View Random Quotes

From Terminal, run the command below multiple times against the random-quote endpoint. Observe repeated call returning different quotes

curl $URL/random-quote | jq
  1. Add a Quote

Create a new quote, with id=6 using the command listed below and observe the request being echoed back

curl -H 'Content-Type: application/json' -d '{"id":"6","author":"Henry David Thoreau","quote":"Go confidently in the direction of your dreams! Live the life you have imagined"}' -X POST $URL/quotes
  1. Delete a quote

Now delete the quote you just added with the delete method and observe an HTTP/1.1 204 response code.

curl -v -X DELETE $URL/quotes/6
  1. Server Error

Experience an error state by running the last request again after the entry has already been deleted

curl -v -X DELETE $URL/quotes/6

Notice the response returns an HTTP:500 Internal Server Error.

Debug the application

In the previous section you found an error state in the application when you tried to delete an entry that was not in the database. In this section you'll set a breakpoint to locate the issue. The error occurred in the DELETE operation, so you'll work with the QuoteController class.

  1. Open src/main/java/com/example/springboot/
  2. Find the deleteQuote() method
  3. Find the the line: Optional<Quote> quote = quoteRepository.findById(id);
  4. Set a breakpoint on that line by clicking the blank space to the left of the line number.
  5. A red indicator will appear indicating the breakpoint is set
  6. Run the delete command again
curl -v -X DELETE $URL/quotes/6
  1. Switch back to the debug view by clicking the icon in the left column
  2. Observe the debug line stopped in the QuoteController class.
  3. In the debugger, click the step over icon b814d39b2e5f3d9e.png
  4. Observe that a code returns an Internal Server Error HTTP 500 to the client which is not ideal.
* Connected to ( port 8080 (#0)
> DELETE /quotes/6 HTTP/1.1
> Host:
> User-Agent: curl/7.74.0
> Accept: */*
* Mark bundle as not supporting multiuse
< HTTP/1.1 500
< Content-Length: 0
< Date: 
* Connection #0 to host left intact

Update the code

The code is incorrect and the else block should be refactored to send back a HTTP 404 not found status code.

Correct the error.

  1. With the Debug session still running, complete the request by pressing the "continue" button in the debug control panel.
  2. Next change the else block to the code:
       else {
                return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);

The method should look like the following

public ResponseEntity<HttpStatus> deleteQuote(@PathVariable("id") Integer id) {
        Optional<Quote> quote = quoteRepository.findById(id);
        if (quote.isPresent()) {
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } else {
            return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
  1. Rerun the delete command
curl -v -X DELETE $URL/quotes/6
  1. Step through the debugger and observe the HTTP 404 Not Found returned to the caller.
* Connected to ( port 8080 (#0)
> DELETE /quotes/6 HTTP/1.1
> Host:
> User-Agent: curl/7.74.0
> Accept: */*
* Mark bundle as not supporting multiuse
< HTTP/1.1 404
< Content-Length: 0
< Date: 
* Connection #0 to host left intact
  1. Stop the debugging session by clicking on the red square in the debug toolbar



6. Congratulations

Congratulations! In this lab you've created a new Java application from scratch and configured it to work effectively with containers. You then deployed and debugged your application to a remote GKE cluster following the same developer flow found in traditional application stacks.

What you have learned

  • InnerLoop development with Cloud Workstations
  • Creating a new Java starter application
  • Walking through the development process
  • Developing a simple CRUD REST Service
  • Debugging application on GKE cluster
  • Connecting application to CloudSQL database