1. Обзор
Серия курсов по кодированию Serverless Migration Station (практические руководства для самостоятельного обучения) и сопутствующие видеоролики призваны помочь бессерверным разработчикам Google Cloud модернизировать свои приложения, помогая им выполнить одну или несколько миграций, в первую очередь отходя от устаревших сервисов. Это сделает ваши приложения более портативными и предоставит вам больше возможностей и гибкости, позволяя интегрироваться с более широким спектром облачных продуктов и получать к ним доступ, а также упростить обновление до более новых языковых версий. Первоначально эта серия ориентирована на самых первых пользователей облака, в первую очередь на разработчиков App Engine (стандартной среды), но эта серия достаточно широка, чтобы включать в себя другие бессерверные платформы, такие как Cloud Functions и Cloud Run , или другие бессерверные платформы, если это применимо.
В этой лаборатории кода модуля 15 объясняется, как добавить использование blobstore
App Engine в пример приложения из модуля 0. Затем вы будете готовы перенести это использование в облачное хранилище в следующем модуле 16.
Вы узнаете, как
- Добавьте использование API/библиотеки App Engine Blobstore.
- Храните пользовательские загрузки в службе
blobstore
- Подготовьтесь к следующему шагу по переходу на Cloud Storage
Что вам понадобится
- Проект Google Cloud Platform с активным платежным аккаунтом GCP.
- Базовые навыки Python
- Знание основных команд Linux.
- Базовые знания разработки и развертывания приложений App Engine.
- Рабочее приложение App Engine модуля 0 (получить из репозитория)
Опрос
Как вы будете использовать этот урок?
Как бы вы оценили свой опыт работы с Python?
Как бы вы оценили свой опыт использования сервисов Google Cloud?
2. Предыстория
Чтобы перейти с API Blobstore App Engine, добавьте его использование к существующему базовому приложению App Engine ndb
из модуля 0. Пример приложения отображает десять последних посещений пользователя. Мы модифицируем приложение, чтобы оно предлагало конечному пользователю загрузить артефакт (файл), соответствующий его «посещению». Если пользователь не желает этого делать, есть опция «пропустить». Независимо от решения пользователя, следующая страница отображает тот же результат, что и приложение из модуля 0 (и многих других модулей этой серии). После реализации интеграции blobstore
App Engine мы можем перенести его в Cloud Storage в следующей лаборатории кода (Модуль 16).
App Engine обеспечивает доступ к системам шаблонов Django и Jinja2 , и единственное, что отличает этот пример (помимо добавления доступа к Blobstore), заключается в том, что он переключается с использования Django в Модуле 0 на Jinja2 здесь, в Модуле 15. Ключевой шаг в модернизации App Engine apps заключается в переносе веб-фреймворков из webapp2
в Flask. Последний использует Jinja2 в качестве системы шаблонов по умолчанию, поэтому мы начинаем двигаться в этом направлении с реализации Jinja2, оставаясь при этом в webapp2
для доступа к Blobstore. Поскольку Flask по умолчанию использует Jinja2, это означает, что в Модуле 16 никаких изменений в шаблоне не потребуется.
3. Настройка/Предварительная работа
Прежде чем мы перейдем к основной части руководства, настройте свой проект, получите код и разверните базовое приложение, чтобы начать работу с рабочим кодом.
1. Проект установки
Если вы уже развернули приложение Модуля 0, мы рекомендуем повторно использовать тот же проект (и код). Альтернативно вы можете создать новый проект или повторно использовать другой существующий проект. Убедитесь, что у проекта есть активный платежный аккаунт и включен App Engine.
2. Получите базовый образец приложения.
Одним из обязательных условий для этой лаборатории кода является наличие работающего примера приложения Модуля 0. Если у вас его нет, вы можете получить его из папки «СТАРТ» Модуля 0 (ссылка ниже). Эта лаборатория кода проведет вас через каждый шаг, завершая кодом, похожим на тот, что находится в папке «FINISH» модуля 15.
- НАЧАЛО: папка модуля 0 (Python 2)
- ФИНИШ: папка модуля 15 (Python 2)
- Весь репозиторий (для клонирования или загрузки ZIP-файла )
Каталог файлов STARTing модуля 0 должен выглядеть следующим образом:
$ ls README.md index.html app.yaml main.py
3. (Повторное) развертывание базового приложения.
Оставшиеся подготовительные шаги, которые необходимо выполнить сейчас:
- Повторно ознакомьтесь с инструментом командной строки
gcloud
- Повторно разверните пример приложения с помощью
gcloud app deploy
- Убедитесь, что приложение работает на App Engine без проблем.
После того как вы успешно выполнили эти шаги и увидели, что ваше веб-приложение работает (вывод аналогичен приведенному ниже), вы готовы добавить в свое приложение использование кэширования.
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. Вот что меняется:
- Цель модуля
os
— создать путь к файлу шаблона Django. Поскольку мы переходим на Jinja2, где это обрабатывается, использованиеos
, а также средства рендеринга шаблонов Django,google.appengine.ext.webapp.template
, больше не требуется, поэтому они удаляются. - Импортируйте API Blobstore:
google.appengine.ext.blobstore
- Импортируйте обработчики Blobstore, найденные в исходной платформе
webapp
— они недоступны вwebapp2
:google.appengine.ext.webapp.blobstore_handlers
- Импортируйте поддержку 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))
Добавить поддержку Blobstore
В отличие от других миграций в этой серии, где мы сохраняем функциональность или вывод примера приложения идентичными (или почти такими же) без (больших) изменений в UX, этот пример демонстрирует более радикальное отклонение от нормы. Вместо того, чтобы немедленно регистрировать новое посещение и затем отображать последние десять посещений, мы обновляем приложение, запрашивая у пользователя файловый артефакт, с помощью которого можно зарегистрировать свое посещение. Конечные пользователи могут либо загрузить соответствующий файл, либо выбрать «Пропустить», чтобы вообще ничего не загружать. После завершения этого шага отобразится страница «последние посещения».
Это изменение позволяет нашему приложению использовать службу 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)
Вот графическое представление изменений, которые были внесены на данный момент:
Поддержка загрузки файлов
Наиболее значительным изменением в функциональности является поддержка загрузки файлов, будь то запрос файла у пользователя, поддержка функции «пропустить» или отображение файла, соответствующего посещению. Все это часть картины. Вот изменения, необходимые для поддержки загрузки файлов:
-
GET
запрос основного обработчика больше не извлекает для отображения самые последние посещения. Вместо этого он предлагает пользователю выполнить загрузку. - Когда конечный пользователь отправляет файл для загрузки или пропускает этот процесс,
POST
из формы передает управление новомуUploadHandler
, производному отgoogle.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler
. - Метод
POST
UploadHandler
выполняет загрузку, вызываетstore_visit()
для регистрации посещения и запускает перенаправление HTTP 307 для отправки пользователя обратно в "/", где... - Метод
POST
основного обработчика запрашивает (черезfetch_visits()
) и отображает самые последние посещения. Если пользователь выбирает «пропустить», файл не загружается, но посещение все равно регистрируется с последующим тем же перенаправлением. - Отображение последних посещений включает в себя новое поле, отображаемое пользователю: либо гиперссылку «Просмотр», если доступен загружаемый файл, либо «Нет» в противном случае. Эти изменения реализованы в шаблоне HTML вместе с добавлением формы загрузки (подробнее об этом скоро).
- Если конечный пользователь нажимает ссылку «Просмотр» для любого посещения с загруженным видео, он отправляет запрос
GET
к новомуViewBlobHandler
, производному отgoogle.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler
, либо отображает файл, если изображение (в браузере, если поддерживается), предложите загрузить, если нет, или вернет ошибку HTTP 404, если не найдена. - В дополнение к новой паре классов обработчиков, а также новой паре маршрутов для отправки к ним трафика, основному обработчику необходим новый метод
POST
для получения описанного выше перенаправления 307.
До этих обновлений в приложении Модуля 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) обработчик загрузки «представления большого двоичного объекта» с методом 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
:
Обновить HTML-шаблон
Некоторые обновления основного приложения затрагивают пользовательский интерфейс приложения, поэтому в веб-шаблоне требуются соответствующие изменения, а именно два:
- Требуется форма загрузки файла с тремя элементами ввода: файлом и парой кнопок отправки для загрузки и пропуска файла соответственно.
- Обновите выходные данные о последних посещениях, добавив ссылку «Просмотр» для посещений с соответствующей загрузкой файла или «Нет» в противном случае.
ДО:
<!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
:
Последнее изменение заключается в том, что Jinja2 предпочитает хранить свои шаблоны в папке templates
, поэтому создайте эту папку и переместите в нее index.html
. Сделав этот последний шаг, вы завершили внесение всех необходимых изменений для добавления использования Blobstore в пример приложения Модуля 0.
(необязательно) «Улучшение» облачного хранилища
Хранилище Blobstore со временем превратилось в облачное хранилище. Это означает, что загрузки Blobstore видны в консоли Cloud, в частности в браузере Cloud Storage. Вопрос в том, где. Ответ — это корзина Cloud Storage по умолчанию для вашего приложения App Engine. Его имя — это полное доменное имя вашего приложения App Engine, PROJECT_ID
.appspot.com
. Это так удобно, ведь идентификаторы всех проектов уникальны, правда?
Обновления, внесенные в пример приложения, помещают загруженные файлы в эту корзину, но у разработчиков есть возможность выбрать более конкретное расположение. Корзина по умолчанию доступна программно через google.appengine.api.app_identity.get_default_gcs_bucket_name()
, требуя нового импорта, если вы хотите получить доступ к этому значению, например, использовать его в качестве префикса для организации загруженных файлов. Например, сортировка по типу файла:
Например, чтобы реализовать что-то подобное для изображений, у вас будет такой код, а также код, который проверяет типы файлов, чтобы выбрать желаемое имя корзины:
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
.appspot.com
), как и main.py
, но обеспечивает основу, необходимую вам, если вы хотите получить образец во что-то большее, как указано в этом разделе. Ниже приведена иллюстрация «различий» между main.py
и main-gcs.py
.
6. Подведение итогов/очистка
В этом разделе завершается эта лабораторная работа по развертыванию приложения и проверке его работы по назначению и во всех отраженных выходных данных. После проверки приложения выполните все действия по очистке и обдумайте следующие шаги.
Развертывание и проверка приложения
Повторно разверните свое приложение с помощью gcloud app deploy
и убедитесь, что приложение работает так, как рекламируется, но отличается по пользовательскому интерфейсу (UX) от приложения Модуля 0. Теперь в вашем приложении есть два разных экрана, первый из которых — приглашение формы загрузки файла посещения:
Оттуда конечные пользователи либо загружают файл и нажимают «Отправить», либо нажимают «Пропустить», чтобы ничего не загружать. В любом случае результатом является экран самого последнего посещения, дополненный ссылками «просмотр» или «нет» между метками времени посещения и информацией о посетителе:
Поздравляем с завершением этой лабораторной работы по добавлению использования App Engine Blobstore в пример приложения модуля 0. Теперь ваш код должен соответствовать тому, что находится в папке FINISH (Модуль 15) . Альтернативный main-gcs.py
также присутствует в этой папке.
Очистить
Общий
Если вы закончили, мы рекомендуем вам отключить приложение App Engine , чтобы избежать выставления счетов. Однако, если вы хотите протестировать или поэкспериментировать еще, на платформе App Engine предусмотрена бесплатная квота , поэтому, пока вы не превысите этот уровень использования, с вас не будет взиматься плата. Это касается вычислений, но за соответствующие службы App Engine также может взиматься плата, поэтому для получения дополнительной информации посетите страницу с ценами . Если эта миграция включает в себя другие облачные службы, они оплачиваются отдельно. В любом случае, если применимо, см. раздел «Специально для этой кодовой лаборатории» ниже.
Для полной информации: развертывание на бессерверной вычислительной платформе Google Cloud, такой как App Engine, требует незначительных затрат на сборку и хранение . Cloud Build имеет собственную бесплатную квоту, как и 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
», если ваше приложение размещено в США.
С другой стороны, если вы не собираетесь продолжать работу с этим приложением или другими связанными с ним программами миграции и хотите полностью удалить все, закройте свой проект .
Специально для этой кодовой лаборатории
Перечисленные ниже услуги являются уникальными для этой лаборатории кода. Дополнительную информацию см. в документации каждого продукта:
- Служба App Engine Blobstore подпадает под действие квот и ограничений на хранимые данные , поэтому ознакомьтесь с ними, а также со страницей цен на устаревшие комплексные услуги .
- Служба хранилища данных App Engine предоставляется Cloud Datastore (Cloud Firestore в режиме хранилища данных), который также имеет уровень бесплатного пользования; дополнительную информацию см. на странице цен .
Следующие шаги
Следующая логическая миграция, которую следует рассмотреть, описана в модуле 16, в котором разработчикам показано, как перейти со службы Blobstore App Engine на использование клиентской библиотеки 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-файла.
Кодлаб | Питон 2 | Питон 3 |
Модуль 0 | Н/Д | |
Модуль 15 (эта кодовая лаборатория) | Н/Д |
Интернет-ресурсы
Ниже приведены онлайн-ресурсы, которые могут иметь отношение к этому руководству:
Механизм приложений
- Служба Blobstore App Engine
- Квоты и ограничения на хранимые данные App Engine
- Документация App Engine
- Среда выполнения Python 2 App Engine (стандартная среда)
- Использование встроенных библиотек App Engine в App Engine Python 2
- Информация о ценах и квотах App Engine
- Запуск платформы App Engine второго поколения (2018 г.)
- Сравнение платформ первого и второго поколения
- Долгосрочная поддержка устаревших сред выполнения
- Репозиторий образцов миграции документации
- Репозиторий образцов миграции, предоставленных сообществом
Google Облако
- Python на облачной платформе Google
- Клиентские библиотеки Google Cloud Python
- Уровень Google Cloud «Всегда бесплатно»
- Google Cloud SDK (инструмент командной строки gcloud)
- Вся документация Google Cloud
Питон
- Системы шаблонов Django и Jinja2.
- веб-фреймворк
webapp2
- документация
webapp2
- ссылки
webapp2_extras
-
webapp2_extras
Документация по Jinja2
Видео
- Станция бессерверной миграции
- Бессерверные экспедиции
- Подпишитесь на Google Cloud Tech
- Подпишитесь на Google Developers
Лицензия
Эта работа распространяется под лицензией Creative Commons Attribution 2.0 Generic License.