ย้ายข้อมูลจาก App Engine Blobstore ไปยัง Cloud Storage (โมดูล 16)

1. ภาพรวม

ชุด Codelab ของ Serverless Migration Station (บทแนะนำแบบลงมือทำด้วยตนเอง) และวิดีโอที่เกี่ยวข้องมีจุดมุ่งหมายเพื่อช่วยให้นักพัฒนาซอฟต์แวร์ Google Cloud Serverless ปรับปรุงแอปพลิเคชันให้ทันสมัยโดยแนะนำการย้ายข้อมูลอย่างน้อย 1 รายการ ซึ่งส่วนใหญ่เป็นการย้ายออกจากบริการเดิม การทำเช่นนี้จะช่วยให้แอปของคุณพกพาได้มากขึ้น รวมถึงมีตัวเลือกและความยืดหยุ่นมากขึ้น ซึ่งจะช่วยให้คุณผสานรวมและเข้าถึงผลิตภัณฑ์ Cloud ได้หลากหลายมากขึ้น และอัปเกรดเป็นภาษาที่ใหม่กว่าได้ง่ายขึ้น แม้ว่าในตอนแรกจะมุ่งเน้นไปที่ผู้ใช้ Cloud รุ่นแรกๆ ซึ่งส่วนใหญ่เป็นนักพัฒนาซอฟต์แวร์ App Engine (สภาพแวดล้อมมาตรฐาน) แต่ซีรีส์นี้ก็กว้างพอที่จะรวมแพลตฟอร์มแบบ Serverless อื่นๆ เช่น Cloud Functions และ Cloud Run หรือที่อื่นๆ หากเกี่ยวข้อง

Codelab นี้จะสอนวิธีย้ายข้อมูลจาก App Engine Blobstore ไปยัง Cloud Storage นอกจากนี้ ยังมีการย้ายข้อมูลโดยนัยจากแหล่งที่มาต่อไปนี้ด้วย

ดูข้อมูลทีละขั้นตอนเพิ่มเติมได้ที่โมดูลการย้ายข้อมูลที่เกี่ยวข้อง

คุณจะได้เรียนรู้วิธีต่อไปนี้

  • เพิ่มการใช้ API/ไลบรารี Blobstore ของ App Engine
  • จัดเก็บการอัปโหลดของผู้ใช้ไปยังบริการ Blobstore
  • เตรียมพร้อมสำหรับขั้นตอนถัดไปในการย้ายข้อมูลไปยัง Cloud Storage

สิ่งที่ต้องมี

แบบสำรวจ

คุณจะใช้บทแนะนำนี้อย่างไร

อ่านอย่างเดียว อ่านและทำแบบฝึกหัด

คุณจะให้คะแนนประสบการณ์การใช้งาน Python เท่าใด

ผู้ฝึกหัด ระดับกลาง ผู้ชำนาญ

คุณจะให้คะแนนประสบการณ์การใช้บริการ Google Cloud เท่าใด

ผู้ฝึกหัด ผู้มีทักษะระดับกลาง ผู้ชำนาญ

2. ฉากหลัง

Codelab นี้เริ่มต้นด้วยแอปตัวอย่างจากโมดูล 15 และสาธิตวิธีย้ายข้อมูลจาก Blobstore (และ NDB) ไปยัง Cloud Storage (และ Cloud NDB) กระบวนการย้ายข้อมูลเกี่ยวข้องกับการแทนที่ทรัพยากร Dependency ในบริการที่รวมมากับ App Engine เดิม ซึ่งช่วยให้คุณย้ายแอปไปยังแพลตฟอร์มแบบ Serverless อื่นของ Cloud หรือแพลตฟอร์มโฮสติ้งอื่นๆ ได้หากต้องการ

การย้ายข้อมูลนี้ต้องใช้ความพยายามมากกว่าการย้ายข้อมูลอื่นๆ ในชุดนี้เล็กน้อย Blobstore มีทรัพยากร Dependency ในเฟรมเวิร์กเว็บแอปเดิม และนี่คือสาเหตุที่แอปตัวอย่างใช้เฟรมเวิร์ก webapp2 แทน Flask บทแนะนำนี้จะอธิบายการย้ายข้อมูลไปยัง Cloud Storage, Cloud NDB, Flask และ Python 3

แอปยังคงลงทะเบียน "การเข้าชม" ของผู้ใช้ปลายทางและแสดงการเข้าชมล่าสุด 10 รายการ แต่ Codelab ก่อนหน้า (โมดูล 15) ได้เพิ่มฟังก์ชันการทำงานใหม่เพื่อรองรับการใช้งาน Blobstore โดยแอปจะแจ้งให้ผู้ใช้ปลายทางอัปโหลดอาร์ติแฟกต์ (ไฟล์) ที่สอดคล้องกับ "การเข้าชม" ของตน ผู้ใช้จะเลือกดำเนินการดังกล่าวหรือเลือก "ข้าม" เพื่อเลือกไม่ใช้ก็ได้ ไม่ว่าผู้ใช้จะตัดสินใจอย่างไร หน้าถัดไปจะแสดงผลลัพธ์เหมือนกับเวอร์ชันก่อนหน้าของแอปนี้ โดยแสดงการเข้าชมล่าสุด สิ่งที่แตกต่างออกไปคือการเข้าชมที่มีอาร์ติแฟกต์ที่เกี่ยวข้องจะมีลิงก์ "ดู" สำหรับแสดงอาร์ติแฟกต์ของการเข้าชม Codelab นี้จะใช้การย้ายข้อมูลที่กล่าวถึงก่อนหน้านี้พร้อมกับคงฟังก์ชันการทำงานที่อธิบายไว้

3. การตั้งค่า/งานเตรียมความพร้อม

ก่อนที่จะเข้าสู่ส่วนหลักของบทแนะนำนี้ เรามาตั้งค่าโปรเจ็กต์ รับโค้ด แล้วทำให้แอปพื้นฐานใช้งานได้กันก่อน เพื่อให้ทราบว่าเราเริ่มต้นด้วยโค้ดที่ใช้งานได้

1. ตั้งค่าโปรเจ็กต์

หากคุณติดตั้งใช้งานแอปโมดูล 15 แล้ว เราขอแนะนำให้ใช้โปรเจ็กต์ (และโค้ด) เดียวกันซ้ำ หรือคุณจะสร้างโปรเจ็กต์ใหม่หรือใช้โปรเจ็กต์อื่นที่มีอยู่ซ้ำก็ได้ ตรวจสอบว่าโปรเจ็กต์มีบัญชีสำหรับการเรียกเก็บเงินที่ใช้งานอยู่และเปิดใช้ App Engine แล้ว

2. ดาวน์โหลดแอปตัวอย่างพื้นฐาน

ข้อกำหนดเบื้องต้นอย่างหนึ่งของ Codelab นี้คือการมีแอปตัวอย่างโมดูล 15 ที่ใช้งานได้ หากไม่มี คุณสามารถรับได้จากโฟลเดอร์ "START" ของโมดูล 15 (ลิงก์ด้านล่าง) Codelab นี้จะแนะนำคุณทีละขั้นตอน โดยสรุปด้วยโค้ดที่คล้ายกับโค้ดในโฟลเดอร์ "FINISH" ของโมดูล 16

ไดเรกทอรีของไฟล์ START ของโมดูล 15 ควรมีลักษณะดังนี้

$ ls
README.md       app.yaml        main-gcs.py     main.py         templates

ไฟล์ main-gcs.py เป็นเวอร์ชันทางเลือกของ main.py จากโมดูล 15 ซึ่งช่วยให้เลือกที่เก็บข้อมูล Cloud Storage ที่แตกต่างจากค่าเริ่มต้นของ URL ที่กำหนดของแอปตามรหัสของโปรเจ็กต์: PROJECT_ID.appspot.com ไฟล์นี้ไม่มีส่วนเกี่ยวข้องกับ Codelab (โมดูล 16) นี้ นอกเหนือจากเทคนิคการย้ายข้อมูลที่คล้ายกันซึ่งสามารถใช้กับไฟล์นั้นได้หากต้องการ

3. (อีกครั้ง) ติดตั้งใช้งานแอปพื้นฐาน

ขั้นตอนการเตรียมงานที่เหลือซึ่งคุณต้องดำเนินการในตอนนี้มีดังนี้

  1. ทำความคุ้นเคยกับเครื่องมือบรรทัดคำสั่ง gcloud อีกครั้ง
  2. ทำให้แอปตัวอย่างใช้งานได้อีกครั้งด้วย gcloud app deploy
  3. ยืนยันว่าแอปทำงานใน App Engine ได้โดยไม่มีปัญหา

เมื่อคุณทำตามขั้นตอนเหล่านั้นเรียบร้อยแล้ว ให้ยืนยันว่าแอปโมดูล 15 ทำงานได้ หน้าแรกจะทักทายผู้ใช้ด้วยแบบฟอร์มที่แจ้งให้อัปโหลดไฟล์อาร์ติแฟกต์การเข้าชม พร้อมตัวเลือกปุ่ม "ข้าม" เพื่อเลือกไม่ใช้

f5b5f9f19d8ae978.png

เมื่อผู้ใช้อัปโหลดไฟล์หรือข้าม แอปจะแสดงหน้า "การเข้าชมล่าสุด" ที่คุ้นเคย ดังนี้

f5ac6b98ee8a34cb.png

การเข้าชมที่มีอาร์ติแฟกต์จะมีลิงก์ "ดู" ทางด้านขวาของการประทับเวลาการเข้าชมเพื่อแสดง (หรือดาวน์โหลด) อาร์ติแฟกต์ เมื่อยืนยันฟังก์ชันการทำงานของแอปแล้ว คุณก็พร้อมที่จะย้ายข้อมูลจากบริการเดิมของ App Engine (webapp2, NDB, Blobstore) ไปยังทางเลือกที่ทันสมัย (Flask, Cloud NDB, Cloud Storage)

4. อัปเดตไฟล์การกำหนดค่า

ไฟล์การกำหนดค่า 3 ไฟล์จะเข้ามามีบทบาทในแอปเวอร์ชันอัปเดต โดยงานที่จำเป็นมีดังนี้

  1. อัปเดตไลบรารีของบุคคลที่สามในตัวที่จำเป็นใน app.yaml รวมถึงเปิดโอกาสให้ย้ายข้อมูลไปยัง Python 3
  2. เพิ่ม requirements.txt ซึ่งระบุไลบรารีที่จำเป็นทั้งหมดที่ไม่ได้อยู่ในตัว
  3. เพิ่ม appengine_config.py เพื่อให้แอปสามารถรองรับทั้งไลบรารีของบุคคลที่สามในตัวและไม่ใช่ในตัว

app.yaml

แก้ไขไฟล์ app.yaml โดยอัปเดตส่วน libraries นำ jinja2 ออก แล้วเพิ่ม grpcio, setuptools และ ssl เลือกเวอร์ชันล่าสุดที่พร้อมใช้งานสำหรับทั้ง 3 ไลบรารี นอกจากนี้ ให้เพิ่มคำสั่ง runtime ของ Python 3 ด้วย แต่ทำเครื่องหมายเป็นความคิดเห็น เมื่อเสร็จแล้ว ควรมีลักษณะดังนี้ (หากคุณเลือก Python 3.9)

ก่อน:

runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

libraries:
- name: jinja2
  version: latest

หลัง:

#runtime: python39
runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

libraries:
- name: grpcio
  version: latest
- name: setuptools
  version: latest
- name: ssl
  version: latest

การเปลี่ยนแปลงส่วนใหญ่เกี่ยวข้องกับไลบรารีในตัวของ Python 2 ที่พร้อมใช้งานบนเซิร์ฟเวอร์ App Engine (คุณจึงไม่ต้องรวมไลบรารีด้วยตนเอง) เรานำ Jinja2 ออกเนื่องจากมาพร้อมกับ Flask ซึ่งเราจะเพิ่มลงใน reqs.txt เมื่อใดก็ตามที่มีการใช้ไลบรารีของไคลเอ็นต์ Google Cloud เช่น ไลบรารีสำหรับ Cloud NDB และ Cloud Storage คุณจะต้องใช้ grpcio และ setuptools สุดท้าย Cloud Storage เองต้องใช้ไลบรารี ssl คำสั่งรันไทม์ที่ทำเครื่องหมายเป็นความคิดเห็นที่ด้านบนมีไว้สำหรับเมื่อคุณพร้อมที่จะพอร์ตแอปนี้ไปยัง Python 3 เราจะพูดถึงหัวข้อนี้ในตอนท้ายของบทแนะนำนี้

requirements.txt

เพิ่มไฟล์ requirements.txt ซึ่งต้องใช้เฟรมเวิร์ก Flask รวมถึงไลบรารีของไคลเอ็นต์ Cloud NDB และ Cloud Storage ซึ่งไม่มีไลบรารีใดเป็นไลบรารีในตัว สร้างไฟล์ที่มีเนื้อหาต่อไปนี้

flask
google-cloud-ndb
google-cloud-storage

รันไทม์ Python 2 App Engine ต้องมีการรวมไลบรารีของบุคคลที่สามที่ไม่ได้ติดตั้งมาในตัวด้วยตนเอง ดังนั้นให้เรียกใช้คำสั่งต่อไปนี้เพื่อติดตั้งไลบรารีเหล่านี้ลงในโฟลเดอร์ lib

pip install -t lib -r requirements.txt

หากมีทั้ง Python 2 และ 3 ในคอมพิวเตอร์สำหรับการพัฒนาซอฟต์แวร์ คุณอาจต้องใช้คำสั่ง pip2 เพื่อให้แน่ใจว่าได้รับไลบรารีเหล่านี้ในเวอร์ชัน Python 2 เมื่ออัปเกรดเป็น Python 3 แล้ว คุณไม่จำเป็นต้องรวมแพ็กเกจด้วยตนเองอีกต่อไป

appengine_config.py

เพิ่มไฟล์ appengine_config.py ที่รองรับไลบรารีของบุคคลที่สามทั้งแบบในตัวและไม่ใช่แบบในตัว สร้างไฟล์ที่มีเนื้อหาต่อไปนี้

import pkg_resources
from google.appengine.ext import vendor

# Set PATH to your libraries folder.
PATH = 'lib'
# Add libraries installed in the PATH folder.
vendor.add(PATH)
# Add libraries to pkg_resources working set to find the distribution.
pkg_resources.working_set.add_entry(PATH)

ขั้นตอนที่เพิ่งเสร็จสมบูรณ์ควรคล้ายหรือเหมือนกับขั้นตอนที่แสดงในส่วนการติดตั้งไลบรารีสำหรับแอป Python 2 ของเอกสาร App Engine และโดยเฉพาะอย่างยิ่ง เนื้อหาของ appengine_config.py ควรตรงกับเนื้อหาในขั้นตอนที่ 5

การทำงานกับไฟล์การกำหนดค่าเสร็จสมบูรณ์แล้ว มาต่อกันที่แอปพลิเคชันเลย

5. แก้ไขไฟล์แอปพลิเคชัน

การนำเข้า

การเปลี่ยนแปลงชุดแรกสำหรับ main.py รวมถึงการสลับสิ่งต่างๆ ที่จะถูกแทนที่ออกไป สิ่งที่จะเปลี่ยนแปลงมีดังนี้

  1. Flask จะเข้ามาแทนที่ webapp2
  2. ใช้ Jinja2 ที่มาพร้อมกับ Flask แทนการใช้ Jinja2 จาก webapp2_extras
  3. App Engine Blobstore และ NDB จะถูกแทนที่ด้วย Cloud NDB และ Cloud Storage
  4. ตัวแฮนเดิล Blobstore ใน webapp จะถูกแทนที่ด้วยชุดค่าผสมของโมดูลไลบรารีมาตรฐาน io, Flask และยูทิลิตี werkzeug
  5. โดยค่าเริ่มต้น Blobstore จะเขียนไปยังที่เก็บข้อมูล Cloud Storage ที่ตั้งชื่อตาม URL ของแอป (PROJECT_ID.appspot.com) เนื่องจากเรากำลังย้ายไปยังไลบรารีของไคลเอ็นต์ Cloud Storage จึงใช้ google.auth เพื่อรับรหัสโปรเจ็กต์เพื่อระบุชื่อที่เก็บข้อมูลเดียวกันทุกประการ (คุณเปลี่ยนชื่อที่เก็บข้อมูลได้เนื่องจากไม่ได้ฮาร์ดโค้ดอีกต่อไป)

ก่อน:

import webapp2
from webapp2_extras import jinja2
from google.appengine.ext import blobstore, ndb
from google.appengine.ext.webapp import blobstore_handlers

ใช้การเปลี่ยนแปลงในรายการด้านบนโดยแทนที่ส่วนการนําเข้าปัจจุบันใน main.py ด้วยข้อมูลโค้ดด้านล่าง

หลัง:

import io

from flask import (Flask, abort, redirect, render_template,
        request, send_file, url_for)
from werkzeug.utils import secure_filename

import google.auth
from google.cloud import exceptions, ndb, storage

การเริ่มต้นและการรองรับ Jinja2 ที่ไม่จำเป็น

บล็อกโค้ดถัดไปที่จะแทนที่คือ BaseHandler ซึ่งระบุการใช้ Jinja2 จาก webapp2_extras This is unnecessary because Jinja2 comes with Flask and is its default templating engine, so remove it.

ในฝั่งโมดูล 16 ให้สร้างอินสแตนซ์ออบเจ็กต์ที่เราไม่มีในแอปเก่า ซึ่งรวมถึงการเริ่มต้นแอป Flask และสร้างไคลเอ็นต์ API สำหรับ Cloud NDB และ Cloud Storage สุดท้าย เราจะรวมชื่อที่เก็บข้อมูล Cloud Storage ตามที่อธิบายไว้ข้างต้นในส่วนการนําเข้า ต่อไปนี้คือตัวอย่างก่อนและหลังการใช้การอัปเดตเหล่านี้

ก่อน:

class BaseHandler(webapp2.RequestHandler):
    'Derived request handler mixing-in Jinja2 support'
    @webapp2.cached_property
    def jinja2(self):
        return jinja2.get_jinja2(app=self.app)

    def render_response(self, _template, **context):
        self.response.write(self.jinja2.render_template(_template, **context))

หลัง:

app = Flask(__name__)
ds_client = ndb.Client()
gcs_client = storage.Client()
_, PROJECT_ID = google.auth.default()
BUCKET = '%s.appspot.com' % PROJECT_ID

อัปเดตสิทธิ์เข้าถึง Datastore

Cloud NDB เข้ากันได้กับ App Engine NDB เป็นส่วนใหญ่ ความแตกต่างอย่างหนึ่งที่ได้กล่าวถึงไปแล้วคือความจำเป็นในการใช้ไคลเอ็นต์ API อีกวิธีหนึ่งคือวิธีหลังกำหนดให้การเข้าถึง Datastore ต้องควบคุมโดย Python Context Manager ของไคลเอ็นต์ API ซึ่งหมายความว่าการเรียกใช้การเข้าถึง Datastore ทั้งหมดที่ใช้ไลบรารีของไคลเอ็นต์ Cloud NDB จะเกิดขึ้นได้ภายในบล็อก Python with เท่านั้น

นั่นคือการเปลี่ยนแปลงอย่างหนึ่ง ส่วนอีกอย่างคือ Cloud Storage ไม่รองรับ Blobstore และออบเจ็กต์ของ Blobstore เช่น BlobKey ดังนั้นให้เปลี่ยน file_blob เป็น ndb.StringProperty แทน ด้านล่างนี้คือคลาสโมเดลข้อมูล รวมถึงฟังก์ชัน store_visit() และ fetch_visits() ที่อัปเดตแล้วซึ่งแสดงถึงการเปลี่ยนแปลงเหล่านี้

ก่อน:

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)
    file_blob = ndb.BlobKeyProperty()

def store_visit(remote_addr, user_agent, upload_key):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent),
            file_blob=upload_key).put()

def fetch_visits(limit):
    'get most recent visits'
    return Visit.query().order(-Visit.timestamp).fetch(limit)

หลัง:

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)
    file_blob = ndb.StringProperty()

def store_visit(remote_addr, user_agent, upload_key):
    'create new Visit entity in Datastore'
    with ds_client.context():
        Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                file_blob=upload_key).put()

def fetch_visits(limit):
    'get most recent visits'
    with ds_client.context():
        return Visit.query().order(-Visit.timestamp).fetch(limit)

ต่อไปนี้คือภาพแสดงการเปลี่ยนแปลงที่เกิดขึ้นจนถึงตอนนี้

a8f74ca392275822.png

กำลังอัปเดตตัวแฮนเดิล

ตัวแฮนเดิลการอัปโหลด

ตัวแฮนเดิลใน webapp2 เป็นคลาส ส่วนใน Flask จะเป็นฟังก์ชัน Flask จะใช้คำกริยาเพื่อตกแต่งฟังก์ชันแทนเมธอดคำกริยา HTTP Blobstore และเครื่องมือจัดการ webapp จะถูกแทนที่ด้วยฟังก์ชันการทำงานจาก Cloud Storage รวมถึง Flask และยูทิลิตีต่างๆ ดังนี้

ก่อน:

class UploadHandler(blobstore_handlers.BlobstoreUploadHandler):
    'Upload blob (POST) handler'
    def post(self):
        uploads = self.get_uploads()
        blob_id = uploads[0].key() if uploads else None
        store_visit(self.request.remote_addr, self.request.user_agent, blob_id)
        self.redirect('/', code=307)

หลัง:

@app.route('/upload', methods=['POST'])
def upload():
    'Upload blob (POST) handler'
    fname = None
    upload = request.files.get('file', None)
    if upload:
        fname = secure_filename(upload.filename)
        blob = gcs_client.bucket(BUCKET).blob(fname)
        blob.upload_from_file(upload, content_type=upload.content_type)
    store_visit(request.remote_addr, request.user_agent, fname)
    return redirect(url_for('root'), code=307)

ข้อควรทราบเกี่ยวกับการอัปเดตนี้มีดังนี้

  • ตอนนี้ระบบจะระบุอาร์ติแฟกต์ไฟล์ด้วยชื่อไฟล์ (fname) หากมี แทนที่จะเป็น blob_id และจะระบุด้วย None หากไม่มี (ผู้ใช้เลือกไม่รับการอัปโหลดไฟล์)
  • เครื่องมือจัดการ Blobstore จะแยกกระบวนการอัปโหลดออกจากผู้ใช้ แต่ Cloud Storage ไม่ได้ทำเช่นนั้น คุณจึงเห็นโค้ดที่เพิ่มเข้ามาใหม่ซึ่งตั้งค่าออบเจ็กต์ Blob และตำแหน่ง (ที่เก็บข้อมูล) ของไฟล์ รวมถึงการเรียกใช้ที่ทำการอัปโหลดจริง (upload_from_file())
  • webapp2 ใช้ตารางการกำหนดเส้นทางที่ด้านล่างของไฟล์แอปพลิเคชัน ขณะที่เส้นทาง Flask จะอยู่ในแฮนเดอร์ที่ตกแต่งแต่ละรายการ
  • เครื่องมือจัดการทั้ง 2 รายการจะสรุปฟังก์ชันการทำงานโดยเปลี่ยนเส้นทางไปยังหน้าแรก ( /) ขณะที่ยังคงคำขอ POST ไว้ด้วยรหัสการแสดงผล HTTP 307

ตัวแฮนเดิลการดาวน์โหลด

การอัปเดตเครื่องมือจัดการการดาวน์โหลดจะใช้รูปแบบที่คล้ายกับเครื่องมือจัดการการอัปโหลด เพียงแต่มีโค้ดที่ต้องดูน้อยกว่ามาก แทนที่ฟังก์ชันการทำงานของ Blobstore และ webapp ด้วยฟังก์ชันการทำงานที่เทียบเท่าของ Cloud Storage และ Flask ดังนี้

ก่อน:

class ViewBlobHandler(blobstore_handlers.BlobstoreDownloadHandler):
    'view uploaded blob (GET) handler'
    def get(self, blob_key):
        self.send_blob(blob_key) if blobstore.get(blob_key) else self.error(404)

หลัง:

@app.route('/view/<path:fname>')
def view(fname):
    'view uploaded blob (GET) handler'
    blob = gcs_client.bucket(BUCKET).blob(fname)
    try:
        media = blob.download_as_bytes()
    except exceptions.NotFound:
        abort(404)
    return send_file(io.BytesIO(media), mimetype=blob.content_type)

หมายเหตุเกี่ยวกับการอัปเดตนี้

  • ขอย้ำอีกครั้งว่า Flask จะตกแต่งฟังก์ชันตัวแฮนเดิลด้วยเส้นทางของฟังก์ชัน ขณะที่ webapp จะทำในตารางการกำหนดเส้นทางที่ด้านล่าง ดังนั้นให้จดจำไวยากรณ์การจับคู่รูปแบบของ (('/view/([^/]+)?') เทียบกับ Flask ('/view/<path:fname>')
  • เช่นเดียวกับเครื่องมือจัดการการอัปโหลด คุณต้องดำเนินการเพิ่มเติมเล็กน้อยในฝั่ง Cloud Storage สำหรับฟังก์ชันการทำงานที่แยกออกมาโดยเครื่องมือจัดการ Blobstore ซึ่งก็คือการระบุไฟล์ (Blob) ที่เป็นปัญหาและการดาวน์โหลดไบนารีอย่างชัดเจน เทียบกับการเรียกใช้เมธอด send_blob() เพียงครั้งเดียวของเครื่องมือจัดการ Blobstore
  • ในทั้ง 2 กรณี ระบบจะแสดงข้อผิดพลาด HTTP 404 แก่ผู้ใช้หากไม่พบอาร์ติแฟกต์

ตัวแฮนเดิลหลัก

การเปลี่ยนแปลงขั้นสุดท้ายในแอปพลิเคชันหลักจะเกิดขึ้นในตัวแฮนเดิลหลัก เมธอดคำกริยา HTTP webapp2 ถูกแทนที่ด้วยฟังก์ชันเดียวที่รวมฟังก์ชันการทำงานเข้าด้วยกัน แทนที่คลาส MainHandler ด้วยฟังก์ชัน root() และนำตารางการกำหนดเส้นทาง webapp2 ออกตามที่แสดงด้านล่าง

ก่อน:

class MainHandler(BaseHandler):
    'main application (GET/POST) handler'
    def get(self):
        self.render_response('index.html',
                upload_url=blobstore.create_upload_url('/upload'))

    def post(self):
        visits = fetch_visits(10)
        self.render_response('index.html', visits=visits)

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/upload', UploadHandler),
    ('/view/([^/]+)?', ViewBlobHandler),
], debug=True)

หลัง:

@app.route('/', methods=['GET', 'POST'])
def root():
    'main application (GET/POST) handler'
    context = {}
    if request.method == 'GET':
        context['upload_url'] = url_for('upload')
    else:
        context['visits'] = fetch_visits(10)
    return render_template('index.html', **context)

แทนที่จะเป็นเมธอด get() และ post() แยกกัน เมธอดเหล่านี้เป็นคำสั่ง if-else ใน root() นอกจากนี้ เนื่องจาก root() เป็นฟังก์ชันเดียว จึงมีการเรียกใช้เพียงครั้งเดียวเพื่อแสดงผลเทมเพลตสำหรับทั้ง GET และ POST ซึ่งไม่สามารถทำได้ใน webapp2

ต่อไปนี้เป็นภาพแสดงการเปลี่ยนแปลงชุดที่ 2 และชุดสุดท้ายใน main.py

5ec38818c32fec2.png

(ไม่บังคับ) "การปรับปรุง" ความเข้ากันได้แบบย้อนหลัง

ดังนั้นโซลูชันที่สร้างขึ้นข้างต้นจึงทำงานได้อย่างสมบูรณ์แบบ... แต่เฉพาะในกรณีที่คุณเริ่มต้นใหม่ตั้งแต่ต้นและไม่มีไฟล์ที่สร้างโดย Blobstore เนื่องจากเราอัปเดตแอปให้ระบุไฟล์ตามชื่อไฟล์แทน BlobKey แอปโมดูล 16 ที่เสร็จสมบูรณ์ตามที่เป็นอยู่จึงไม่สามารถดูไฟล์ Blobstore ได้ กล่าวคือ เราได้ทำการเปลี่ยนแปลงที่เข้ากันไม่ได้แบบย้อนหลังเพื่อทำการย้ายข้อมูลนี้ ตอนนี้เราขอนำเสนอ main.py เวอร์ชันอื่นที่เรียกว่า main-migrate.py (พบในที่เก็บ) ซึ่งพยายามเชื่อมช่องว่างนี้

"ส่วนขยาย" แรกที่รองรับไฟล์ที่สร้างโดย Blobstore คือโมเดลข้อมูลที่มี BlobKeyProperty (นอกเหนือจาก StringProperty สำหรับไฟล์ที่สร้างโดย Cloud Storage) ดังนี้

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)
    file_blob = ndb.BlobKeyProperty()  # backwards-compatibility
    file_gcs  = ndb.StringProperty()

ระบบจะใช้พร็อพเพอร์ตี้ file_blob เพื่อระบุไฟล์ที่สร้างโดย Blobstore ส่วน file_gcs จะใช้สำหรับไฟล์ Cloud Storage ตอนนี้เมื่อสร้างการเข้าชมใหม่ ให้จัดเก็บค่าใน file_gcs อย่างชัดเจนแทน file_blob ดังนั้น store_visit จะมีลักษณะแตกต่างออกไปเล็กน้อย ดังนี้

ก่อน:

def store_visit(remote_addr, user_agent, upload_key):
    'create new Visit entity in Datastore'
    with ds_client.context():
        Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                file_blob=upload_key).put()

หลัง:

def store_visit(remote_addr, user_agent, upload_key):
    'create new Visit entity in Datastore'
    with ds_client.context():
        Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                file_gcs=upload_key).put()

เมื่อดึงข้อมูลการเข้าชมล่าสุด ให้ "ปรับข้อมูลให้เป็นมาตรฐาน" ก่อนส่งไปยังเทมเพลต ดังนี้

ก่อน:

@app.route('/', methods=['GET', 'POST'])
def root():
    'main application (GET/POST) handler'
    context = {}
    if request.method == 'GET':
        context['upload_url'] = url_for('upload')
    else:
        context['visits'] = fetch_visits(10)
    return render_template('index.html', **context)

หลัง:

@app.route('/', methods=['GET', 'POST'])
def root():
    'main application (GET/POST) handler'
    context = {}
    if request.method == 'GET':
        context['upload_url'] = url_for('upload')
    else:
        context['visits'] = etl_visits(fetch_visits(10))
    return render_template('index.html', **context)

จากนั้นยืนยันว่ามี file_blob หรือ file_gcs (หรือไม่มีทั้งคู่) หากมีไฟล์อยู่ ให้เลือกไฟล์ที่มีอยู่และใช้ตัวระบุดังกล่าว (BlobKey สำหรับไฟล์ที่สร้างโดย Blobstore หรือชื่อไฟล์สำหรับไฟล์ที่สร้างโดย Cloud Storage) เมื่อเราพูดถึง "ไฟล์ที่สร้างโดย Cloud Storage" เราหมายถึงไฟล์ที่สร้างโดยใช้ไลบรารีของไคลเอ็นต์ Cloud Storage Blobstore ยังเขียนลงใน Cloud Storage ด้วย แต่ในกรณีนี้ ไฟล์เหล่านั้นจะเป็นไฟล์ที่ Blobstore สร้างขึ้น

ตอนนี้สิ่งที่สำคัญกว่าคือฟังก์ชัน etl_visits() นี้คืออะไร ซึ่งใช้เพื่อปรับมาตรฐานหรือ ETL (แยก เปลี่ยนรูปแบบ และโหลด) ข้อมูลสำหรับผู้ใช้ปลายทาง ซึ่งจะมีลักษณะดังนี้

def etl_visits(visits):
    return [{
            'visitor': v.visitor,
            'timestamp': v.timestamp,
            'file_blob': v.file_gcs if hasattr(v, 'file_gcs') \
                    and v.file_gcs else v.file_blob
            } for v in visits]

ซึ่งอาจดูเหมือนสิ่งที่คุณคาดหวังไว้ นั่นคือโค้ดจะวนซ้ำการเข้าชมทั้งหมด และสำหรับการเข้าชมแต่ละครั้ง จะนำข้อมูลผู้เข้าชมและการประทับเวลามาใช้ตามคำสั่ง จากนั้นจะตรวจสอบว่ามี file_gcs หรือ file_blob อยู่หรือไม่ และหากมี ก็จะเลือกอย่างใดอย่างหนึ่ง (หรือ None หากไม่มีทั้ง 2 อย่าง)

ต่อไปนี้คือภาพแสดงความแตกต่างระหว่าง main.py และ main-migrate.py

718b05b2adadb2e1.png

หากคุณเริ่มต้นใหม่โดยไม่มีไฟล์ที่สร้างโดย Blobstore ให้ใช้ main.py แต่หากคุณกำลังเปลี่ยนผ่านและต้องการไฟล์สนับสนุนที่สร้างโดยทั้ง Blobstore และ Cloud Storage โปรดดู main-migrate.py เป็นตัวอย่างวิธีจัดการกับสถานการณ์ดังกล่าวเพื่อช่วยคุณวางแผนการย้ายข้อมูลสำหรับแอปของคุณเอง เมื่อทำการย้ายข้อมูลที่ซับซ้อน อาจเกิดกรณีพิเศษขึ้นได้ ดังนั้นตัวอย่างนี้จึงมีจุดประสงค์เพื่อแสดงให้เห็นถึงความเกี่ยวข้องที่มากขึ้นในการปรับปรุงแอปจริงให้ทันสมัยด้วยข้อมูลจริง

6. สรุป/ล้างข้อมูล

ส่วนนี้จะสรุป Codelab นี้ด้วยการทำให้แอปใช้งานได้ โดยยืนยันว่าแอปทำงานได้ตามที่ต้องการและในเอาต์พุตที่แสดง หลังจากตรวจสอบแอปแล้ว ให้ทำตามขั้นตอนการล้างข้อมูลและพิจารณาขั้นตอนถัดไป

ทำให้แอปพลิเคชันใช้งานได้และยืนยันแอปพลิเคชัน

ก่อนทำให้แอปใช้งานได้อีกครั้ง โปรดเรียกใช้ pip install -t lib -r requirements.txt เพื่อรับไลบรารีของบุคคลที่สามที่รวมไว้ในตัวในโฟลเดอร์ lib หากต้องการเรียกใช้โซลูชันที่เข้ากันได้แบบย้อนหลัง ให้เปลี่ยนชื่อ main-migrate.py เป็น main.py ก่อน ตอนนี้ให้เรียกใช้ gcloud app deploy และยืนยันว่าแอปทำงานเหมือนกับแอปโมดูล 15 หน้าจอแบบฟอร์มจะมีลักษณะดังนี้

f5b5f9f19d8ae978.png

หน้าการเข้าชมล่าสุดจะมีลักษณะดังนี้

f5ac6b98ee8a34cb.png

ขอแสดงความยินดีที่ทำ Codelab นี้เสร็จสมบูรณ์ ซึ่งเป็นการแทนที่ App Engine Blobstore ด้วย Cloud Storage, App Engine NDB ด้วย Cloud NDB และ webapp2 ด้วย Flask ตอนนี้โค้ดของคุณควรตรงกับโค้ดในโฟลเดอร์ FINISH (โมดูล 16) main-migrate.py ทางเลือกก็อยู่ในโฟลเดอร์นั้นด้วย

"การย้ายข้อมูล" Python 3

คำสั่ง runtime ของ Python 3 ที่ทำเครื่องหมายเป็นความคิดเห็นไว้ที่ด้านบนของ app.yaml คือสิ่งเดียวที่จำเป็นในการพอร์ตแอปนี้ไปยัง Python 3 ซอร์สโค้ดเองเข้ากันได้กับ Python 3 อยู่แล้ว จึงไม่จำเป็นต้องทำการเปลี่ยนแปลงใดๆ หากต้องการทำให้เป็นแอป Python 3 ให้ทำตามขั้นตอนต่อไปนี้

  1. ยกเลิกการคอมเมนต์คำสั่ง runtime ของ Python 3 ที่ด้านบนของ app.yaml
  2. ลบบรรทัดอื่นๆ ทั้งหมดใน app.yaml
  3. ลบไฟล์ appengine_config.py (ไม่ได้ใช้ในรันไทม์ Python 3)
  4. ลบโฟลเดอร์ lib หากมี (ไม่จำเป็นสำหรับรันไทม์ Python 3)

ล้างข้อมูล

ทั่วไป

หากคุณดำเนินการเสร็จแล้ว เราขอแนะนำให้ปิดใช้แอป App Engine เพื่อหลีกเลี่ยงการเรียกเก็บเงิน อย่างไรก็ตาม หากคุณต้องการทดสอบหรือทดลองเพิ่มเติม แพลตฟอร์ม App Engine มีโควต้าฟรี และตราบใดที่คุณไม่ใช้เกินระดับการใช้งานนั้น คุณก็จะไม่ถูกเรียกเก็บเงิน ซึ่งเป็นค่าใช้จ่ายสำหรับการประมวลผล แต่อาจมีค่าใช้จ่ายสำหรับบริการ App Engine ที่เกี่ยวข้องด้วย ดังนั้นโปรดดูข้อมูลเพิ่มเติมในหน้าการกำหนดราคา หากการย้ายข้อมูลนี้เกี่ยวข้องกับบริการอื่นๆ ของ Cloud ระบบจะเรียกเก็บเงินสำหรับบริการเหล่านั้นแยกต่างหาก ไม่ว่าในกรณีใดก็ตาม โปรดดูส่วน "เฉพาะสำหรับ Codelab นี้" ด้านล่าง (หากมี)

เพื่อความโปร่งใส การติดตั้งใช้งานในแพลตฟอร์มการประมวลผลแบบ Serverless ของ Google Cloud เช่น App Engine จะมีค่าใช้จ่ายในการสร้างและพื้นที่เก็บข้อมูลเล็กน้อย Cloud Build มีโควต้าฟรีของตัวเองเช่นเดียวกับ Cloud Storage พื้นที่เก็บข้อมูลของรูปภาพนั้นจะใช้โควต้าบางส่วน อย่างไรก็ตาม คุณอาจอาศัยอยู่ในภูมิภาคที่ไม่มีรุ่นฟรีดังกล่าว ดังนั้นโปรดคำนึงถึงการใช้พื้นที่เก็บข้อมูลเพื่อลดค่าใช้จ่ายที่อาจเกิดขึ้น "โฟลเดอร์" Cloud Storage ที่เฉพาะเจาะจงซึ่งคุณควรตรวจสอบมีดังนี้

  • console.cloud.google.com/storage/browser/LOC.artifacts.PROJECT_ID.appspot.com/containers/images
  • console.cloud.google.com/storage/browser/staging.PROJECT_ID.appspot.com
  • ลิงก์พื้นที่เก็บข้อมูลด้านบนจะขึ้นอยู่กับ PROJECT_ID และ *LOC*ation เช่น "us" หากแอปโฮสต์อยู่ในสหรัฐอเมริกา

ในทางกลับกัน หากคุณจะไม่ดำเนินการต่อกับแอปพลิเคชันนี้หรือ Codelab การย้ายข้อมูลอื่นๆ ที่เกี่ยวข้อง และต้องการลบทุกอย่างออกทั้งหมด ให้ปิดโปรเจ็กต์

เฉพาะสำหรับ Codelab นี้

บริการที่ระบุไว้ด้านล่างนี้มีไว้สำหรับ Codelab นี้โดยเฉพาะ ดูข้อมูลเพิ่มเติมได้ในเอกสารประกอบของผลิตภัณฑ์แต่ละรายการ ดังนี้

โปรดทราบว่าหากย้ายข้อมูลจากโมดูล 15 ไปยัง 16 คุณจะยังมีข้อมูลอยู่ใน Blobstore เราจึงรวมข้อมูลราคาไว้ด้านบน

ขั้นตอนถัดไป

นอกเหนือจากบทแนะนำนี้แล้ว โมดูลการย้ายข้อมูลอื่นๆ ที่มุ่งเน้นการย้ายออกจากบริการที่รวมเป็นแพ็กเกจเดิมที่ควรพิจารณามีดังนี้

  • โมดูล 2: ย้ายข้อมูลจาก App Engine ndb ไปยัง Cloud NDB
  • โมดูล 7-9: ย้ายข้อมูลจากพุชงานของคิวงาน App Engine ไปยัง Cloud Tasks
  • โมดูล 12-13: ย้ายข้อมูลจาก App Engine Memcache ไปยัง Cloud Memorystore
  • โมดูล 18-19: ย้ายข้อมูลจากคิวงาน App Engine (พุลทาสก์) ไปยัง Cloud Pub/Sub

App Engine ไม่ใช่แพลตฟอร์มแบบ Serverless เพียงแพลตฟอร์มเดียวใน Google Cloud อีกต่อไป หากคุณมีแอป App Engine ขนาดเล็กหรือแอปที่มีฟังก์ชันการทำงานจำกัด และต้องการเปลี่ยนเป็น Microservice แบบสแตนด์อโลน หรือต้องการแยกแอปแบบโมโนลิธิกออกเป็นคอมโพเนนต์ที่นำกลับมาใช้ใหม่ได้หลายรายการ เหตุผลเหล่านี้เป็นเหตุผลที่ดีในการพิจารณาย้ายไปใช้ Cloud Functions หากการสร้างคอนเทนเนอร์กลายเป็นส่วนหนึ่งของเวิร์กโฟลว์การพัฒนาแอปพลิเคชัน โดยเฉพาะอย่างยิ่งหากประกอบด้วยไปป์ไลน์ CI/CD (การรวมอย่างต่อเนื่อง/การส่งมอบอย่างต่อเนื่องหรือการติดตั้งใช้งานอย่างต่อเนื่อง) ให้พิจารณาย้ายข้อมูลไปยัง Cloud Run สถานการณ์เหล่านี้จะอยู่ในข้อบังคับต่อไปนี้

  • ย้ายข้อมูลจาก App Engine ไปยัง Cloud Functions: ดูโมดูล 11
  • ย้ายข้อมูลจาก App Engine ไปยัง Cloud Run: ดูโมดูล 4 เพื่อสร้างคอนเทนเนอร์ให้แอปด้วย Docker หรือโมดูล 5 เพื่อดำเนินการโดยไม่ต้องใช้คอนเทนเนอร์ ความรู้เกี่ยวกับ Docker หรือ Dockerfile

การเปลี่ยนไปใช้แพลตฟอร์มแบบ Serverless อื่นเป็นตัวเลือกที่ไม่บังคับ และเราขอแนะนำให้พิจารณาตัวเลือกที่ดีที่สุดสำหรับแอปและ Use Case ของคุณก่อนทำการเปลี่ยนแปลงใดๆ

ไม่ว่าคุณจะพิจารณาโมดูลการย้ายข้อมูลใดต่อไป คุณจะเข้าถึงเนื้อหา Serverless Migration Station ทั้งหมด (Codelab, วิดีโอ, ซอร์สโค้ด [หากมี]) ได้ที่ที่เก็บโอเพนซอร์ส README ของที่เก็บยังมีคำแนะนำเกี่ยวกับการย้ายข้อมูลที่ควรพิจารณาและ "ลำดับ" ที่เกี่ยวข้องของโมดูลการย้ายข้อมูลด้วย

7. แหล่งข้อมูลเพิ่มเติม

ปัญหา/ความคิดเห็นเกี่ยวกับ Codelab

หากพบปัญหาเกี่ยวกับ Codelab นี้ โปรดค้นหาปัญหาก่อนที่จะยื่นเรื่อง ลิงก์เพื่อค้นหาและสร้างปัญหาใหม่

แหล่งข้อมูลเกี่ยวกับการย้ายข้อมูล

ดูลิงก์ไปยังโฟลเดอร์ที่เก็บสำหรับโมดูล 15 (START) และโมดูล 16 (FINISH) ได้ในตารางด้านล่าง นอกจากนี้ คุณยังเข้าถึงได้จากที่เก็บสำหรับการย้ายข้อมูล Codelab ของ App Engine ทั้งหมด ซึ่งคุณสามารถโคลนหรือดาวน์โหลดไฟล์ ZIP ได้

Codelab

Python 2

Python 3

โมดูล 15

code

ไม่มี

โมดูล 16 (Codelab นี้)

code

(เหมือนกับ Python 2)

แหล่งข้อมูลออนไลน์

แหล่งข้อมูลออนไลน์ที่อาจเกี่ยวข้องกับบทแนะนำนี้มีดังนี้

App Engine Blobstore และ Cloud Storage

แพลตฟอร์ม App Engine

ข้อมูลอื่นๆ เกี่ยวกับระบบคลาวด์

Python

วิดีโอ

ใบอนุญาต

งานนี้ได้รับอนุญาตภายใต้สัญญาอนุญาตครีเอทีฟคอมมอนส์สำหรับยอมรับสิทธิของผู้สร้าง (Creative Commons Attribution License) 2.0 แบบทั่วไป