Как использовать хранилище больших объектов App Engine (модуль 15)

1. Обзор

Серия обучающих материалов Serverless Migration Station (самостоятельные практические уроки) и сопутствующих видеороликов призвана помочь разработчикам бессерверных приложений Google Cloud модернизировать свои приложения, проведя их через одну или несколько миграций, в первую очередь, через отказ от устаревших сервисов. Это делает ваши приложения более портативными, предоставляет больше возможностей и гибкости, позволяя интегрироваться с более широким спектром облачных продуктов и получать к ним доступ, а также упрощает обновление до более новых версий языков программирования. Хотя изначально серия ориентирована на самых первых пользователей облачных сервисов, в первую очередь разработчиков App Engine (стандартная среда), она достаточно широка, чтобы охватить и другие бессерверные платформы, такие как Cloud Functions и Cloud Run , или другие, если это применимо.

В этом практическом занятии по модулю 15 объясняется, как добавить использование blobstore App Engine в пример приложения из модуля 0. После этого вы будете готовы перенести это использование в облачное хранилище в модуле 16.

Вы узнаете, как

  • Добавить возможность использования API/библиотеки App Engine Blobstore.
  • Хранить пользовательские загрузки в сервис blobstore
  • Подготовьтесь к следующему шагу — миграции в облачное хранилище.

Что вам понадобится

Опрос

Как вы будете использовать этот учебный материал?

Прочитайте только от начала до конца. Прочитайте текст и выполните упражнения.

Как бы вы оценили свой опыт работы с Python?

Новичок Средний Профессионал

Как бы вы оценили свой опыт использования сервисов Google Cloud?

Новичок Средний Профессионал

2. Предыстория

Для перехода с API App Engine Blobstore необходимо добавить его использование в существующее базовое приложение App Engine ndb из модуля 0. В примере приложения пользователю отображаются десять последних посещений. Мы модифицируем приложение таким образом, чтобы оно предлагало пользователю загрузить артефакт (файл), соответствующий его «посещению». Если пользователь не хочет этого делать, есть опция «пропустить». Независимо от решения пользователя, на следующей странице отображается тот же результат, что и в приложении из модуля 0 (и многих других модулях этой серии). После реализации интеграции App Engine blobstore мы сможем перенести приложение в Cloud Storage в следующем практическом занятии (модуль 16).

App Engine предоставляет доступ к системам шаблонизации Django и Jinja2 , и одно из отличий этого примера (помимо добавления доступа к Blobstore) заключается в том, что в модуле 0 используется не Django, а Jinja2, а в модуле 15. Ключевым шагом в модернизации приложений App Engine является миграция веб-фреймворков с webapp2 на Flask. Последний использует Jinja2 в качестве системы шаблонизации по умолчанию, поэтому мы начинаем двигаться в этом направлении, внедряя Jinja2, оставаясь при этом на webapp2 для доступа к Blobstore. Поскольку Flask использует Jinja2 по умолчанию, это означает, что в модуле 16 никаких изменений в шаблоне не потребуется.

3. Подготовка/Предварительные работы

Прежде чем перейти к основной части руководства, настройте свой проект, получите код и разверните базовое приложение, чтобы начать работу с работающим кодом.

1. Настройка проекта

Если вы уже развернули приложение «Модуль 0», мы рекомендуем повторно использовать тот же проект (и код). В качестве альтернативы вы можете создать совершенно новый проект или использовать другой существующий проект. Убедитесь, что у проекта есть активный платежный аккаунт и включен App Engine.

2. Получите базовый образец приложения.

Одно из предварительных условий для выполнения этого практического задания — наличие работающего примера приложения из Модуля 0. Если у вас его нет, вы можете получить его из папки «START» Модуля 0 (ссылка ниже). В этом практическом задании вы шаг за шагом пройдете весь процесс, и в конце получите код, похожий на тот, что находится в папке «FINISH» Модуля 15.

Структура каталога файлов, запускающих модуль 0, должна выглядеть следующим образом:

$ ls
README.md               index.html
app.yaml                main.py

3. (Повторное) развертывание базового приложения

Осталось выполнить следующие подготовительные шаги:

  1. Вспомните, как работает инструмент командной строки gcloud
  2. Повторно разверните демонстрационное приложение с помощью gcloud app deploy
  3. Убедитесь, что приложение работает в App Engine без проблем.

После успешного выполнения этих шагов и проверки работоспособности вашего веб-приложения (с результатом, аналогичным приведенному ниже), вы готовы добавить в приложение кэширование.

a7a9d2b80d706a2b.png

4. Обновите конфигурационные файлы.

app.yaml

Существенных изменений в конфигурации приложения нет, однако, как упоминалось ранее, мы переходим от шаблонизации Django (по умолчанию) к Jinja2, поэтому для переключения пользователям необходимо указать последнюю версию Jinja2, доступную на серверах App Engine, и сделать это можно, добавив ее в раздел встроенных сторонних библиотек файла app.yaml .

ДО:

runtime: python27
threadsafe: yes
api_version: 1

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

Отредактируйте файл app.yaml , добавив новый раздел libraries , как показано здесь:

ПОСЛЕ:

runtime: python27
threadsafe: yes
api_version: 1

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

libraries:
- name: jinja2
  version: latest

Обновлять другие конфигурационные файлы не требуется, поэтому перейдем к файлам приложения.

5. Измените файлы приложения.

Поддержка импорта и Jinja2

Первый набор изменений в файле main.py включает добавление использования API Blobstore и замену шаблонизатора Django на Jinja2. Вот что меняется:

  1. Цель модуля os — создать путь к файлу шаблона Django. Поскольку мы переходим на Jinja2, где это обрабатывается автоматически, использование ` os , а также средства рендеринга шаблонов Django ` google.appengine.ext.webapp.template больше не требуется, поэтому они удаляются.
  2. Импортируйте API Blobstore: google.appengine.ext.blobstore
  3. Импортируйте обработчики Blobstore, которые есть в оригинальной webapp платформе — они недоступны в webapp2 : google.appengine.ext.webapp.blobstore_handlers
  4. Импортируйте поддержку Jinja2 из пакета webapp2_extras

ДО:

import os
import webapp2
from google.appengine.ext import ndb
from google.appengine.ext.webapp import template

Внесите изменения, указанные в списке выше, заменив текущий раздел импорта в main.py приведенным ниже фрагментом кода.

ПОСЛЕ:

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

После импорта добавьте шаблонный код для поддержки использования Jinja2, как описано в документации webapp2_extras . Следующий фрагмент кода оборачивает стандартный класс обработчика запросов webapp2 функциональностью Jinja2, поэтому добавьте этот блок кода в main.py сразу после импорта:

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

Добавить поддержку хранилища BLOB-объектов.

В отличие от других миграций в этой серии, где мы сохраняем функциональность или вывод тестового приложения идентичными (или почти идентичными) без (значительных) изменений в пользовательском интерфейсе, этот пример представляет собой более радикальное отступление от нормы. Вместо того чтобы сразу регистрировать новое посещение, а затем отображать десять последних, мы обновляем приложение таким образом, чтобы оно запрашивало у пользователя файл для регистрации посещения. Затем конечные пользователи могут либо загрузить соответствующий файл, либо выбрать «Пропустить», чтобы ничего не загружать вообще. После завершения этого шага отображается страница «последние посещения».

Это изменение позволяет нашему приложению использовать сервис Blobstore для хранения (и, возможно, последующего отображения) изображения или другого типа файла на странице последних посещений.

Обновите модель данных и внедрите ее использование.

Мы добавляем больше данных, а именно обновляем модель данных, чтобы она хранила идентификатор (называемый " BlobKey ") файла, загруженного в Blobstore, и добавляем ссылку для сохранения этого идентификатора в store_visit() . Поскольку эти дополнительные данные возвращаются вместе со всем остальным при запросе, fetch_visits() остается без изменений.

Вот фотографии до и после внесения этих обновлений, касающихся file_blob и свойства ndb.BlobKeyProperty :

ДО:

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

def store_visit(remote_addr, user_agent):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent)).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.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)

Вот наглядное представление изменений, которые уже были внесены:

2270783776759f7f.png

Поддержка загрузки файлов

Наиболее существенное изменение функциональности связано с поддержкой загрузки файлов, будь то запрос файла у пользователя, поддержка функции «пропустить» или отображение файла, соответствующего посещению. Все это является частью общей картины. Вот изменения, необходимые для поддержки загрузки файлов:

  1. Основной GET запрос обработчика больше не получает информацию о последних посещениях для отображения. Вместо этого он запрашивает у пользователя возможность загрузки файла.
  2. Когда конечный пользователь отправляет файл для загрузки или пропускает этот процесс, POST из формы передает управление новому UploadHandler , производному от google.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler .
  3. Метод POST класса UploadHandler выполняет загрузку, вызывает store_visit() для регистрации посещения и запускает HTTP-перенаправление 307, чтобы отправить пользователя обратно на "/", где...
  4. Основной обработчик POST -запроса (через fetch_visits() ) отображает самые последние посещения. Если пользователь выбирает «пропустить», файл не загружается, но посещение все равно регистрируется, после чего происходит перенаправление.
  5. В разделе «Последние посещения» появилось новое поле, отображаемое пользователю: либо гиперссылка «Просмотр», если доступен файл для загрузки, либо «Нет» в противном случае. Эти изменения реализованы в HTML-шаблоне вместе с добавлением формы загрузки (подробнее об этом позже).
  6. Если пользователь нажимает на ссылку «просмотреть» при посещении сайта с загруженным видео, он отправляет GET запрос к новому объекту ViewBlobHandler , производному от google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler . В этом случае файл либо отображается (если это изображение, в браузере, если поддерживается), либо предлагается его загрузить, если изображение не найдено, либо возвращается ошибка HTTP 404.
  7. В дополнение к новой паре классов обработчиков, а также новой паре маршрутов для отправки трафика к ним, основному обработчику необходим новый метод POST для приема перенаправления 307, описанного выше.

До этих обновлений приложение Module 0 содержало только основной обработчик с методом GET и единственным маршрутом:

ДО:

class MainHandler(webapp2.RequestHandler):
    'main application (GET) handler'
    def get(self):
        store_visit(self.request.remote_addr, self.request.user_agent)
        visits = fetch_visits(10)
        tmpl = os.path.join(os.path.dirname(__file__), 'index.html')
        self.response.out.write(template.render(tmpl, {'visits': visits}))

app = webapp2.WSGIApplication([
    ('/', MainHandler),
], debug=True)

После внесения этих изменений теперь имеется три обработчика: 1) обработчик загрузки с методом POST , 2) обработчик загрузки "просмотра BLOB-объекта" с методом GET и 3) основной обработчик с методами GET и POST . Внесите эти изменения, чтобы остальная часть вашего приложения выглядела следующим образом.

ПОСЛЕ:

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)

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)

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)

В этом коде мы только что добавили несколько ключевых вызовов:

  • В MainHandler.get вызывается метод blobstore.create_upload_url . Этот вызов генерирует URL-адрес, на который отправляется POST запрос из формы, и вызывает обработчик загрузки для отправки файла в Blobstore.
  • В UploadHandler.post происходит вызов функции blobstore_handlers.BlobstoreUploadHandler.get_uploads . Именно здесь происходит настоящая магия: файл помещается в Blobstore, и возвращается уникальный и постоянный идентификатор файла — его BlobKey .
  • В ViewBlobHandler.get вызов blobstore_handlers.BlobstoreDownloadHandler.send с указанием BlobKey файла приводит к загрузке файла и его переадресации в браузер конечного пользователя.

Эти вызовы составляют основную часть обращений к функциям, добавленным в приложение. Вот графическое представление второго и последнего набора изменений в main.py :

da2960525ac1b90d.png

Обновить HTML-шаблон

Некоторые обновления основного приложения затрагивают пользовательский интерфейс (UI), поэтому требуются соответствующие изменения в веб-шаблоне, а именно два изменения:

  1. Для загрузки файла необходима форма с тремя полями ввода: самим файлом и парой кнопок отправки для загрузки файла и пропуска, соответственно.
  2. Обновите вывод последних посещений, добавив ссылку «просмотреть» для посещений с соответствующей загрузкой файла или ссылку «нет» в остальных случаях.

ДО:

<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>

<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
    <li>{{ visit.timestamp.ctime }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>

</body>
</html>

Внесите изменения, указанные в списке выше, чтобы создать обновленный шаблон:

ПОСЛЕ:

<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>

<h1>VisitMe example</h1>
{% if upload_url %}

<h3>Welcome... upload a file? (optional)</h3>
<form action="{{ upload_url }}" method="POST" enctype="multipart/form-data">
    <input type="file" name="file"><p></p>
    <input type="submit"> <input type="submit" value="Skip">
</form>

{% else %}

<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
<li>{{ visit.timestamp.ctime() }}
    <i><code>
    {% if visit.file_blob %}
        (<a href="/view/{{ visit.file_blob }}" target="_blank">view</a>)
    {% else %}
        (none)
    {% endif %}
    </code></i>
    from {{ visit.visitor }}
</li>
{% endfor %}
</ul>

{% endif %}

</body>
</html>

На этом изображении показаны необходимые изменения в index.html :

8583e975f25aa9e7.png

Последнее изменение заключается в том, что Jinja2 предпочитает размещать свои шаблоны в папке templates , поэтому создайте эту папку и переместите index.html внутрь неё. С этим последним шагом вы завершили все необходимые изменения для добавления использования Blobstore в пример приложения модуля 0.

(необязательно) «Улучшение» облачного хранилища

Хранилище Blobstore в конечном итоге превратилось в само облачное хранилище. Это означает, что загруженные данные из Blobstore видны в консоли облачного хранилища, а именно в браузере облачного хранилища. Вопрос в том, где именно. Ответ — в стандартном сегменте облачного хранилища вашего приложения App Engine. Его имя — это полное доменное имя вашего приложения App Engine, PROJECT_ID .appspot.com . Это очень удобно, потому что все идентификаторы проектов уникальны, не так ли?

Обновления, внесенные в демонстрационное приложение, помещают загруженные файлы в указанный сегмент, но у разработчиков есть возможность выбрать более конкретное местоположение. Доступ к сегменту по умолчанию осуществляется программно через google.appengine.api.app_identity.get_default_gcs_bucket_name() , для чего потребуется новый импорт, если вы хотите получить доступ к этому значению, например, для использования в качестве префикса для организации загруженных файлов. Например, сортировка по типу файла:

f61f7a23a1518705.png

Чтобы реализовать нечто подобное, например, для изображений, вам понадобится примерно такой код, а также код, который проверяет типы файлов, чтобы выбрать нужное имя хранилища:

ROOT_BUCKET = app_identity.get_default_gcs_bucket_name()
IMAGE_BUCKET = '%s/%s' % (ROOT_BUCKET, 'images')

Также вам потребуется проверить загружаемые изображения с помощью такого инструмента, как модуль imghdr из стандартной библиотеки Python, чтобы подтвердить тип изображения. Наконец, вероятно, вам захочется ограничить размер загружаемых файлов на случай появления злоумышленников.

Допустим, все это уже сделано. Как нам обновить наше приложение, чтобы оно поддерживало указание места хранения загруженных файлов? Ключевой момент — изменить вызов blobstore.create_upload_url в MainHandler.get , чтобы указать желаемое местоположение в Cloud Storage для загрузки, добавив параметр gs_bucket_name следующим образом:

blobstore.create_upload_url('/upload', gs_bucket_name=IMAGE_BUCKET))

Поскольку это необязательное обновление, позволяющее указать, куда должны сохраняться загружаемые файлы, оно не является частью файла main.py в репозитории. Вместо этого в репозитории доступен альтернативный файл main-gcs.py , который вы можете просмотреть. Вместо использования отдельной папки в хранилище, код в main-gcs.py хранит загружаемые файлы в корневом хранилище ( PROJECT_ID ), как и main.py , но предоставляет необходимую структуру, если вы захотите преобразовать пример во что-то большее .appspot.com как указано в этом разделе. Ниже приведена иллюстрация различий между main.py и main-gcs.py .

256e1ea68241a501.png

6. Подведение итогов/Завершение

В этом разделе завершается выполнение данного практического задания путем развертывания приложения, проверки его корректной работы и отображения всех полученных результатов. После проверки приложения выполните необходимые действия по очистке и обдумайте дальнейшие шаги.

Разверните и проверьте приложение.

Переразверните приложение с помощью gcloud app deploy и убедитесь, что оно работает должным образом, отличаясь по пользовательскому интерфейсу (UX) от приложения Модуля 0. Теперь в вашем приложении два разных экрана, первый из которых — это форма для загрузки файла:

f5b5f9f19d8ae978.png Далее пользователи либо загружают файл и нажимают «Отправить», либо нажимают «Пропустить», чтобы ничего не загружать. В любом случае отображается экран последнего посещения, дополненный ссылками «просмотреть» или «нет» между временными метками посещения и информацией о посетителе:

f5ac6b98ee8a34cb.png

Поздравляем с завершением этого практического задания по добавлению использования App Engine Blobstore в пример приложения из модуля 0. Ваш код теперь должен соответствовать содержимому папки FINISH (модуль 15) . Альтернативный main-gcs.py также находится в этой папке.

Уборка

Общий

Если на этом пока всё, мы рекомендуем отключить ваше приложение App Engine, чтобы избежать дополнительных расходов. Однако, если вы хотите продолжить тестирование или эксперименты, платформа App Engine предоставляет бесплатную квоту , поэтому, пока вы не превысите этот лимит, с вас не должны взиматься дополнительные платежи. Это касается вычислительных ресурсов, но могут также взиматься плата за соответствующие услуги App Engine, поэтому проверьте страницу с ценами для получения дополнительной информации. Если эта миграция включает другие облачные сервисы, они оплачиваются отдельно. В любом случае, если применимо, см. раздел «Информация, относящаяся к этому практическому занятию» ниже.

Для полной ясности, развертывание на бессерверной вычислительной платформе 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 *, например, " us ", если ваше приложение размещено в США.

С другой стороны, если вы не собираетесь продолжать работу над этим приложением или другими связанными с миграцией кодовыми руководствами и хотите полностью удалить все, закройте свой проект .

Это относится именно к данному практическому занятию.

Перечисленные ниже услуги являются уникальными для данной учебной лаборатории. Для получения более подробной информации обратитесь к документации по каждому продукту:

Следующие шаги

Следующий логичный вариант миграции рассматривается в Модуле 16, где показано, как разработчики могут перейти с сервиса App Engine Blobstore на использование клиентской библиотеки Cloud Storage. Преимущества перехода включают доступ к большему количеству функций Cloud Storage, а также знакомство с клиентской библиотекой, работающей для приложений вне App Engine, будь то в Google Cloud, других облаках или даже локально. Если вам не нужны все функции Cloud Storage или вас беспокоит влияние на стоимость, вы можете остаться на App Engine Blobstore.

Помимо модуля 16, существует множество других возможных вариантов миграции, таких как Cloud NDB и Cloud Datastore, Cloud Tasks или Cloud Memorystore. Также доступны миграции между продуктами Cloud Run и Cloud Functions. В репозитории миграции представлены все примеры кода, ссылки на все доступные практические занятия и видеоуроки, а также рекомендации по выбору подходящих вариантов миграции и соответствующему «порядку» миграции.

7. Дополнительные ресурсы

Вопросы/отзывы по Codelab

Если вы обнаружите какие-либо проблемы в этом практическом задании, пожалуйста, сначала найдите свою проблему, прежде чем сообщать о ней. Ссылки для поиска и создания новых проблем:

Миграционные ресурсы

Ссылки на папки репозитория для Модуля 0 (НАЧАЛО) и Модуля 15 (ЗАВЕРШЕНИЕ) можно найти в таблице ниже. К ним также можно получить доступ из репозитория для всех миграций кода App Engine, которые можно клонировать или загрузить в виде ZIP-файла.

Кодлаб

Python 2

Python 3

Модуль 0

код

Н/Д

Модуль 15 (данная практическая работа)

код

Н/Д

Онлайн-ресурсы

Ниже приведены онлайн-ресурсы, которые могут быть полезны для данного урока:

App Engine

Google Облако

Python

Видео

Лицензия

Данная работа распространяется под лицензией Creative Commons Attribution 2.0 Generic.