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

1. ภาพรวม

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

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

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

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

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

สิ่งที่คุณต้องมี

แบบสำรวจ

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

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

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

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

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

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

2. ฉากหลัง

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

การย้ายข้อมูลนี้ต้องใช้ความพยายามมากกว่าการย้ายข้อมูลอื่นๆ ในชุดนี้ Blobstore มีการอ้างอิงเฟรมเวิร์ก webapp ดั้งเดิม จึงเป็นเหตุผลที่แอปตัวอย่างใช้เฟรมเวิร์ก 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

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

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

ไฟล์ main-gcs.py เป็นเวอร์ชันสำรองของ main.py จากโมดูลที่ 15 ซึ่งช่วยให้เลือก Bucket ของ 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 จาก webapp2_extras ให้ใช้ Jinja2 ที่มาพร้อมกับ Flask
  3. Cloud NDB และ Cloud Storage จะมาแทนที่ App Engine Blobstore และ NDB
  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 ไม่จำเป็นต้องทำเช่นนี้เนื่องจาก Jinja2 มาพร้อมกับ Flask และเป็นเครื่องมือสร้างเทมเพลตเริ่มต้น จึงให้นำออก

ในส่วนของโมดูล 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 ของไคลเอ็นต์ 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 และยูทิลิตีของ 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) หากมี และ None ในกรณีอื่นๆ (ผู้ใช้เลือกไม่ให้อัปโหลดไฟล์) แทนที่จะเป็น blob_id
  • ตัวแฮนเดิล 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 (หรือไม่มีทั้ง 2 อย่าง) หากมีไฟล์ ให้เลือกไฟล์ที่มีอยู่และใช้ตัวระบุนั้น (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 ที่เกี่ยวข้องด้วย ดังนั้นโปรดดูข้อมูลเพิ่มเติมในหน้าการกำหนดราคา หากการย้ายข้อมูลนี้เกี่ยวข้องกับบริการอื่นๆ ของระบบคลาวด์ ระบบจะเรียกเก็บเงินสำหรับบริการเหล่านั้นแยกต่างหาก ไม่ว่าจะในกรณีใดก็ตาม หากมี ให้ดูส่วน "เฉพาะสำหรับ 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 นี้

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Codelab

Python 2

Python 3

โมดูล 15

รหัส

ไม่มี

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

รหัส

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

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

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

App Engine Blobstore และ Cloud Storage

แพลตฟอร์ม App Engine

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

Python

วิดีโอ

ใบอนุญาต

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