Фінальний проєкт: Dayquote
Зберіть, задеплойте й випустіть новий .NET-сервіс на Cloud Run з порожньої теки, використовуючи все з модулів 1-6 і без покрокових підказок.
Від: ваш тімлід Тема: Dayquote, до п’ятниці
На головну сторінку інтранету хочуть блок «цитата дня». Продакт хоче, щоб він був детермінованим (у той самий день та сама цитата, всюди), мобільна команда хоче поле
langу наступному релізі, не зламавши поточний застосунок, а безпека хоче адмінський endpoint, який ніхто не викличе без підпису. Правила ті самі, що й для Sniplink: усе як код, CI без ключів, жодного клікання в консолі. Специфікація в додатку. Кроки я не перевірятиму, лише результат.
Цього разу без логу інциденту. Поки що нічого не зламано, бо поки що нічого й не існує.
Що це за модуль
Section titled “Що це за модуль”У модулях 1-6 я проводив вас через кожне рішення на прикладі Sniplink. Тут так не буде. Ви отримуєте специфікацію, файл із цитатами й набір автоматичних перевірок, і будуєте Dayquote з порожньої теки: stateless API, що для будь-якої дати повертає одну з 20 цитат.
Застосунок навмисно маленький. Його бізнес-логіка вміщується в тридцять рядків, тож майже всі ваші три години йдуть туди, про що цей курс: образ, ідентичність, секрет, налаштування життєвого циклу, реліз і пайплайн. У цьому й суть. На реальній роботі endpoint є найпростішою частиною.
Що ви будуєте
Section titled “Що ви будуєте”Обов’язковий контракт описано в
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. На некоректну або відсутню дату повертається400Problem 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, що дорівнює вашому ідентифікатору студента із закоміченого конфігу stackdayquote:studentId.
Адмінський ключ є ключем лаби, показаним на сторінці цієї лаби; той самий механізм, що й у модулі 3, але зі свіжим ключем для цієї лаби. Оскільки ключ видала платформа, вона сама може підписувати адмінські пінги й перевіряти, що ви приймаєте правильний підпис і відхиляєте неправильний. Ви нічого секретного їй не надсилаєте, і вона ніколи не просить ваш сервіс розкрити ключ.
Правила
Section titled “Правила”Перевикористання дозволене й вітається. Копіюйте свій Dockerfile,
cloudbuild.yaml, патерни компонентів Pulumi і ILabSecretSource зі Sniplink.
Інженери не переписують робочу інфраструктуру заради спорту.
Але це має бути новий сервіс і новий stack. Конкретно:
- Новий репозиторій (або принаймні нова історія гілки
main) з власним кодом Dayquote, тестами,Dockerfileіcloudbuild.yaml. - Нові проєкти й stack-и Pulumi:
dayquoteвinfra/для сервісу й пайплайну та невеликийdayquote-secretsвinfra/secrets/, який володіє лише секретом адмінського ключа (чому так, пояснює розділ 5 SPEC). Обидва використовують stackdev, як Sniplink. Не перейменованийsniplinkі не другий сервіс, прикручений до stack-а Sniplink.pulumi destroyна Dayquote не має чіпати Sniplink, і навпаки. - Новий сервіс Cloud Run, назва якого починається з
dayquote-, новий runtime сервіс-акаунт, новий секрет, новий тригер. Перевірки шукають саме їх. quotes.jsonбез змін. Ті самі 20 записів, у тому самому порядку.- Жодного деплою сервісу з ноутбука. З ноутбука можна запускати
pulumi preview, локальні Docker-білди, stack секретів і одноразове застосування платформних ресурсів (build SA, зв’язок із репозиторієм, тригер), точно як у модулі 6. Сам сервіс деплоїть тригер.
Використати той самий курсовий проєкт, що й для Sniplink, нормально й найпростіше. Якщо ви його вже видалили, спершу перезапустіть bootstrap-скрипт із модуля 0 і одноразове встановлення GitHub-застосунку з модуля 6.
Рекомендований порядок роботи
Section titled “Рекомендований порядок роботи”Я б робив у такому порядку, бо кожен крок дає щось, що можна перевірити, перш ніж наступний сховає помилку.
-
Функція, локально (20 хв).
dotnet new web, завантажтеquotes.json, напишіть обчислення індексу й endpoint. Напишіть xUnit-тести на чотири розібрані приклади зі специфікації, включно з 1999-12-31, перш ніж довіряти своєму modulo. Додайте Problem Details для некоректного вводу. -
Образ (20 хв). Багатоетапний Dockerfile, chiseled runtime,
InvariantGlobalization. Запустіть його з-e PORT=8080 -p 8080:8080і викличте черезcurl. Якщоquotes.jsonнемає в контейнері, ви дізнаєтеся про це тут, а не на Cloud Run. -
Адмінський endpoint (25 хв). Читайте ключ із файлу, шлях до якого задано в конфігурації, читайте сире тіло, порівнюйте за константний час. Протестуйте локально з файлом ключа на диску й підписом з
openssl dgst -sha256 -hmac. -
Stack секретів (20 хв).
infra/secrets/: секрет і одна версія зpulumi config set --secret adminKey, з ключем лаби v1 з цієї сторінки.pulumi upз ноутбука. -
Stack сервісу, один раз вручну (30 хв).
infra/: репозиторій Artifact Registry, runtime SA, accessor-прив’язка на секреті, сервіс Cloud Run з томом, пробами, масштабуванням, кодом трафіку з модуля 6 і змінними середовища. Запустітьpulumi upз ноутбука один раз, передавши digest вручну зібраного образу йrevisionSuffix=manual1через--config(як це робитиме пайплайн), щоб довести, що stack працює. Правило 5 стосується фінального стану, а не першої спроби; крок 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. -
Реліз (25 хв). Додайте
lang. У тому самому коміті задайтеversion: 2.0.0,trafficMode: canary,stableRevisionяк назву ревізії v1 з кроку 6, іcanaryPercent: 0. Запуште. Перевірте обидві URL-адреси. Відтепер нічого не пуште в сервіс. -
Перевірка й ротація (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належать шаблону ревізії. Якщо задати їх деінде, це або впаде, або не застосується до ревізії, яку перевіряє проба.
Як працює оцінювання
Section titled “Як працює оцінювання”Вставте на сторінці лаби три значення: 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.
- Скопіюйте ключ лаби v1 з цієї сторінки у свій stack секретів:
cd infra/secrets && pulumi config set --secret adminKey(вставте на запит), потімpulumi upтам же. - Пуште в
main, доки тригер не задеплоїть v1 зtrafficMode: promote, а потім v2 зtrafficMode: canary,stableRevision, що вказує на ревізію v1, іcanaryPercent: 0. - Натисніть «Перевірити». Коли прогін зупиниться на
secret-rotation, скопіюйте ключ лаби v2, задайте його в stack-у секретів і запустіть тамpulumi up(це додасть нову версію й вимкне стару), зачекайте, доки мине вікно кешу, потім натисніть «Продовжити». Нічого не пуште в репозиторій сервісу між цими кроками.
Перевірте лабу
Сертифікат
Section titled “Сертифікат”Проходження m07-final за вже пройдених m01-m06 видає сертифікат
Containerizing & Deploying .NET on Google Cloud у вашому профілі на
learn.vladtimchenko.dev. У нього є публічне посилання для верифікації, яке
можна додати в LinkedIn чи CV. Сертифікат фіксує, які лаби пройдено і коли; він
не зберігає ID вашого проєкту чи будь-що з вашого сервісу.
Він засвідчує статус «Completed»: справжній сервіс працював у вашому проєкті й пройшов зовнішні перевірки. Це сертифікат незалежного курсу, а не сертифікація Google.
Прибирання
Section titled “Прибирання”Після цього модуля stack більше ніде не використовується. Знесіть усе, щоб воно перестало коштувати центи.
export PROJECT_ID=your-course-projectexport 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 infrapulumi destroypulumi stack rm dev
# The secrets stack: secret and its versions (after the service stack,# which holds the accessor binding on this secret)cd secretspulumi destroypulumi stack rm devЯкщо ви створили тригер Cloud Build у stack-у сервісу, pulumi destroy його
видалить. Перевірте, що нічого не лишилося, бо забутий тригер і далі
запускається на кожен push:
gcloud builds triggers list --region="$REGION" --project="$PROJECT_ID"# Delete anything Dayquote- or Sniplink-related that is still listedgcloud builds triggers delete TRIGGER_NAME --region="$REGION" --project="$PROJECT_ID"Зробіть те саме для Sniplink, якщо ви залишили його після модуля 6.
Потім подумайте про видалення всього курсового проєкту. Це найчистіший спосіб переконатися, що версії ключів KMS, бакет зі state і будь-який образ, що лишився в Artifact Registry, перестануть тарифікуватися. Саме тому модуль 0 просив окремий проєкт лише для курсу. Проєкт можна відновити протягом приблизно 30 днів.
gcloud projects delete "$PROJECT_ID"Натомість залиште проєкт, якщо плануєте пройти рівень Verified, коли він з’явиться; він читає той самий проєкт.
Що ви вивчили
Section titled “Що ви вивчили”- Ви вмієте провести .NET-сервіс від порожньої теки до Cloud Run, де кожне налаштування обране свідомо, а не скопійоване з туторіалу.
- Специфікація з розібраними прикладами й граничними датами ловить баги, яких локальні happy path ніколи не ловлять.
- Читання секретів у runtime, окрема ідентичність і CI без ключів складаються в один шлях деплою, який ви можете пояснити рядок за рядком.
- Адитивні зміни API плюс canary-тег на 0 % дають двом версіям безпечно співіснувати.
- Перевірка ззовні має межі; ви знаєте, які з ваших тверджень доведені, а які самозвітні.