1. 總覽
在第一個程式碼研究室中,您會將圖片儲存在 bucket 中。這會產生檔案建立事件,並由部署在 Cloud Run 中的服務處理。這項服務會呼叫 Vision API 進行圖片分析,並將結果儲存在資料存放區中。

課程內容
- Cloud Storage
- Cloud Run
- Cloud Vision API
- Cloud Firestore
2. 設定和需求
自修實驗室環境設定
- 登入 Google Cloud 控制台,然後建立新專案或重複使用現有專案。如果沒有 Gmail 或 Google Workspace 帳戶,請先建立帳戶。



- 專案名稱是這個專案參與者的顯示名稱。這是 Google API 未使用的字元字串。你隨時可以更新。
- 專案 ID 在所有 Google Cloud 專案中都是不重複的,而且設定後即無法變更。Cloud 控制台會自動產生專屬字串,通常您不需要在意該字串為何。在大多數程式碼研究室中,您需要參照專案 ID (通常標示為
PROJECT_ID)。如果您不喜歡產生的 ID,可以產生另一個隨機 ID。你也可以嘗試使用自己的名稱,看看是否可用。完成這個步驟後就無法變更,且專案期間會維持不變。 - 請注意,有些 API 會使用第三個值,也就是「專案編號」。如要進一步瞭解這三種值,請參閱說明文件。
- 接著,您需要在 Cloud 控制台中啟用帳單,才能使用 Cloud 資源/API。完成這個程式碼研究室的費用不高,甚至可能完全免費。如要關閉資源,避免在本教學課程結束後繼續產生費用,請刪除您建立的資源或專案。Google Cloud 新使用者可參加價值$300 美元的免費試用計畫。
啟動 Cloud Shell
雖然可以透過筆電遠端操作 Google Cloud,但在本程式碼研究室中,您將使用 Google Cloud Shell,這是可在雲端執行的指令列環境。
在 Google Cloud 控制台中,點選右上工具列的 Cloud Shell 圖示:

佈建並連線至環境的作業需要一些時間才能完成。完成後,您應該會看到如下的內容:

這部虛擬機器搭載各種您需要的開發工具,並提供永久的 5GB 主目錄,而且可在 Google Cloud 運作,大幅提升網路效能並強化驗證功能。您可以在瀏覽器中完成本程式碼研究室的所有作業。您不需要安裝任何軟體。
3. 啟用 API
在本實驗室中,您將使用 Cloud Functions 和 Vision API,但首先必須在 Cloud 控制台或使用 gcloud 啟用這些服務。
如要在 Cloud 控制台中啟用 Vision API,請在搜尋列中搜尋 Cloud Vision API:

系統會將您導向 Cloud Vision API 頁面:

按一下 ENABLE 按鈕。
或者,您也可以使用 gcloud 指令列工具,透過 Cloud Shell 啟用。
在 Cloud Shell 中執行下列指令:
gcloud services enable vision.googleapis.com
您應該會看到作業順利完成:
Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.
同時啟用 Cloud Run 和 Cloud Build:
gcloud services enable cloudbuild.googleapis.com \ run.googleapis.com
4. 建立 bucket (主控台)
建立圖片的儲存空間值區。您可以透過 Google Cloud Platform 主控台 ( console.cloud.google.com) 或 Cloud Shell 或本機開發環境的 gsutil 指令列工具執行這項操作。
前往「儲存空間」
在「漢堡」選單 (☰) 中,前往 Storage 頁面。

為 bucket 命名
按一下 CREATE BUCKET 按鈕。

按一下「CONTINUE」。
選擇位置

在所選區域 (此處為 Europe) 建立多區域 bucket。
按一下「CONTINUE」。
選擇預設儲存空間級別

為資料選擇 Standard 儲存空間級別。
按一下「CONTINUE」。
設定存取權控管

由於您將使用可公開存取的圖片,因此希望儲存在這個 bucket 中的所有圖片都具有相同的統一存取權控管機制。
選擇 Uniform 存取權控管選項。
按一下「CONTINUE」。
設定保護/加密

保留預設值 (Google-managed key)),因為您不會使用自己的加密金鑰。
按一下 CREATE,最終完成值區建立程序。
將 allUsers 新增為儲存空間檢視者
前往 Permissions 分頁:

將 allUsers 成員新增至 bucket,並指派 Storage > Storage Object Viewer 角色,如下所示:

按一下「SAVE」。
5. 建立 bucket (gsutil)
您也可以使用 Cloud Shell 中的 gsutil 指令列工具建立 bucket。
在 Cloud Shell 中,為不重複的值區名稱設定變數。Cloud Shell 已將 GOOGLE_CLOUD_PROJECT 設為專屬專案 ID。你可以將該值附加至 bucket 名稱。
例如:
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
在歐洲建立標準多區域:
gsutil mb -l EU gs://${BUCKET_PICTURES}
確認統一值區層級存取權:
gsutil uniformbucketlevelaccess set on gs://${BUCKET_PICTURES}
將 bucket 設為公開:
gsutil iam ch allUsers:objectViewer gs://${BUCKET_PICTURES}
前往控制台的 Cloud Storage 部分,您應該會看到公開 uploaded-pictures bucket:

如上一個步驟所述,測試您是否可以將圖片上傳至值區,並確認上傳的圖片可公開存取。
6. 測試 bucket 的公開存取權
返回儲存空間瀏覽器,您會在清單中看到自己的 bucket,並顯示「公開」存取權 (包括提醒您任何人都能存取該 bucket 內容的警告符號)。

現在值區已可接收圖片。
按一下 bucket 名稱,即可查看 bucket 詳細資料。

您可以在該處嘗試 Upload files 按鈕,測試是否能將圖片新增至值區。檔案選擇器彈出式視窗會要求你選取檔案。選取後,系統會將檔案上傳至儲存空間,並再次顯示自動指派給這個新檔案的public存取權。

Public 存取權標籤旁邊也會顯示小小的連結圖示。點選後,瀏覽器會前往該圖片的公開網址,格式如下:
https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png
其中 BUCKET_NAME 是您為 bucket 選擇的全域專屬名稱,後面則是圖片的檔案名稱。
按一下圖片名稱旁的核取方塊,即可啟用 DELETE 按鈕,並刪除第一張圖片。
7. 準備資料庫
您會將 Vision API 提供的圖片資訊儲存到 Cloud Firestore 資料庫。這項服務是快速、全代管、無伺服器且雲端原生的 NoSQL 文件資料庫。前往 Cloud 控制台的「Firestore」部分,準備資料庫:

系統會提供兩個選項:Native mode 或 Datastore mode。使用原生模式,即可享有離線支援和即時同步等額外功能。
按一下 SELECT NATIVE MODE。

選擇多地區 (這裡選擇歐洲,但最好與函式和儲存空間值區位於相同地區)。
按一下 CREATE DATABASE 按鈕。
資料庫建立完成後,您應該會看到下列內容:

按一下 + START COLLECTION 按鈕,建立新的集合。
為集合命名 pictures。

您不需要建立文件,當新圖片儲存在 Cloud Storage 中,並由 Vision API 分析時,您會以程式輔助方式新增圖片。
按一下「Save」。
Firestore 會在新建立的集合中建立第一個預設文件,您可以安全地刪除該文件,因為其中不含任何實用資訊:

我們將在集合中以程式輔助方式建立的文件會包含 4 個欄位:
- name (字串):上傳圖片的檔案名稱,也是文件的鍵
- 標籤 (字串陣列):Vision API 辨識項目的標籤
- color (字串):主色的十六進位顏色代碼 (即 #ab12ef)
- created (日期):儲存這張圖片中繼資料的時間戳記
- 縮圖 (布林值):選用欄位,如果系統已為這張相片產生縮圖,這個欄位就會存在並設為 true
由於我們會在 Firestore 中搜尋有縮圖的圖片,並依建立日期排序,因此需要建立搜尋索引。
您可以在 Cloud Shell 中使用下列指令建立索引:
gcloud firestore indexes composite create \
--collection-group=pictures \
--field-config field-path=thumbnail,order=descending \
--field-config field-path=created,order=descending
您也可以在 Cloud 控制台中執行這項操作,方法是點選左側導覽欄中的 Indexes,然後建立複合索引,如下所示:

按一下 Create,建立索引可能需要幾分鐘的時間。
8. 複製程式碼
複製程式碼 (如果您在上一個程式碼研究室中尚未複製):
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
接著,您可以前往包含服務的目錄,開始建構實驗室:
cd serverless-photosharing-workshop/services/image-analysis/java
服務的檔案版面配置如下:

9. 探索服務代碼
首先,您會瞭解如何透過 BOM 啟用 Java 用戶端程式庫:pom.xml
首先,請開啟 pom.xml 檔案,其中列出 Java 應用程式的依附元件;重點是 Vision、Cloud Storage 和 Firestore API 的使用方式
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0-M3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>services</groupId>
<artifactId>image-analysis</artifactId>
<version>0.0.1</version>
<name>image-analysis</name>
<description>Spring App for Image Analysis</description>
<properties>
<java.version>17</java.version>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<spring-cloud.version>2023.0.0-M2</spring-cloud.version>
<testcontainers.version>1.19.1</testcontainers.version>
</properties>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.24.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
—
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud.functions</groupId>
<artifactId>functions-framework-api</artifactId>
<version>1.1.0</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-firestore</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-vision</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-storage</artifactId>
</dependency>
這項功能是在 EventController 類別中實作。每次將新圖片上傳至值區時,服務都會收到通知以進行處理:
@RestController
public class EventController {
private static final Logger logger = Logger.getLogger(EventController.class.getName());
private static final List<String> requiredFields = Arrays.asList("ce-id", "ce-source", "ce-type", "ce-specversion");
@RequestMapping(value = "/", method = RequestMethod.POST)
public ResponseEntity<String> receiveMessage(
@RequestBody Map<String, Object> body, @RequestHeader Map<String, String> headers) throws IOException, InterruptedException, ExecutionException {
...
}
程式碼會繼續驗證 Cloud Events 標頭:
System.out.println("Header elements");
for (String field : requiredFields) {
if (headers.get(field) == null) {
String msg = String.format("Missing expected header: %s.", field);
System.out.println(msg);
return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
} else {
System.out.println(field + " : " + headers.get(field));
}
}
System.out.println("Body elements");
for (String bodyField : body.keySet()) {
System.out.println(bodyField + " : " + body.get(bodyField));
}
if (headers.get("ce-subject") == null) {
String msg = "Missing expected header: ce-subject.";
System.out.println(msg);
return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
}
現在可以建構要求,程式碼會準備這類要求,以便傳送至 Vision API:
try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
List<AnnotateImageRequest> requests = new ArrayList<>();
ImageSource imageSource = ImageSource.newBuilder()
.setGcsImageUri("gs://" + bucketName + "/" + fileName)
.build();
Image image = Image.newBuilder()
.setSource(imageSource)
.build();
Feature featureLabel = Feature.newBuilder()
.setType(Type.LABEL_DETECTION)
.build();
Feature featureImageProps = Feature.newBuilder()
.setType(Type.IMAGE_PROPERTIES)
.build();
Feature featureSafeSearch = Feature.newBuilder()
.setType(Type.SAFE_SEARCH_DETECTION)
.build();
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
.addFeatures(featureLabel)
.addFeatures(featureImageProps)
.addFeatures(featureSafeSearch)
.setImage(image)
.build();
requests.add(request);
我們要求 Vision API 提供 3 項主要功能:
- 標籤偵測:瞭解圖片內容
- 圖片屬性:提供圖片的有趣屬性 (我們感興趣的是圖片的主色)
- 安全搜尋:判斷圖片是否適合顯示 (不應含有成人 / 醫療 / 煽情 / 暴力內容)
此時,我們可以呼叫 Vision API:
...
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result = vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
...
以下是 Vision API 的回應範例:
{
"faceAnnotations": [],
"landmarkAnnotations": [],
"logoAnnotations": [],
"labelAnnotations": [
{
"locations": [],
"properties": [],
"mid": "/m/01yrx",
"locale": "",
"description": "Cat",
"score": 0.9959855675697327,
"confidence": 0,
"topicality": 0.9959855675697327,
"boundingPoly": null
},
✄ - - - ✄
],
"textAnnotations": [],
"localizedObjectAnnotations": [],
"safeSearchAnnotation": {
"adult": "VERY_UNLIKELY",
"spoof": "UNLIKELY",
"medical": "VERY_UNLIKELY",
"violence": "VERY_UNLIKELY",
"racy": "VERY_UNLIKELY",
"adultConfidence": 0,
"spoofConfidence": 0,
"medicalConfidence": 0,
"violenceConfidence": 0,
"racyConfidence": 0,
"nsfwConfidence": 0
},
"imagePropertiesAnnotation": {
"dominantColors": {
"colors": [
{
"color": {
"red": 203,
"green": 201,
"blue": 201,
"alpha": null
},
"score": 0.4175916016101837,
"pixelFraction": 0.44456374645233154
},
✄ - - - ✄
]
}
},
"error": null,
"cropHintsAnnotation": {
"cropHints": [
{
"boundingPoly": {
"vertices": [
{ "x": 0, "y": 118 },
{ "x": 1177, "y": 118 },
{ "x": 1177, "y": 783 },
{ "x": 0, "y": 783 }
],
"normalizedVertices": []
},
"confidence": 0.41695669293403625,
"importanceFraction": 1
}
]
},
"fullTextAnnotation": null,
"webDetection": null,
"productSearchResults": null,
"context": null
}
如果沒有傳回任何錯誤,我們就可以繼續,這就是我們有這個 if 區塊的原因:
if (responses.size() == 0) {
logger.info("No response received from Vision API.");
return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
}
AnnotateImageResponse response = responses.get(0);
if (response.hasError()) {
logger.info("Error: " + response.getError().getMessage());
return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
}
我們要取得圖片中辨識到的事物、類別或主題的標籤:
List<String> labels = response.getLabelAnnotationsList().stream()
.map(annotation -> annotation.getDescription())
.collect(Collectors.toList());
logger.info("Annotations found:");
for (String label: labels) {
logger.info("- " + label);
}
我們想知道圖片的主色:
String mainColor = "#FFFFFF";
ImageProperties imgProps = response.getImagePropertiesAnnotation();
if (imgProps.hasDominantColors()) {
DominantColorsAnnotation colorsAnn = imgProps.getDominantColors();
ColorInfo colorInfo = colorsAnn.getColors(0);
mainColor = rgbHex(
colorInfo.getColor().getRed(),
colorInfo.getColor().getGreen(),
colorInfo.getColor().getBlue());
logger.info("Color: " + mainColor);
}
讓我們檢查圖片是否安全無虞:
boolean isSafe = false;
if (response.hasSafeSearchAnnotation()) {
SafeSearchAnnotation safeSearch = response.getSafeSearchAnnotation();
isSafe = Stream.of(
safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
safeSearch.getSpoof(), safeSearch.getViolence())
.allMatch( likelihood ->
likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
);
logger.info("Safe? " + isSafe);
}
我們會檢查成人 / 惡搞 / 醫療 / 暴力 / 煽情特徵,判斷這些特徵不太可能或非常可能存在。
如果安全搜尋結果沒問題,我們可以在 Firestore 中儲存中繼資料:
// Saving result to Firestore
if (isSafe) {
ApiFuture<WriteResult> writeResult =
eventService.storeImage(fileName, labels,
mainColor);
logger.info("Picture metadata saved in Firestore at " +
writeResult.get().getUpdateTime());
}
...
public ApiFuture<WriteResult> storeImage(String fileName,
List<String> labels,
String mainColor) {
FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
Firestore pictureStore = firestoreOptions.getService();
DocumentReference doc = pictureStore.collection("pictures").document(fileName);
Map<String, Object> data = new HashMap<>();
data.put("labels", labels);
data.put("color", mainColor);
data.put("created", new Date());
return doc.set(data, SetOptions.merge());
}
10. 使用 GraalVM 建構應用程式映像檔
在這個選用步驟中,您將使用 GraalVM 建構 JIT based app image 和 Native Java app image。
如要執行建構作業,請務必安裝並設定適當的 JDK 和 native-image 建構工具。你可以選擇以下做法:
To start,下載 GraalVM 22.3.x Community Edition,然後按照 GraalVM 安裝頁面的指示操作。
SDKMAN! 可大幅簡化這個程序。
如要使用 SDKman 安裝適當的 JDK 發行版本,請先使用安裝指令:
sdk install java 17.0.8-graal
指示 SDKman 使用這個版本,進行 JIT 和 AOT 建構:
sdk use java 17.0.8-graal
在 Cloudshell 中,您可以執行下列簡單指令,輕鬆安裝 GraalVM 和原生映像檔公用程式:
# download GraalVM wget https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_linux-x64_bin.tar.gz tar -xzf graalvm-jdk-17_linux-x64_bin.tar.gz ls -lart # configure Java 17 and GraalVM for Java 17 # note the name of the latest GraalVM version, as unpacked by the tar command echo Existing JVM: $JAVA_HOME cd graalvm-jdk-17.0.8+9.1 export JAVA_HOME=$PWD cd bin export PATH=$PWD:$PATH echo JAVA HOME: $JAVA_HOME echo PATH: $PATH cd ../.. # validate the version with java -version # observe Java(TM) SE Runtime Environment Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14) Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14, mixed mode, sharing)
首先,設定 GCP 專案環境變數:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
接著,您可以前往包含服務的目錄,開始建構實驗室:
cd serverless-photosharing-workshop/services/image-analysis/java
建構 JIT 應用程式映像檔:
./mvnw package
在終端機中觀察建構記錄:
... [INFO] Results: [INFO] [INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] [INFO] --- maven-jar-plugin:3.3.0:jar (default-jar) @ image-analysis --- [INFO] Building jar: /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar [INFO] [INFO] --- spring-boot-maven-plugin:3.2.0-M3:repackage (repackage) @ image-analysis --- [INFO] Replacing main artifact /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar with repackaged archive, adding nested dependencies in BOOT-INF/. [INFO] The original artifact has been renamed to /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar.original [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 15.335 s [INFO] Finished at: 2023-10-10T19:33:25Z [INFO] ------------------------------------------------------------------------
建構 Native(使用 AOT) 映像檔:
./mvnw native:compile -Pnative
在終端機中觀察建構記錄,包括原生映像檔建構記錄:
請注意,建構作業需要較長的時間,視您測試的機器而定。
...
[2/7] Performing analysis... [*********] (124.5s @ 4.53GB)
29,732 (93.19%) of 31,905 classes reachable
60,161 (70.30%) of 85,577 fields reachable
261,973 (67.29%) of 389,319 methods reachable
2,940 classes, 2,297 fields, and 97,421 methods registered for reflection
81 classes, 90 fields, and 62 methods registered for JNI access
4 native libraries: dl, pthread, rt, z
[3/7] Building universe... (11.7s @ 4.67GB)
[4/7] Parsing methods... [***] (6.1s @ 5.91GB)
[5/7] Inlining methods... [****] (4.5s @ 4.39GB)
[6/7] Compiling methods... [******] (35.3s @ 4.60GB)
[7/7] Creating image... (12.9s @ 4.61GB)
80.08MB (47.43%) for code area: 190,483 compilation units
73.81MB (43.72%) for image heap: 660,125 objects and 189 resources
14.95MB ( 8.86%) for other data
168.84MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area: Top 10 object types in image heap:
2.66MB com.google.cloud.vision.v1p4beta1 18.51MB byte[] for code metadata
2.60MB com.google.cloud.vision.v1 9.27MB java.lang.Class
2.49MB com.google.protobuf 7.34MB byte[] for reflection metadata
2.40MB com.google.cloud.vision.v1p3beta1 6.35MB byte[] for java.lang.String
2.17MB com.google.storage.v2 5.72MB java.lang.String
2.12MB com.google.firestore.v1 4.46MB byte[] for embedded resources
1.64MB sun.security.ssl 4.30MB c.oracle.svm.core.reflect.SubstrateMethodAccessor
1.51MB i.g.xds.shaded.io.envoyproxy.envoy.config.core.v3 4.27MB byte[] for general heap data
1.47MB com.google.cloud.vision.v1p2beta1 2.50MB com.oracle.svm.core.hub.DynamicHubCompanion
1.34MB i.g.x.shaded.io.envoyproxy.envoy.config.route.v3 1.17MB java.lang.Object[]
58.34MB for 977 more packages 9.19MB for 4667 more object types
------------------------------------------------------------------------------------------------------------------------
13.5s (5.7% of total time) in 75 GCs | Peak RSS: 9.44GB | CPU load: 6.13
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
/home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis (executable)
/home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis.build_artifacts.txt (txt)
========================================================================================================================
Finished generating '/home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis' in 3m 57s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 04:28 min
[INFO] Finished at: 2023-10-10T19:53:30Z
[INFO] ------------------------------------------------------------------------
11. 建構及發布容器映像檔
現在讓我們建構兩個不同版本的容器映像檔:一個是 JIT image,另一個是 Native Java image。
首先,設定 GCP 專案環境變數:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
建構 JIT 映像檔:
./mvnw spring-boot:build-image -Pji
在終端機中觀察建構記錄:
[INFO] [creator] Timer: Saving docker.io/library/image-analysis-maven-jit:latest... started at 2023-10-10T20:00:31Z [INFO] [creator] *** Images (4c84122a1826): [INFO] [creator] docker.io/library/image-analysis-maven-jit:latest [INFO] [creator] Timer: Saving docker.io/library/image-analysis-maven-jit:latest... ran for 6.975913605s and ended at 2023-10-10T20:00:38Z [INFO] [creator] Timer: Exporter ran for 8.068588001s and ended at 2023-10-10T20:00:38Z [INFO] [creator] Timer: Cache started at 2023-10-10T20:00:38Z [INFO] [creator] Reusing cache layer 'paketo-buildpacks/syft:syft' [INFO] [creator] Adding cache layer 'buildpacksio/lifecycle:cache.sbom' [INFO] [creator] Timer: Cache ran for 200.449002ms and ended at 2023-10-10T20:00:38Z [INFO] [INFO] Successfully built image 'docker.io/library/image-analysis-maven-jit:latest' [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 43.887 s [INFO] Finished at: 2023-10-10T20:00:39Z [INFO] ------------------------------------------------------------------------
建構 AOT(原生) 映像檔:
./mvnw spring-boot:build-image -Pnative
在終端機中觀察建構記錄,包括原生映像檔建構記錄。
注意:
- 建構時間會較長,視您測試的機器而定
- 圖片可使用 UPX 進一步壓縮,但會對啟動效能造成些微負面影響,因此這個建構版本不會使用 UPX,這始終是微小的取捨
... [INFO] [creator] Saving docker.io/library/image-analysis-maven-native:latest... [INFO] [creator] *** Images (13167702674e): [INFO] [creator] docker.io/library/image-analysis-maven-native:latest [INFO] [creator] Adding cache layer 'paketo-buildpacks/bellsoft-liberica:native-image-svm' [INFO] [creator] Adding cache layer 'paketo-buildpacks/syft:syft' [INFO] [creator] Adding cache layer 'paketo-buildpacks/native-image:native-image' [INFO] [creator] Adding cache layer 'buildpacksio/lifecycle:cache.sbom' [INFO] [INFO] Successfully built image 'docker.io/library/image-analysis-maven-native:latest' [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 03:37 min [INFO] Finished at: 2023-10-10T20:05:16Z [INFO] ------------------------------------------------------------------------
驗證映像檔是否已建構完成:
docker images | grep image-analysis
將這兩個映像檔加上標記並推送至 GCR:
# JIT image
docker tag image-analysis-maven-jit gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit
docker push gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit
# Native(AOT) image
docker tag image-analysis-maven-native gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-native
docker push gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-native
12. 部署至 Cloud Run
部署服務。
您將部署服務兩次,一次使用 JIT 映像檔,另一次使用 AOT(原生) 映像檔。這兩項服務部署作業會平行處理來自 Bucket 的相同圖片,以供比較。
首先,設定 GCP 專案環境變數:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
gcloud config set project ${GOOGLE_CLOUD_PROJECT}
gcloud config set run/region
gcloud config set run/platform managed
gcloud config set eventarc/location europe-west1
部署 JIT 映像檔,並在控制台中觀察部署記錄:
gcloud run deploy image-analysis-jit \
--image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit \
--region europe-west1 \
--memory 2Gi --allow-unauthenticated
...
Deploying container to Cloud Run service [image-analysis-jit] in project [...] region [europe-west1]
✓ Deploying... Done.
✓ Creating Revision...
✓ Routing traffic...
✓ Setting IAM Policy...
Done.
Service [image-analysis-jit] revision [image-analysis-jvm-00009-huc] has been deployed and is serving 100 percent of traffic.
Service URL: https://image-analysis-jit-...-ew.a.run.app
部署原生映像檔,並觀察主控台中的部署記錄:
gcloud run deploy image-analysis-native \
--image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-native \
--region europe-west1 \
--memory 2Gi --allow-unauthenticated
...
Deploying container to Cloud Run service [image-analysis-native] in project [...] region [europe-west1]
✓ Deploying... Done.
✓ Creating Revision...
✓ Routing traffic...
✓ Setting IAM Policy...
Done.
Service [image-analysis-native] revision [image-analysis-native-00005-ben] has been deployed and is serving 100 percent of traffic.
Service URL: https://image-analysis-native-...-ew.a.run.app
13. 設定 Eventarc 觸發條件
Eventarc 提供標準化解決方案,可管理已分離微服務之間的狀態變更流程 (稱為「事件」)。觸發之後,Eventarc 會透過 Pub/Sub 訂閱項目將這些事件轉送至不同的目的地 (請參閱本文的「事件目的地」一節),同時為您管理傳送、安全防護、授權、觀測和錯誤處理工作。
您可以建立 Eventarc 觸發條件,讓 Cloud Run 服務接收特定事件或事件集的通知。指定觸發條件的篩選器,即可設定事件的轉送路徑,包括事件來源和目標 Cloud Run 服務。
首先,設定 GCP 專案環境變數:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
gcloud config set project ${GOOGLE_CLOUD_PROJECT}
gcloud config set run/region
gcloud config set run/platform managed
gcloud config set eventarc/location europe-west1
將 pubsub.publisher 授予 Cloud Storage 服務帳戶:
SERVICE_ACCOUNT="$(gsutil kms serviceaccount -p ${GOOGLE_CLOUD_PROJECT})"
gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role='roles/pubsub.publisher'
為 JIT 和原生服務映像檔設定 Eventarc 觸發條件,以處理圖片:
gcloud eventarc triggers list --location=eu
gcloud eventarc triggers create image-analysis-jit-trigger \
--destination-run-service=image-analysis-jit \
--destination-run-region=europe-west1 \
--location=eu \
--event-filters="type=google.cloud.storage.object.v1.finalized" \
--event-filters="bucket=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}" \
--service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com
gcloud eventarc triggers create image-analysis-native-trigger \
--destination-run-service=image-analysis-native \
--destination-run-region=europe-west1 \
--location=eu \
--event-filters="type=google.cloud.storage.object.v1.finalized" \
--event-filters="bucket=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}" \
--service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com
您會發現已建立兩個觸發條件:
gcloud eventarc triggers list --location=eu
14. 測試服務版本
服務部署成功後,請將圖片發布至 Cloud Storage,確認服務是否已叫用、Vision API 傳回的內容,以及中繼資料是否儲存在 Firestore 中。
返回 Cloud Storage,然後按一下我們在實驗室開始時建立的 bucket:

進入值區詳細資料頁面後,按一下 Upload files 按鈕即可上傳圖片。
舉例來說,程式碼基底的 /services/image-analysis/java 下提供 GeekHour.jpeg 圖片。選取圖片並按下 Open button:

現在可以檢查服務的執行情況,先輸入 image-analysis-jit,再輸入 image-analysis-native。
在「漢堡」選單 (☰) 中,前往 Cloud Run > image-analysis-jit 服務。
按一下「記錄」,觀察輸出內容:

在記錄清單中,我確實看到 JIT 服務 image-analysis-jit 已遭叫用。
記錄會顯示服務執行的開始和結束時間。中間則會顯示我們在函式中加入的記錄,記錄陳述式位於 INFO 層級。我們看到:
- 觸發函式的事件詳細資料,
- Vision API 呼叫的原始結果,
- 我們在您上傳的圖片中找到的標籤,
- 主要顏色資訊,
- 圖片是否適合顯示
- 最終,這些圖片中繼資料會儲存在 Firestore 中。
請為 image-analysis-native 服務重複執行這個程序。
在「漢堡」選單 (☰) 中,前往 Cloud Run > image-analysis-native 服務。
按一下「記錄」,觀察輸出內容:

現在請觀察圖片中繼資料是否已儲存在 Fiorestore 中。
再次從「漢堡」選單 (☰) 前往 Firestore 部分。在 Data 子專區 (預設顯示),您應該會看到 pictures 集合新增的文件,對應您剛上傳的圖片:

15. 清除 (選用)
如果您不打算繼續進行本系列的其他實驗室,可以清除資源來節省費用,並成為優質的雲端使用者。您可以按照下列方式個別清除資源。
刪除值區:
gsutil rb gs://${BUCKET_PICTURES}
刪除函式:
gcloud functions delete picture-uploaded --region europe-west1 -q
從集合中選取「刪除集合」,即可刪除 Firestore 集合:

或者,您也可以刪除整個專案:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
16. 恭喜!
恭喜!您已成功實作專案的第一項重要服務!
涵蓋內容
- Cloud Storage
- Cloud Run
- Cloud Vision API
- Cloud Firestore
- 原生 Java 圖片