使用 Cloud Workstations 和 Cloud Code 进行开发

1. 概览

此实验演示了旨在简化软件工程师在容器化环境中开发 Java 应用的开发工作流程的功能和特性。典型的容器开发需要用户了解容器和容器构建流程的详细信息。此外,开发者通常必须中断工作流程,离开 IDE 以在远程环境中测试和调试应用。借助本教程中提到的工具和技术,开发者无需离开 IDE 即可高效处理容器化应用。

学习内容

在本实验中,您将学习在 GCP 中使用容器进行开发的各种方法,包括:

  • 使用 Cloud Workstations 进行内部循环开发
  • 创建新的 Java 初始应用
  • 开发流程概览
  • 开发简单的 CRUD REST 服务
  • 在 GKE 集群上调试应用
  • 将应用连接到 Cloud SQL 数据库

58a4cdd3ed7a123a.png

2. 设置和要求

自定进度的环境设置

  1. 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时更新。
  • 项目 ID 在所有 Google Cloud 项目中是唯一的,并且是不可变的(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(通常用 PROJECT_ID 标识)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且此 ID 在项目期间会一直保留。
  • 此外,还有第三个值,即部分 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档
  1. 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。若要关闭资源以避免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除整个项目。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。

启动 Cloudshell 编辑器

本实验经过精心设计和测试,可与 Google Cloud Shell 编辑器搭配使用。如需访问编辑器,请执行以下操作:

  1. 访问您的 Google 项目:https://console.cloud.google.com
  2. 点击右上角的 Cloud Shell 编辑器图标

8560cc8d45e8c112.png

  1. 系统会在窗口底部打开一个新窗格
  2. 点击“打开编辑器”按钮

9e504cb98a6a8005.png

  1. 编辑器将打开,右侧显示资源管理器,中央区域显示编辑器
  2. 屏幕底部还应显示一个终端窗格
  3. 如果终端未打开,请使用 `ctrl+`` 组合键打开新的终端窗口

设置 gcloud

在 Cloud Shell 中,设置项目 ID 以及要将应用部署到的区域。将它们保存为 PROJECT_IDREGION 变量。

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

克隆源代码

本实验的源代码位于 GitHub 上 GoogleCloudPlatform 中的 container-developer-workshop 中。使用以下命令克隆该代码库,然后切换到该目录。

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

预配本实验中使用的基础设施

在本实验中,您将向 GKE 部署代码,并访问存储在 Cloud SQL 数据库中的数据。下面的设置脚本会为您准备此基础架构。预配过程将需要 25 分钟以上。等待脚本运行完毕,然后再继续执行下一部分。

./setup_with_cw.sh &

Cloud Workstations 集群

在 Cloud 控制台中打开 Cloud Workstations。等待集群处于 READY 状态。

305e1a3d63ac7ff6.png

创建工作站配置

如果您的 Cloud Shell 会话已断开连接,请点击“重新连接”,然后运行 gcloud CLI 命令来设置项目 ID。在运行命令之前,请将下面的示例项目 ID 替换为您的 Qwiklabs 项目 ID。

gcloud config set project qwiklabs-gcp-project-id

在终端中运行以下脚本,以创建 Cloud Workstations 配置。

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

验证“配置”部分下的结果。需要 2 分钟才能转换为 READY 状态。

7a6af5aa2807a5f2.png

在控制台中打开 Cloud Workstations 并创建新实例。

a53adeeac81a78c8.png

将名称更改为 my-workstation,然后选择现有配置:codeoss-java

f21c216997746097.png

验证“工作站”部分下的结果。

66a9fc8b20543e32.png

启动工作站

启动并发布工作站。

c91bb69b61ec8635.png

点击地址栏中的相应图标,允许使用第三方 Cookie。1b8923e2943f9bc4.png

fcf9405b6957b7d7.png

点击“网站无法正常运行?”。

36a84c0e2e3b85b.png

点击“允许使用 Cookie”。

2259694328628fba.png

工作站启动后,您会看到 Code OSS IDE 启动。在工作站 IDE 的“开始使用”页面上,点击“标记为完成”

94874fba9b74cc22.png

3. 创建新的 Java 初始应用

在此部分中,您将从头开始创建一个新的 Java Spring Boot 应用,并使用 spring.io 提供的示例应用。打开新终端。

c31d48f2e4938c38.png

克隆示例应用

  1. 创建起始应用
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

如果您看到此消息,请点击“允许”按钮,以便您将内容复制并粘贴到工作站中。

58149777e5cc350a.png

  1. 解压缩应用
unzip sample-app.zip -d sample-app
  1. 打开“sample-app”文件夹
cd sample-app && code-oss-cloud-workstations -r --folder-uri="$PWD"

添加 spring-boot-devtools 和 Jib

如需启用 Spring Boot DevTools,请在编辑器中从探索器找到并打开 pom.xml。接下来,将以下代码粘贴到描述行(即 <description>Demo project for Spring Boot</description>)之后

  1. 在 pom.xml 中添加 spring-boot-devtools

打开项目根目录中的 pom.xml。在 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. 在 pom.xml 中启用 jib-maven-plugin

Jib 是 Google 的一款开源 Java 容器化工具,可让 Java 开发者使用他们熟悉的 Java 工具来构建容器。Jib 是一款快速简单的容器映像构建工具,可处理将应用打包到容器映像中的所有步骤。它不需要您编写 Dockerfile 或安装 Docker,并且直接集成到 Maven 和 Gradle 中。

pom.xml 文件中向下滚动,然后更新 Build 部分以包含 Jib 插件。构建部分完成后应与以下内容一致。

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>

生成清单

Skaffold 提供集成式工具来简化容器开发。在此步骤中,您将初始化 Skaffold,它会自动创建基本的 Kubernetes YAML 文件。该流程会尝试识别包含容器映像定义(例如 Dockerfile)的目录,然后为每个目录创建部署和服务清单。

在终端中执行以下命令以开始该流程。

d869e0cd38e983d7.png

  1. 在终端中执行以下命令
skaffold init --generate-manifests
  1. 当系统提示时:
  • 使用箭头键将光标移动到 Jib Maven Plugin
  • 按空格键即可选择相应选项。
  • 按 Enter 键继续
  1. 输入 8080 作为端口
  2. 输入 y 以保存配置

向工作区添加了两个文件 skaffold.yamldeployment.yaml

Skaffold 输出:

b33cc1e0c2077ab8.png

更新应用名称

配置中包含的默认值目前与您的应用名称不一致。更新文件以引用您的应用名称,而不是默认值。

  1. 更改 Skaffold 配置中的条目
  • 打开“skaffold.yaml
  • 选择当前设置为 pom-xml-image 的图片名称
  • 右键点击,然后选择“更改所有匹配项”
  • demo-app 格式输入新名称
  1. 更改 Kubernetes 配置中的条目
  • 打开 deployment.yaml 文件
  • 选择当前设置为 pom-xml-image 的图片名称
  • 右键点击,然后选择“更改所有匹配项”
  • demo-app 格式输入新名称

启用自动同步模式

为了实现优化的热重载体验,您将使用 Jib 提供的同步功能。在此步骤中,您将配置 Skaffold 以在构建流程中利用该功能。

请注意,您在 Skaffold 配置中配置的“sync”配置文件利用了您在上一步中配置的 Spring“sync”配置文件,您在其中启用了对 spring-dev-tools 的支持。

  1. 更新 Skaffold 配置

skaffold.yaml 文件中,将文件的整个 build 部分替换为以下规范。请勿更改文件的其他部分。

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

添加默认路由

/src/main/java/com/example/springboot/ 文件夹中创建一个名为 HelloController.java 的文件。

a624f5dd0c477c09.png

将以下内容粘贴到文件中,以创建默认的 HTTP 路由。

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. 开发流程概览

在本部分中,您将使用 Cloud Code 插件完成几个步骤,以了解基本流程并验证初始应用的配置和设置。

Cloud Code 与 Skaffold 集成,可简化您的开发流程。在后续步骤中部署到 GKE 时,Cloud Code 和 Skaffold 会自动构建您的容器映像,将其推送到 Container Registry,然后将您的应用部署到 GKE。这会在后台进行,不会让开发者流程察觉到相关细节。Cloud Code 还通过为基于容器的开发提供传统的调试和热同步功能来增强您的开发流程。

登录 Google Cloud

点击 Cloud Code 图标,然后选择“登录 Google Cloud”:

1769afd39be372ff.png

点击“Proceed to sign in”(继续登录)。

923bb1c8f63160f9.png

在终端中查看输出,然后打开链接:

517fdd579c34aa21.png

使用您的 Qwiklabs 学生凭据登录。

db99b345f7a8e72c.png

选择“允许”:

a5376553c430ac84.png

复制验证码,然后返回到工作站标签页。

6719421277b92eac.png

粘贴验证码,然后按 Enter 键。

e9847cfe3fa8a2ce.png

添加 Kubernetes 集群

  1. 添加集群

62a3b97bdbb427e5.png

  1. 选择 Google Kubernetes Engine:

9577de423568bbaa.png

  1. 选择项目。

c5202fcbeebcd41c.png

  1. 选择在初始设置中创建的“quote-cluster”。

366cfd8bc27cd3ed.png

9d68532c9bc4a89b.png

使用 gcloud CLI 设置当前项目 ID

从 Qwiklabs 页面复制本实验的项目 ID。

fcff2d10007ec5bc.png

运行 gcloud CLI 命令以设置项目 ID。在运行命令之前,请替换示例项目 ID。

gcloud config set project qwiklabs-gcp-project-id

示例输出:

f1c03d01b7ac112c.png

在 Kubernetes 上调试

  1. 在左侧窗格底部选择 Cloud Code。

60b8e4e95868b561.png

  1. 在“开发会话”下方显示的面板中,选择“在 Kubernetes 上调试”。

如果看不到该选项,请向下滚动。

7d30833d96632ca0.png

  1. 选择“是”以使用当前上下文。

a024a69b64de7e9e.png

  1. 选择在初始设置期间创建的“quote-cluster”。

faebabf372e3caf0.png

  1. 选择“容器代码库”。

fabc6dce48bae1b4.png

  1. 选择下部窗格中的“输出”标签页,查看进度和通知
  2. 在右侧的渠道下拉菜单中选择“Kubernetes:运行/调试 - 详细”,以查看其他详细信息和从容器实时传输的日志

86b44c59db58f8f3.png

等待应用部署完成。

9f37706a752829fe.png

  1. Cloud 控制台中查看已部署到 GKE 的应用。

6ad220e5d1980756.png

  1. 如需返回简化视图,请从“输出”标签页的下拉菜单中选择“Kubernetes:运行/调试”。
  2. 构建和测试完成后,“输出”标签页将显示 Resource deployment/demo-app status completed successfully,并列出以下网址:“Forwarded 网址 from service demo-app: http://localhost:8080”
  3. 在 Cloud Code 终端中,将鼠标悬停在输出中的网址 (http://localhost:8080) 上,然后在显示的工具提示中选择“打开链接”。

28c5539880194a8e.png

系统会打开一个新标签页,您将看到以下输出内容:

d67253ca16238f49.png

利用断点

  1. 打开位于 /src/main/java/com/example/springboot/HelloController.javaHelloController.java 应用
  2. 找到根路径的 return 语句,该语句的内容为 return String.format("Hello from your %s environment!", target);
  3. 点击行号左侧的空白处,为该行添加一个断点。系统会显示一个红色指示器,表明已设置断点

5027dc6da2618a39.png

  1. 重新加载浏览器,并注意调试程序会在断点处停止进程,以便您调查在 GKE 中远程运行的应用的变量和状态

71acfb426623cec2.png

  1. 向下点击进入“变量”部分,直到找到“目标”变量。
  2. 观察当前值为“local”

a1160d2ed2bb5c82.png

  1. 双击变量名称“target”,然后在弹出式窗口中,

将值更改为“Cloud Workstations”

e597a556a5c53f32.png

  1. 点击调试控制面板中的“继续”按钮

ec17086191770d0d.png

  1. 在浏览器中查看响应,其中现在显示了您刚刚输入的更新后的值。

6698a9db9e729925.png

  1. 点击行号左侧的红色指示器,移除断点。这样可防止您的代码在您继续完成本实验时在此行停止执行。

热重载

  1. 更改语句以返回不同的值,例如“Hello from %s Code”
  2. 文件会自动保存并同步到 GKE 中的远程容器
  3. 刷新浏览器即可查看更新后的结果。
  4. 点击调试工具栏中的红色方块,停止调试会话

a541f928ec8f430e.png c2752bb28d82af86.png

选择“是,每次运行后都清理”。

984eb2fa34867d70.png

5. 开发简单的 CRUD REST 服务

至此,您的应用已完全配置好,可以进行容器化开发,并且您已通过 Cloud Code 完成了基本开发工作流程。在以下部分中,您将练习所学知识,添加连接到 Google Cloud 中托管式数据库的 REST 服务端点。

配置依赖项

应用代码使用数据库来持久保留 REST 服务数据。通过在 pom.xml 中添加以下内容来确保依赖项可用

  1. 打开 pom.xml 文件,并将以下内容添加到配置的依赖项部分

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>

代码 REST 服务

Quote.java

/src/main/java/com/example/springboot/ 中创建一个名为 Quote.java 的文件,并将以下代码复制到其中。这定义了应用中使用的 Quote 对象的实体模型。

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

src/main/java/com/example/springboot 中创建一个名为 QuoteRepository.java 的文件,并将以下代码复制到其中

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();
}

此代码使用 JPA 来持久保留数据。该类扩展了 Spring JPARepository 接口,并允许创建自定义代码。在您添加的代码中,有一个 findRandomQuote 自定义方法。

QuoteController.java

为了公开服务的端点,QuoteController 类将提供此功能。

src/main/java/com/example/springboot 中创建一个名为 QuoteController.java 的文件,并将以下内容复制到其中

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);
        }
    }

}

添加数据库配置

application.yaml

为服务访问的后端数据库添加配置。修改(或创建,如果不存在)src/main/resources 下名为 application.yaml 的文件,并为后端添加参数化 Spring 配置。

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

添加数据库迁移

src/main/resources 下创建文件夹 db/migration

创建 SQL 文件:V1__create_quotes_table.sql

将以下内容粘贴到文件中

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');

Kubernetes 配置

deployment.yaml 文件进行以下添加操作后,应用便可连接到 Cloud SQL 实例。

  • TARGET - 配置变量以指示应用所执行的环境
  • SPRING_PROFILES_ACTIVE - 显示有效的 Spring 配置文件,该配置文件将配置为 cloud-dev
  • DB_HOST - 数据库的私有 IP,在创建数据库实例时已记录,或者通过点击 Google Cloud 控制台导航菜单中的 SQL 找到 - 请更改该值!
  • DB_USER 和 DB_PASS - 在 CloudSQL 实例配置中设置,以 Secret 形式存储在 GCP 中

使用以下内容更新您的 deployment.yaml。

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

在终端中运行以下命令,将 DB_HOST 值替换为数据库的地址:

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

打开 deployment.yaml,并验证 DB_HOST 值是否已更新为实例 IP。

fd63c0aede14beba.png

部署和验证应用

  1. 在 Cloud Shell Editor 底部的窗格中,选择 Cloud Code,然后选择屏幕顶部的“在 Kubernetes 上调试”。

33a5cf41aae91adb.png

  1. 构建和测试完成后,“输出”标签页将显示 Resource deployment/demo-app status completed successfully,并列出网址:“Forwarded 网址 from service demo-app: http://localhost: 8080”(来自服务 demo-app 的转发网址:http://localhost:8080)。请注意,有时端口可能会有所不同,例如 8081。如果需要,请设置相应的值。在终端中设置网址的值
export URL=localhost:8080
  1. 查看随机名言

在终端中,针对 random-quote 端点多次运行以下命令。观察到重复调用返回不同的报价

curl $URL/random-quote | jq
  1. 添加报价

使用下列命令创建 ID 为 6 的新报价,并观察系统回显的请求

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. 删除报价

现在,使用删除方法删除您刚刚添加的引用,并观察 HTTP/1.1 204 响应代码。

curl -v -X DELETE $URL/quotes/6
  1. 服务器错误

在条目已被删除后再次运行最后一个请求,体验错误状态

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

请注意,响应会返回一个 HTTP:500 Internal Server Error

调试应用

在上一部分中,您尝试删除数据库中不存在的条目时,发现应用处于错误状态。在本部分中,您将设置一个断点来定位问题。该错误发生在 DELETE 操作中,因此您将使用 QuoteController 类。

  1. 打开“src/main/java/com/example/springboot/QuoteController.java
  2. 找到 deleteQuote() 方法
  3. 找到以下行:Optional<Quote> quote = quoteRepository.findById(id);
  4. 点击行号左侧的空白处,在该行上设置断点。
  5. 系统会显示一个红色指示图标,表明已设置断点
  6. 再次运行 delete 命令
curl -v -X DELETE $URL/quotes/6
  1. 点击左侧列中的图标,切换回调试视图
  2. 观察到调试行在 QuoteController 类中停止。
  3. 在调试器中,点击 step over 图标 b814d39b2e5f3d9e.png
  4. 请注意,代码会向客户端返回内部服务器错误 HTTP 500,这并不理想。
   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

更新代码

此代码不正确,应重构 else 块以发送回 HTTP 404 未找到状态代码。

更正错误。

  1. 在调试会话仍在运行时,按调试控制面板中的“继续”按钮完成请求。
  2. 接下来,将 else 块更改为以下代码:
       else {
                return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
            }

该方法应如下所示

@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. 重新运行删除命令
curl -v -X DELETE $URL/quotes/6
  1. 逐步调试,并观察返回给调用方的 HTTP 404 Not Found。
   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. 点击调试工具栏中的红色方块,停止调试会话

12bc3c82f63dcd8a.png

6f19c0f855832407.png

6. 恭喜

恭喜!在本实验中,您从头开始创建了一个新的 Java 应用,并将其配置为可与容器有效搭配使用。然后,您按照传统应用堆栈中相同的开发者流程,将应用部署到远程 GKE 集群并进行调试。

您学到的内容

  • 使用 Cloud Workstations 进行内部循环开发
  • 创建新的 Java 初始应用
  • 开发流程概览
  • 开发简单的 CRUD REST 服务
  • 在 GKE 集群上调试应用
  • 将应用连接到 Cloud SQL 数据库