Перейти до вмісту

Ви увійшли як

Вхід у Curious Dev Learn

Увійдіть, щоб перевіряти лаби, зберігати прогрес і отримати сертифікат. Уроки відкриті й без акаунта.

Модуль 073 год 15 хвЛаба

Фінальний проєкт: Dayquote

Зберіть, задеплойте й випустіть новий .NET-сервіс на Cloud Run з порожньої теки, використовуючи все з модулів 1-6 і без покрокових підказок.

Від: ваш тімлід Тема: Dayquote, до п’ятниці

На головну сторінку інтранету хочуть блок «цитата дня». Продакт хоче, щоб він був детермінованим (у той самий день та сама цитата, всюди), мобільна команда хоче поле lang у наступному релізі, не зламавши поточний застосунок, а безпека хоче адмінський endpoint, який ніхто не викличе без підпису. Правила ті самі, що й для Sniplink: усе як код, CI без ключів, жодного клікання в консолі. Специфікація в додатку. Кроки я не перевірятиму, лише результат.

Цього разу без логу інциденту. Поки що нічого не зламано, бо поки що нічого й не існує.

У модулях 1-6 я проводив вас через кожне рішення на прикладі Sniplink. Тут так не буде. Ви отримуєте специфікацію, файл із цитатами й набір автоматичних перевірок, і будуєте Dayquote з порожньої теки: stateless API, що для будь-якої дати повертає одну з 20 цитат.

Застосунок навмисно маленький. Його бізнес-логіка вміщується в тридцять рядків, тож майже всі ваші три години йдуть туди, про що цей курс: образ, ідентичність, секрет, налаштування життєвого циклу, реліз і пайплайн. У цьому й суть. На реальній роботі endpoint є найпростішою частиною.

Обов’язковий контракт описано в labs/m07-final/SPEC.uk.md. Прочитайте його повністю, перш ніж писати код. Коротко:

  • GET /api/quote?date=YYYY-MM-DD повертає { date, index, text, author }, де index = (days since 2000-01-01) mod 20, а цитата береться з quotes.json. На некоректну або відсутню дату повертається 400 Problem Details.
  • v2 API додає "lang": "en" і більше нічого не змінює. v1 обслуговує основну URL-адресу, v2 сидить за тегом canary з 0 %, за моделлю трафіку з модуля 6: одна ревізія на білд (dayquote-api-<shortSha>), трафік береться із закоміченого конфігу stack (trafficMode, stableRevision, canaryPercent).
  • POST /api/admin/ping приймає запит, лише якщо X-Admin-Signature містить hex HMAC-SHA256 від сирого тіла з адмінським ключем, який сервіс читає з тому Secret Manager у момент запиту.
  • /health/live, /health/ready і LabKit.
  • Chiseled-образ без root; окремий runtime сервіс-акаунт; concurrency 20; максимум 2 інстанси; startup- і liveness-проби; коректне (graceful) завершення.
  • Збирається й деплоїться лише тригером Cloud Build на вашому публічному GitHub-репозиторії, з APP_VERSION, COMMIT_SHA і BUILD_ID на кожній ревізії та з LABKIT_STUDENT_ID, що дорівнює вашому ідентифікатору студента із закоміченого конфігу stack dayquote:studentId.

Адмінський ключ є ключем лаби, показаним на сторінці цієї лаби; той самий механізм, що й у модулі 3, але зі свіжим ключем для цієї лаби. Оскільки ключ видала платформа, вона сама може підписувати адмінські пінги й перевіряти, що ви приймаєте правильний підпис і відхиляєте неправильний. Ви нічого секретного їй не надсилаєте, і вона ніколи не просить ваш сервіс розкрити ключ.

Перевикористання дозволене й вітається. Копіюйте свій Dockerfile, cloudbuild.yaml, патерни компонентів Pulumi і ILabSecretSource зі Sniplink. Інженери не переписують робочу інфраструктуру заради спорту.

Але це має бути новий сервіс і новий stack. Конкретно:

  1. Новий репозиторій (або принаймні нова історія гілки main) з власним кодом Dayquote, тестами, Dockerfile і cloudbuild.yaml.
  2. Нові проєкти й stack-и Pulumi: dayquote в infra/ для сервісу й пайплайну та невеликий dayquote-secrets в infra/secrets/, який володіє лише секретом адмінського ключа (чому так, пояснює розділ 5 SPEC). Обидва використовують stack dev, як Sniplink. Не перейменований sniplink і не другий сервіс, прикручений до stack-а Sniplink. pulumi destroy на Dayquote не має чіпати Sniplink, і навпаки.
  3. Новий сервіс Cloud Run, назва якого починається з dayquote-, новий runtime сервіс-акаунт, новий секрет, новий тригер. Перевірки шукають саме їх.
  4. quotes.json без змін. Ті самі 20 записів, у тому самому порядку.
  5. Жодного деплою сервісу з ноутбука. З ноутбука можна запускати pulumi preview, локальні Docker-білди, stack секретів і одноразове застосування платформних ресурсів (build SA, зв’язок із репозиторієм, тригер), точно як у модулі 6. Сам сервіс деплоїть тригер.

Використати той самий курсовий проєкт, що й для Sniplink, нормально й найпростіше. Якщо ви його вже видалили, спершу перезапустіть bootstrap-скрипт із модуля 0 і одноразове встановлення GitHub-застосунку з модуля 6.

Рекомендований порядок роботи

Section titled “Рекомендований порядок роботи”

Я б робив у такому порядку, бо кожен крок дає щось, що можна перевірити, перш ніж наступний сховає помилку.

  1. Функція, локально (20 хв). dotnet new web, завантажте quotes.json, напишіть обчислення індексу й endpoint. Напишіть xUnit-тести на чотири розібрані приклади зі специфікації, включно з 1999-12-31, перш ніж довіряти своєму modulo. Додайте Problem Details для некоректного вводу.

  2. Образ (20 хв). Багатоетапний Dockerfile, chiseled runtime, InvariantGlobalization. Запустіть його з -e PORT=8080 -p 8080:8080 і викличте через curl. Якщо quotes.json немає в контейнері, ви дізнаєтеся про це тут, а не на Cloud Run.

  3. Адмінський endpoint (25 хв). Читайте ключ із файлу, шлях до якого задано в конфігурації, читайте сире тіло, порівнюйте за константний час. Протестуйте локально з файлом ключа на диску й підписом з openssl dgst -sha256 -hmac.

  4. Stack секретів (20 хв). infra/secrets/: секрет і одна версія з pulumi config set --secret adminKey, з ключем лаби v1 з цієї сторінки. pulumi up з ноутбука.

  5. Stack сервісу, один раз вручну (30 хв). infra/: репозиторій Artifact Registry, runtime SA, accessor-прив’язка на секреті, сервіс Cloud Run з томом, пробами, масштабуванням, кодом трафіку з модуля 6 і змінними середовища. Запустіть pulumi up з ноутбука один раз, передавши digest вручну зібраного образу й revisionSuffix=manual1 через --config (як це робитиме пайплайн), щоб довести, що stack працює. Правило 5 стосується фінального стану, а не першої спроби; крок 6 замінить цю ревізію.

  6. Пайплайн (35 хв). Build SA, репозиторій 2-го покоління на вашому наявному підключенні, тригер з IgnoredFiles = ["infra/secrets/**"], cloudbuild.yaml з dotnet test, білдом, push і pulumi up. Закомітьте version: 1.0.0 і trafficMode: promote, запуште в main і стежте за логом білда, доки основна URL-адреса не почне віддавати dayquote-api-<sha> зі справжніми COMMIT_SHA/BUILD_ID.

  7. Реліз (25 хв). Додайте lang. У тому самому коміті задайте version: 2.0.0, trafficMode: canary, stableRevision як назву ревізії v1 з кроку 6, і canaryPercent: 0. Запуште. Перевірте обидві URL-адреси. Відтепер нічого не пуште в сервіс.

  8. Перевірка й ротація (20 хв). Натисніть «Перевірити», виконайте підказку щодо ротації, виправте те, що впало.

Разом це 3 год 15 хв для того, хто нещодавно пройшов модулі 1-6. Якщо у вас була довга перерва, додайте годину на перечитування власного коду Sniplink.

Типові причини провалу

Section titled “Типові причини провалу”

Кожна з них проходить швидкий локальний тест і валить перевірку.

  • Від’ємний modulo. days % 20 дає -1 для 1999-12-31. % у C# дає остачу від ділення. Перевірка quote-before-epoch існує саме для цього.
  • Хешування повторно серіалізованого JSON. Якщо прив’язати тіло адмінського запиту до record, а потім хешувати JsonSerializer.Serialize(record), ви отримаєте інші байти, ніж надіслала платформа. Читайте сире тіло. Поле bodySha256 у відповіді покаже, чи ви хешували правильні байти.
  • Читання ключа один раз на старті. Тоді secret-rotation впаде, бо та сама ревізія тримає старий ключ. Читайте файл на кожен запит або кешуйте на кілька секунд.
  • Ротація створює нову ревізію. Якщо додавання нової версії ключа йде через основний тригер, білд створює нову ревізію, і перевірка бачить іншу. Тримайте версії секрету в проєкті dayquote-secrets, налаштуйте тригер ігнорувати infra/secrets/** і робіть ротацію з ноутбука.
  • Canary на 10 %. Тоді кожен десятий запит на основну адресу потрапляє на v2, і main-is-v1 падає випадково. На момент перевірки тримайте 0 %.
  • 400 без detail. Results.Problem() без аргументу detail його пропускає, а TypedResults.BadRequest() взагалі не повертає Problem Details.
  • quotes.json немає в образі. Його треба скопіювати в publish output (CopyToPublishDirectory) або вбудувати як ресурс. Якщо його немає, /health/ready має відповідати 503, а не падати.
  • Concurrency задано на сервісі, а не на шаблоні. У Cloud Run v2 MaxInstanceRequestConcurrency і Scaling належать шаблону ревізії. Якщо задати їх деінде, це або впаде, або не застосується до ревізії, яку перевіряє проба.

Вставте на сторінці лаби три значення: URL-адресу основного сервісу, URL-адресу тегу canary--- і URL-адресу вашого публічного GitHub-репозиторію. Платформа запускає 32 перевірки в порядку, наведеному в специфікації: ID-токен, контрактні тести, версії на обох URL-адресах, ідентичність, secret proof і адмінські пінги, ротація, проба concurrency, походження білда й перевірки образу.

Перевірка з позначкою blocking зупиняє прогін, якщо падає, бо все після неї впало б із тієї самої причини: токен, назва сервісу й тег canary. Усі інші перевірки запускаються й звітують незалежно, кожна з однорядковою підказкою.

Кілька перевірок самозвітні: версії, ID коміту й білда, лічильники concurrency і чотири перевірки образу (non-root користувач, відсутність shell, invariant globalization, .NET 10). Вони довіряють LabKit усередині вашого контейнера, тож сервіс міг би збрехати. Опційний рівень Verified, який з’явиться пізніше, читатиме образ і конфіг Cloud Run напряму. Три вимоги взагалі не можна побачити ззовні, тож вони лишаються на вашій совісті як список самоперевірки:

  • Runtime SA має secretAccessor лише на секреті адмінського ключа, з прив’язкою на секреті, а не на проєкті.
  • Startup-проба цілиться в /health/ready, а liveness-проба в /health/live, обидві визначені в Pulumi.
  • HostOptions.ShutdownTimeout менший за 10-секундний grace period Cloud Run, і ви бачили, як деплой завершується без невдалих запитів.

Мета: Dayquote v1 на основній URL-адресі й v2 на тегу canary, задеплоєні вашим тригером Cloud Build, що проходять кожну перевірку в SPEC.uk.md.

  1. Скопіюйте ключ лаби v1 з цієї сторінки у свій stack секретів: cd infra/secrets && pulumi config set --secret adminKey (вставте на запит), потім pulumi up там же.
  2. Пуште в main, доки тригер не задеплоїть v1 з trafficMode: promote, а потім v2 з trafficMode: canary, stableRevision, що вказує на ревізію v1, і canaryPercent: 0.
  3. Натисніть «Перевірити». Коли прогін зупиниться на secret-rotation, скопіюйте ключ лаби v2, задайте його в stack-у секретів і запустіть там pulumi up (це додасть нову версію й вимкне стару), зачекайте, доки мине вікно кешу, потім натисніть «Продовжити». Нічого не пуште в репозиторій сервісу між цими кроками.

Перевірте лабу

Завантажуємо лабу…

Увійдіть, щоб перевірити цю лабу й зберегти прогрес. Усе вище працює без акаунта.

  • URL вашого сервісу Cloud Run
  • URL вашого canary-тегу
  • URL вашого публічного репозиторію GitHub
  • Лабораторний ключ v1

Перевірок для сервісу, який ви розгорнули: 32.

Деякі перевірки самозвітні: перевірка довіряє тому, що сервіс каже про себе, а решту перевіряє ззовні.

Проходження m07-final за вже пройдених m01-m06 видає сертифікат Containerizing & Deploying .NET on Google Cloud у вашому профілі на learn.vladtimchenko.dev. У нього є публічне посилання для верифікації, яке можна додати в LinkedIn чи CV. Сертифікат фіксує, які лаби пройдено і коли; він не зберігає ID вашого проєкту чи будь-що з вашого сервісу.

Він засвідчує статус «Completed»: справжній сервіс працював у вашому проєкті й пройшов зовнішні перевірки. Це сертифікат незалежного курсу, а не сертифікація Google.

Після цього модуля stack більше ніде не використовується. Знесіть усе, щоб воно перестало коштувати центи.

Terminal window
export PROJECT_ID=your-course-project
export REGION=europe-west1
# The service stack: Cloud Run, runtime SA, Artifact Registry repo, trigger.
# Like Sniplink after module 6, it needs the per-build config to evaluate;
# pass the deployed values with --config if it asks for dayquote:image.
cd infra
pulumi destroy
pulumi stack rm dev
# The secrets stack: secret and its versions (after the service stack,
# which holds the accessor binding on this secret)
cd secrets
pulumi destroy
pulumi stack rm dev

Якщо ви створили тригер Cloud Build у stack-у сервісу, pulumi destroy його видалить. Перевірте, що нічого не лишилося, бо забутий тригер і далі запускається на кожен push:

Terminal window
gcloud builds triggers list --region="$REGION" --project="$PROJECT_ID"
# Delete anything Dayquote- or Sniplink-related that is still listed
gcloud builds triggers delete TRIGGER_NAME --region="$REGION" --project="$PROJECT_ID"

Зробіть те саме для Sniplink, якщо ви залишили його після модуля 6.

Потім подумайте про видалення всього курсового проєкту. Це найчистіший спосіб переконатися, що версії ключів KMS, бакет зі state і будь-який образ, що лишився в Artifact Registry, перестануть тарифікуватися. Саме тому модуль 0 просив окремий проєкт лише для курсу. Проєкт можна відновити протягом приблизно 30 днів.

Terminal window
gcloud projects delete "$PROJECT_ID"

Натомість залиште проєкт, якщо плануєте пройти рівень Verified, коли він з’явиться; він читає той самий проєкт.

  • Ви вмієте провести .NET-сервіс від порожньої теки до Cloud Run, де кожне налаштування обране свідомо, а не скопійоване з туторіалу.
  • Специфікація з розібраними прикладами й граничними датами ловить баги, яких локальні happy path ніколи не ловлять.
  • Читання секретів у runtime, окрема ідентичність і CI без ключів складаються в один шлях деплою, який ви можете пояснити рядок за рядком.
  • Адитивні зміни API плюс canary-тег на 0 % дають двом версіям безпечно співіснувати.
  • Перевірка ззовні має межі; ви знаєте, які з ваших тверджень доведені, а які самозвітні.