Desarrollo con Cloud Workstations y Cloud Code

1. Descripción general

En este lab, se demuestran las funciones y capacidades diseñadas para optimizar el flujo de trabajo de desarrollo de los ingenieros de software encargados de desarrollar aplicaciones en Java en un entorno de contenedores. El desarrollo de contenedores típico requiere que el usuario comprenda los detalles de los contenedores y el proceso de compilación de contenedores. Además, los desarrolladores suelen tener que interrumpir su flujo de trabajo y salir del IDE para probar y depurar sus aplicaciones en entornos remotos. Con las herramientas y tecnologías que se mencionan en este instructivo, los desarrolladores pueden trabajar de manera eficaz con aplicaciones en contenedores sin salir de su IDE.

Qué aprenderás

En este lab, aprenderás métodos para desarrollar con contenedores en GCP, incluidos los siguientes:

  • Desarrollo de InnerLoop con Cloud Workstations
  • Cómo crear una nueva aplicación inicial de Java
  • Explicación del proceso de desarrollo
  • Cómo desarrollar un servicio REST de CRUD simple
  • Depuración de la aplicación en el clúster de GKE
  • Conecta la aplicación a la base de datos de Cloud SQL

58a4cdd3ed7a123a.png

2. Configuración y requisitos

Cómo configurar el entorno a tu propio ritmo

  1. Accede a Google Cloud Console y crea un proyecto nuevo o reutiliza uno existente. Si aún no tienes una cuenta de Gmail o de Google Workspace, debes crear una.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • El Nombre del proyecto es el nombre visible de los participantes de este proyecto. Es una cadena de caracteres que no se utiliza en las APIs de Google. Puedes actualizarla en cualquier momento.
  • El ID del proyecto es único en todos los proyectos de Google Cloud y es inmutable (no se puede cambiar después de configurarlo). La consola de Cloud genera automáticamente una cadena única. Por lo general, no importa cuál sea. En la mayoría de los codelabs, deberás hacer referencia al ID del proyecto (suele identificarse como PROJECT_ID). Si no te gusta el ID que se generó, podrías generar otro aleatorio. También puedes probar uno propio y ver si está disponible. No se puede cambiar después de este paso y se usará el mismo durante todo el proyecto.
  • Recuerda que hay un tercer valor, un número de proyecto, que usan algunas APIs. Obtén más información sobre estos tres valores en la documentación.
  1. A continuación, deberás habilitar la facturación en la consola de Cloud para usar las APIs o los recursos de Cloud. Ejecutar este codelab no debería costar mucho, tal vez nada. Para cerrar recursos y evitar que se generen cobros más allá de este instructivo, puedes borrar los recursos que creaste o borrar todo el proyecto. Los usuarios nuevos de Google Cloud son aptos para participar en el programa Prueba gratuita de USD 300.

Inicia el editor de Cloud Shell

Este lab se diseñó y probó para usarse con el Editor de Google Cloud Shell. Para acceder al editor, haz lo siguiente:

  1. Accede a tu proyecto de Google en https://console.cloud.google.com.
  2. En la esquina superior derecha, haz clic en el ícono del editor de Cloud Shell.

8560cc8d45e8c112.png

  1. Se abrirá un panel nuevo en la parte inferior de la ventana.
  2. Haz clic en el botón Abrir editor.

9e504cb98a6a8005.png

  1. El editor se abrirá con un explorador a la derecha y un editor en el área central.
  2. También debería haber un panel de terminal disponible en la parte inferior de la pantalla.
  3. Si la terminal NO está abierta, usa la combinación de teclas "Ctrl + `" para abrir una ventana de terminal nueva.

Configura gcloud

En Cloud Shell, establece el ID del proyecto y la región en la que deseas implementar tu aplicación. Guárdalos como variables PROJECT_ID y REGION.

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

Clona el código fuente

El código fuente de este lab se encuentra en container-developer-workshop en GoogleCloudPlatform en GitHub. Clónalo con el siguiente comando y, luego, cambia al directorio.

git clone https://github.com/GoogleCloudPlatform/container-developer-workshop.git
cd container-developer-workshop/labs/spring-boot

Aprovisiona la infraestructura que se usa en este lab

En este lab, implementarás código en GKE y accederás a los datos almacenados en una base de datos de Cloud SQL. La siguiente secuencia de comandos de configuración prepara esta infraestructura por ti. El proceso de aprovisionamiento tardará más de 25 minutos. Espera a que se complete la secuencia de comandos antes de pasar a la siguiente sección.

./setup_with_cw.sh &

Clúster de Cloud Workstations

Abre Cloud Workstations en Cloud Console. Espera a que el clúster tenga el estado READY.

305e1a3d63ac7ff6.png

Crea la configuración de las estaciones de trabajo

Si se desconectó tu sesión de Cloud Shell, haz clic en "Reconnect" y, luego, ejecuta el comando de la CLI de gcloud para establecer el ID del proyecto. Antes de ejecutar el comando, reemplaza el ID del proyecto de ejemplo que se muestra a continuación por el ID de tu proyecto de Qwiklabs.

gcloud config set project qwiklabs-gcp-project-id

Ejecuta la siguiente secuencia de comandos en la terminal para crear la configuración de Cloud Workstations.

cd ~/container-developer-workshop/labs/spring-boot
./workstation_config_setup.sh

Verifica los resultados en la sección Configurations. La transición al estado READY tardará 2 minutos.

7a6af5aa2807a5f2.png

Abre Cloud Workstations en la consola y crea una instancia nueva.

a53adeeac81a78c8.png

Cambia el nombre a my-workstation y selecciona la configuración existente: codeoss-java.

f21c216997746097.png

Verifica los resultados en la sección Workstations.

66a9fc8b20543e32.png

Iniciar estación de trabajo

Inicia la estación de trabajo.

c91bb69b61ec8635.png

Para permitir las cookies de terceros, haz clic en el ícono de la barra de direcciones. 1b8923e2943f9bc4.png

fcf9405b6957b7d7.png

Haz clic en “¿No funciona el sitio?”.

36a84c0e2e3b85b.png

Haz clic en "Permitir cookies".

2259694328628fba.png

Una vez que se inicie la estación de trabajo, aparecerá el IDE de Code OSS. Haz clic en "Marcar como completado" en la página de introducción del IDE de la estación de trabajo.

94874fba9b74cc22.png

3. Cómo crear una nueva aplicación inicial de Java

En esta sección, crearás una nueva aplicación de Java Spring Boot desde cero con una aplicación de muestra proporcionada por spring.io. Abre una terminal nueva.

c31d48f2e4938c38.png

Clona la aplicación de ejemplo

  1. Crea una aplicación inicial
curl  https://start.spring.io/starter.zip -d dependencies=web -d type=maven-project -d javaVersion=17 -d packageName=com.example.springboot -o sample-app.zip

Haz clic en el botón Permitir si ves este mensaje para que puedas copiar y pegar en la estación de trabajo.

58149777e5cc350a.png

  1. Descomprime la aplicación
unzip sample-app.zip -d sample-app
  1. Abre la carpeta "sample-app".
cd sample-app && code-oss-cloud-workstations -r --folder-uri="$PWD"

Agrega spring-boot-devtools y Jib

Para habilitar Spring Boot DevTools, busca y abre pom.xml desde el explorador en tu editor. A continuación, pega el siguiente código después de la línea de descripción que dice <description>Demo project for Spring Boot</description>.

  1. Agrega spring-boot-devtools en pom.xml.

Abre el archivo pom.xml en la raíz del proyecto. Agrega la siguiente configuración después de la entrada Description.

pom.xml

  <!--  Spring profiles-->
  <profiles>
    <profile>
      <id>sync</id>
      <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-devtools</artifactId>
        </dependency>
      </dependencies>
    </profile>
  </profiles>
  1. Habilita jib-maven-plugin en pom.xml

Jib es una herramienta de código abierto de Google para crear contenedores de Java que permite a los desarrolladores de Java compilar contenedores con las herramientas de Java que conocen. Jib es un compilador de imágenes de contenedor rápido y sencillo que controla todos los pasos del empaquetado de tu aplicación en una imagen de contenedor. No requiere que escribas un Dockerfile ni que tengas instalado Docker, y se integra directamente en Maven y Gradle.

Desplázate hacia abajo en el archivo pom.xml y actualiza la sección Build para incluir el complemento de Jib. Cuando se complete, la sección de compilación debería coincidir con lo siguiente.

pom.xml

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <!--  Jib Plugin-->
      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>3.2.0</version>
      </plugin>
       <!--  Maven Resources Plugin-->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>3.1.0</version>
      </plugin>
    </plugins>
  </build>

Generar manifiestos

Skaffold proporciona herramientas integradas para simplificar el desarrollo de contenedores. En este paso, inicializarás Skaffold, que creará automáticamente archivos YAML de Kubernetes básicos. El proceso intenta identificar directorios con definiciones de imágenes de contenedores, como un Dockerfile, y, luego, crea un manifiesto de implementación y servicio para cada uno.

Ejecuta el siguiente comando en la terminal para comenzar el proceso.

d869e0cd38e983d7.png

  1. Ejecuta el siguiente comando en la terminal
skaffold init --generate-manifests
  1. Realice las siguientes acciones cuando se le solicite:
  • Usa las flechas para mover el cursor a Jib Maven Plugin.
  • Presiona la barra espaciadora para seleccionar la opción.
  • Presiona Intro para continuar
  1. Ingresa 8080 para el puerto.
  2. Ingresa y para guardar la configuración.

Se agregan dos archivos al espacio de trabajo skaffold.yaml y deployment.yaml

Resultado de Skaffold:

b33cc1e0c2077ab8.png

Actualiza el nombre de la app

Actualmente, los valores predeterminados incluidos en la configuración no coinciden con el nombre de tu aplicación. Actualiza los archivos para que hagan referencia al nombre de tu aplicación en lugar de los valores predeterminados.

  1. Cambia las entradas en la configuración de Skaffold
  • Abrir skaffold.yaml
  • Selecciona el nombre de la imagen que está configurado actualmente como pom-xml-image.
  • Haz clic con el botón derecho y elige Cambiar todas las ocurrencias.
  • Escribe el nombre nuevo como demo-app
  1. Cambia las entradas en la configuración de Kubernetes
  • Abre el archivo deployment.yaml.
  • Selecciona el nombre de la imagen que está configurado actualmente como pom-xml-image.
  • Haz clic con el botón derecho y elige Cambiar todas las ocurrencias.
  • Escribe el nombre nuevo como demo-app

Habilita el modo de sincronización automática

Para facilitar una experiencia de recarga en caliente optimizada, utilizarás la función de sincronización que proporciona Jib. En este paso, configurarás Skaffold para que utilice esa función en el proceso de compilación.

Ten en cuenta que el perfil "sync" que configuras en la configuración de Skaffold aprovecha el perfil "sync" de Spring que configuraste en el paso anterior, en el que habilitaste la compatibilidad con spring-dev-tools.

  1. Actualiza la configuración de Skaffold

En el archivo skaffold.yaml, reemplaza toda la sección de compilación del archivo por la siguiente especificación. No alteres otras secciones del archivo.

skaffold.yaml

build:
  artifacts:
  - image: demo-app
    jib:
      project: com.example:demo
      type: maven
      args: 
      - --no-transfer-progress
      - -Psync
      fromImage: gcr.io/distroless/java17-debian11:debug
    sync:
      auto: true

Agrega una ruta predeterminada

Crea un archivo llamado HelloController.java en la carpeta /src/main/java/com/example/springboot/.

a624f5dd0c477c09.png

Pega el siguiente contenido en el archivo para crear una ruta http predeterminada.

HelloController.java

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;

@RestController
public class HelloController {

    @Value("${target:local}")
    String target;

    @GetMapping("/") 
    public String hello()
    {
        return String.format("Hello from your %s environment!", target);
    }
}

4. Explicación del proceso de desarrollo

En esta sección, seguirás algunos pasos con el complemento de Cloud Code para aprender los procesos básicos y validar la configuración de tu aplicación de inicio.

Cloud Code se integra con Skaffold para optimizar tu proceso de desarrollo. Cuando realices la implementación en GKE en los siguientes pasos, Cloud Code y Skaffold compilarán automáticamente tu imagen de contenedor, la enviarán a Container Registry y, luego, implementarán tu aplicación en GKE. Esto sucede en segundo plano, abstrayendo los detalles del flujo del desarrollador. Cloud Code también mejora tu proceso de desarrollo, ya que proporciona capacidades tradicionales de depuración y sincronización en caliente para el desarrollo basado en contenedores.

Accede a Google Cloud

Haz clic en el ícono de Cloud Code y selecciona "Acceder a Google Cloud":

1769afd39be372ff.png

Haz clic en "Continuar al acceso".

923bb1c8f63160f9.png

Verifica el resultado en la terminal y abre el vínculo:

517fdd579c34aa21.png

Accede con las credenciales de estudiante de Qwiklabs.

db99b345f7a8e72c.png

Selecciona "Permitir":

a5376553c430ac84.png

Copia el código de verificación y regresa a la pestaña Workstation.

6719421277b92eac.png

Pega el código de verificación y presiona Intro.

e9847cfe3fa8a2ce.png

Agrega un clúster de Kubernetes

  1. Agrega un clúster

62a3b97bdbb427e5.png

  1. Selecciona Google Kubernetes Engine:

9577de423568bbaa.png

  1. Selecciona el proyecto.

c5202fcbeebcd41c.png

  1. Selecciona "quote-cluster", que se creó en la configuración inicial.

366cfd8bc27cd3ed.png

9d68532c9bc4a89b.png

Configura el ID del proyecto actual con la CLI de gcloud

Copia el ID del proyecto para este lab desde la página de Qwiklabs.

fcff2d10007ec5bc.png

Ejecuta el comando de la CLI de gcloud para establecer el ID del proyecto. Reemplaza el ID del proyecto de muestra antes de ejecutar el comando.

gcloud config set project qwiklabs-gcp-project-id

Resultado de muestra:

f1c03d01b7ac112c.png

Depura en Kubernetes

  1. En el panel izquierdo de la parte inferior, selecciona Cloud Code.

60b8e4e95868b561.png

  1. En el panel que aparece en SESIONES DE DESARROLLO, selecciona Debug on Kubernetes.

Desplázate hacia abajo si no ves la opción.

7d30833d96632ca0.png

  1. Selecciona "Sí" para usar el contexto actual.

a024a69b64de7e9e.png

  1. Selecciona "quote-cluster", que se creó durante la configuración inicial.

faebabf372e3caf0.png

  1. Selecciona Container Repository.

fabc6dce48bae1b4.png

  1. Selecciona la pestaña Output en el panel inferior para ver el progreso y las notificaciones.
  2. Selecciona "Kubernetes: Run/Debug - Detailed" en el menú desplegable del canal a la derecha para ver detalles adicionales y registros que se transmiten en vivo desde los contenedores.

86b44c59db58f8f3.png

Espera a que se implemente la aplicación.

9f37706a752829fe.png

  1. Revisa la aplicación implementada en GKE en la consola de Cloud.

6ad220e5d1980756.png

  1. Para volver a la vista simplificada, selecciona "Kubernetes: Run/Debug" en el menú desplegable de la pestaña OUTPUT.
  2. Cuando finalicen la compilación y las pruebas, la pestaña Output dirá: Resource deployment/demo-app status completed successfully y se mostrará una URL: "Forwarded URL from service demo-app: http://localhost:8080".
  3. En la terminal de Cloud Code, coloca el cursor sobre la URL del resultado (http://localhost:8080) y, luego, en la sugerencia que aparece, selecciona Seguir vínculo.

28c5539880194a8e.png

Se abrirá una pestaña nueva y verás el siguiente resultado:

d67253ca16238f49.png

Utiliza puntos de interrupción

  1. Abre la aplicación HelloController.java ubicada en /src/main/java/com/example/springboot/HelloController.java.
  2. Ubica la instrucción de devolución para la ruta raíz que dice return String.format("Hello from your %s environment!", target);.
  3. Para agregar un punto de interrupción a esa línea, haz clic en el espacio en blanco a la izquierda del número de línea. Aparecerá un indicador rojo para señalar que se estableció el punto de interrupción.

5027dc6da2618a39.png

  1. Vuelve a cargar el navegador y observa que el depurador detiene el proceso en el punto de interrupción y te permite investigar las variables y el estado de la aplicación que se ejecuta de forma remota en GKE.

71acfb426623cec2.png

  1. Haz clic hacia abajo en la sección de variables hasta que encuentres la variable "Objetivo".
  2. Observa el valor actual como "local".

a1160d2ed2bb5c82.png

  1. Haz doble clic en el nombre de la variable "target" y, en la ventana emergente,

Cambia el valor a "Cloud Workstations".

e597a556a5c53f32.png

  1. Haz clic en el botón Continuar en el panel de control de depuración.

ec17086191770d0d.png

  1. Revisa la respuesta en tu navegador, que ahora muestra el valor actualizado que acabas de ingresar.

6698a9db9e729925.png

  1. Para quitar el punto de interrupción, haz clic en el indicador rojo que se encuentra a la izquierda del número de línea. Esto evitará que tu código detenga la ejecución en esta línea a medida que avances en este lab.

Recarga en caliente

  1. Cambia la instrucción para que devuelva un valor diferente, como "Hola desde %s Code".
  2. El archivo se guarda y sincroniza automáticamente en los contenedores remotos de GKE.
  3. Actualiza el navegador para ver los resultados actualizados.
  4. Para detener la sesión de depuración, haz clic en el cuadrado rojo de la barra de herramientas de depuración.

a541f928ec8f430e.png c2752bb28d82af86.png

Selecciona "Sí, limpiar después de cada ejecución".

984eb2fa34867d70.png

5. Cómo desarrollar un servicio REST de CRUD simple

En este punto, tu aplicación está completamente configurada para el desarrollo en contenedores y ya completaste el flujo de trabajo de desarrollo básico con Cloud Code. En las siguientes secciones, practicarás lo que aprendiste agregando extremos de servicio REST que se conectan a una base de datos administrada en Google Cloud.

Configura dependencias

El código de la aplicación usa una base de datos para conservar los datos del servicio de REST. Para asegurarte de que las dependencias estén disponibles, agrega lo siguiente en pom.xml:

  1. Abre el archivo pom.xml y agrega lo siguiente a la sección de dependencias de la configuración:

pom.xml

    <!--  Database dependencies-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.flywaydb</groupId>
      <artifactId>flyway-core</artifactId>
    </dependency>
    <dependency>
      <groupId>javax.persistence</groupId>
      <artifactId>javax.persistence-api</artifactId>
      <version>2.2</version>
    </dependency>

Codifica el servicio REST

Quote.java

Crea un archivo llamado Quote.java en /src/main/java/com/example/springboot/ y copia el siguiente código. Define el modelo de entidad para el objeto Quote que se usa en la aplicación.

package com.example.springboot;

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

import java.util.Objects;

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

    @Column(name="quote")
    private String quote;

    @Column(name="author")
    private String author;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.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) {
        this.author = author;
    }

    @Override
    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, quote1.id) &&
                Objects.equals(quote, quote1.quote) &&
                Objects.equals(author, quote1.author);
    }

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

QuoteRepository.java

Crea un archivo llamado QuoteRepository.java en src/main/java/com/example/springboot y copia el siguiente código:

package com.example.springboot;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface QuoteRepository extends JpaRepository<Quote,Integer> {

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

Este código usa JPA para conservar los datos. La clase extiende la interfaz JPARepository de Spring y permite la creación de código personalizado. En el código, agregaste un método personalizado findRandomQuote.

QuoteController.java

Para exponer el extremo del servicio, una clase QuoteController proporcionará esta funcionalidad.

Crea un archivo llamado QuoteController.java en src/main/java/com/example/springboot y copia el siguiente contenido:

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;

@RestController
public class QuoteController {

    private final QuoteRepository quoteRepository;

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

    @GetMapping("/random-quote") 
    public Quote randomQuote()
    {
        return quoteRepository.findRandomQuote();  
    }

    @GetMapping("/quotes") 
    public ResponseEntity<List<Quote>> allQuotes()
    {
        try {
            List<Quote> quotes = new ArrayList<Quote>();
            
            quoteRepository.findAll().forEach(quotes::add);

            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) {
            System.out.println(e.getMessage());
            return new ResponseEntity<List<Quote>>(HttpStatus.INTERNAL_SERVER_ERROR);
        }        
    }

    @PostMapping("/quotes")
    public ResponseEntity<Quote> createQuote(@RequestBody Quote quote) {
        try {
            Quote saved = quoteRepository.save(quote);
            return new ResponseEntity<Quote>(saved, HttpStatus.CREATED);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            return new ResponseEntity<Quote>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }     

    @PutMapping("/quotes/{id}")
    public ResponseEntity<Quote> updateQuote(@PathVariable("id") Integer id, @RequestBody Quote quote) {
        try {
            Optional<Quote> existingQuote = quoteRepository.findById(id);
            
            if(existingQuote.isPresent()){
                Quote updatedQuote = existingQuote.get();
                updatedQuote.setAuthor(quote.getAuthor());
                updatedQuote.setQuote(quote.getQuote());

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

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

}

Agrega configuraciones de bases de datos

application.yaml

Agrega la configuración de la base de datos de backend a la que accede el servicio. Edita (o crea si no está presente) el archivo llamado application.yaml en src/main/resources y agrega una configuración de Spring parametrizada para el backend.

target: local

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

Add Database Migration

Crea carpetas db/migration en src/main/resources

Crea un archivo SQL: V1__create_quotes_table.sql

Pega el siguiente contenido en el archivo.

V1__create_quotes_table.sql

CREATE TABLE quotes(
   id INTEGER PRIMARY KEY,
   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');

Configuración de Kubernetes

Las siguientes adiciones al archivo deployment.yaml permiten que la aplicación se conecte a las instancias de Cloud SQL.

  • TARGET: Configura la variable para indicar el entorno en el que se ejecuta la app.
  • SPRING_PROFILES_ACTIVE: Muestra el perfil de Spring activo, que se configurará en cloud-dev.
  • DB_HOST: Es la IP privada de la base de datos, que se registró cuando se creó la instancia de la base de datos o haciendo clic en SQL en el menú de navegación de la consola de Google Cloud. ¡Cambia el valor!
  • DB_USER y DB_PASS: Se configuran en la instancia de Cloud SQL y se almacenan como un secreto en GCP.

Actualiza tu archivo deployment.yaml con el siguiente contenido.

deployment.yaml

apiVersion: v1
kind: Service
metadata:
  name: demo-app
  labels:
    app: demo-app
spec:
  ports:
  - port: 8080
    protocol: TCP
  clusterIP: None
  selector:
    app: demo-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-app
  labels:
    app: demo-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
    spec:
      containers:
      - name: demo-app
        image: demo-app
        env:
          - 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
            valueFrom:
              secretKeyRef:
                name: gke-cloud-sql-secrets
                key: username
          - name: DB_PASS
            valueFrom:
              secretKeyRef:
                name: gke-cloud-sql-secrets
                key: password
          - name: DB_NAME
            valueFrom:
              secretKeyRef:
                name: gke-cloud-sql-secrets
                key: database

Reemplaza el valor de DB_HOST por la dirección de tu base de datos ejecutando los siguientes comandos en la terminal:

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

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

Abre deployment.yaml y verifica que el valor de DB_HOST se haya actualizado con la IP de la instancia.

fd63c0aede14beba.png

Implementa y valida la aplicación

  1. En el panel que se encuentra en la parte inferior del editor de Cloud Shell, selecciona Cloud Code y, luego, Debug on Kubernetes en la parte superior de la pantalla.

33a5cf41aae91adb.png

  1. Cuando finalicen la compilación y las pruebas, la pestaña Output dirá: Resource deployment/demo-app status completed successfully y se mostrará una URL: "Forwarded URL from service demo-app: http://localhost:8080". Ten en cuenta que, a veces, el puerto puede ser diferente, como 8081. Si es así, establece el valor adecuado. Establece el valor de la URL en la terminal
export URL=localhost:8080
  1. Ver citas aleatorias

Desde la terminal, ejecuta el siguiente comando varias veces en el endpoint de citas aleatorias. Observa que la llamada repetida devuelve diferentes cotizaciones

curl $URL/random-quote | jq
  1. Cómo agregar una cotización

Crea una nueva cotización, con id=6, usando el comando que se indica a continuación y observa cómo se devuelve la solicitud.

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. Cómo borrar una cotización

Ahora, borra la cita que acabas de agregar con el método de eliminación y observa un código de respuesta HTTP/1.1 204.

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

Experimenta un estado de error ejecutando la última solicitud nuevamente después de que ya se haya borrado la entrada.

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

Observa que la respuesta devuelve un HTTP:500 Internal Server Error.

Depura la aplicación

En la sección anterior, encontraste un estado de error en la aplicación cuando intentaste borrar una entrada que no estaba en la base de datos. En esta sección, establecerás un punto de interrupción para ubicar el problema. El error se produjo en la operación DELETE, por lo que trabajarás con la clase QuoteController.

  1. Abrir src/main/java/com/example/springboot/QuoteController.java
  2. Busca el método deleteQuote().
  3. Busca la línea: Optional<Quote> quote = quoteRepository.findById(id);
  4. Haz clic en el espacio en blanco a la izquierda del número de línea para establecer un punto de interrupción en esa línea.
  5. Aparecerá un indicador rojo que indica que se estableció el punto de interrupción.
  6. Vuelve a ejecutar el comando delete.
curl -v -X DELETE $URL/quotes/6
  1. Para volver a la vista de depuración, haz clic en el ícono de la columna izquierda.
  2. Observa la línea de depuración detenida en la clase QuoteController.
  3. En el depurador, haz clic en el ícono step over. b814d39b2e5f3d9e.png
  4. Observa que un código devuelve un error interno del servidor HTTP 500 al cliente, lo que no es ideal.
   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> DELETE /quotes/6 HTTP/1.1
> Host: 127.0.0.1:8080
> 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 127.0.0.1 left intact

Actualiza el código

El código es incorrecto y el bloque else se debe refactorizar para devolver un código de estado HTTP 404 not found.

Corrige el error.

  1. Con la sesión de depuración aún en ejecución, completa la solicitud presionando el botón "Continuar" en el panel de control de depuración.
  2. A continuación, cambia el bloque else al siguiente código:
       else {
                return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
            }

El método debería verse de la siguiente manera:

@DeleteMapping("/quotes/{id}")
public ResponseEntity<HttpStatus> deleteQuote(@PathVariable("id") Integer id) {
        Optional<Quote> quote = quoteRepository.findById(id);
        if (quote.isPresent()) {
            quoteRepository.deleteById(id);
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } else {
            return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
        }
    }
  1. Vuelve a ejecutar el comando de borrado
curl -v -X DELETE $URL/quotes/6
  1. Pasa por el depurador y observa el error HTTP 404 Not Found que se devolvió a la persona que llama.
   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> DELETE /quotes/6 HTTP/1.1
> Host: 127.0.0.1:8080
> 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 127.0.0.1 left intact
  1. Para detener la sesión de depuración, haz clic en el cuadrado rojo de la barra de herramientas de depuración.

12bc3c82f63dcd8a.png

6f19c0f855832407.png

6. Felicitaciones

¡Felicitaciones! En este lab, creaste una nueva aplicación de Java desde cero y la configuraste para que funcione de manera eficaz con contenedores. Luego, implementaste y depuraste tu aplicación en un clúster de GKE remoto siguiendo el mismo flujo de desarrollador que se encuentra en las pilas de aplicaciones tradicionales.

Qué aprendiste

  • Desarrollo de InnerLoop con Cloud Workstations
  • Cómo crear una nueva aplicación inicial de Java
  • Explicación del proceso de desarrollo
  • Cómo desarrollar un servicio REST de CRUD simple
  • Depuración de la aplicación en el clúster de GKE
  • Conecta la aplicación a la base de datos de Cloud SQL