Налаштуйте робоче середовище
Створіть окремий проєкт Google Cloud для курсу, встановіть інструменти, запустіть bootstrap-скрипт і підніміть Sniplink локально.
Цей урок проведе вас від нуля до проєкту Google Cloud, яким може керувати Pulumi, плюс Sniplink, що працює на вашому ноутбуці. Це єдиний урок, де ви налаштовуєте інфраструктуру напряму через gcloud, і наприкінці ви побачите, чому це все одно не ClickOps: це один скрипт у git, який можна прочитати і запустити двічі.
Акаунти
Section titled “Акаунти”Вам потрібні два акаунти.
Google Cloud. Зареєструйтеся на cloud.google.com з особистим Google-акаунтом. Нові акаунти отримують free trial із кредитом (на момент написання 300 USD на 90 днів) на додачу до always-free tier. Скажу прямо про те, що людям не подобається: trial просить платіжну картку. Google використовує її для перевірки особи і обіцяє не списувати гроші під час trial, якщо ви самі не перейдете на платний акаунт. Коли trial закінчується, ресурси зупиняються, доки ви не перейдете на платний план.
Trial покриває все в цьому курсі. Якщо ви виконуватимете кроки з прибирання, весь курс має обійтися в центи, а більша частина вкладається у free tier. Нижче, у розділі «Скільки це коштує», є конкретика.
GitHub. Будь-який безкоштовний акаунт. Ви створите власний репозиторій зі стартового коду курсу, а в модулі 6 Cloud Build збиратиме з нього. Для лаби модуля 6 ваш репозиторій має бути публічним, бо платформа перевіряє в ньому коміт.
Створіть окремий проєкт для курсу
Section titled “Створіть окремий проєкт для курсу”Створіть новий проєкт Google Cloud, який використовуватиметься лише для цього курсу. Не ваш pet-проєкт і не той, що лишився з туторіалу дворічної давнини. Новий.
- Ізоляція. Кожна IAM-прив’язка і кожен ресурс у ньому існують через цей курс. Коли щось не так, підозрювати більше нічого.
- Прибирання однією командою. Коли закінчите, видалення проєкту прибере все, що в ньому є, включно з тим, про що ви забули.
- Рівень Verified згодом. Необов’язковий рівень Verified попросить вас надати платформі доступ лише на читання до вашого проєкту. Це комфортно лише тоді, коли в проєкті немає нічого, крім курсової роботи.
Project ID глобально унікальні, у нижньому регістрі, 6-30 символів. Щось на кшталт sniplink-course-4821 підійде. Я використовую ці два рядки export до кінця уроку; задайте їх один раз у своєму shell:
export PROJECT_ID="sniplink-course-4821" # your own, globally uniqueexport REGION="europe-west1"Сам проєкт ви створите в розділі про інструменти нижче, одразу після встановлення gcloud.
Інструменти
Section titled “Інструменти”Встановіть ось це. Версії мають менше значення, ніж можна побоюватися, але використовуйте .NET 10, бо курс таргетить net10.0.
| Інструмент | Навіщо | Перевірка |
|---|---|---|
| .NET 10 SDK | збирати, тестувати й запускати Sniplink | dotnet --version (10.0.x) |
| Docker Desktop або Docker Engine | збирати й запускати контейнери локально (з модуля 2; корисно вже з модуля 1) | docker version |
Google Cloud CLI (gcloud) |
bootstrap, логи, автентифікація для Docker і Pulumi | gcloud version |
| Pulumi CLI | інфраструктура як код | pulumi version |
| Git | ваш репозиторій | git --version |
| Редактор | Rider, Visual Studio або VS Code з C# Dev Kit |
GitHub CLI (gh) необов’язковий, але заощадить кілька кроків нижче.
Запустіть перевірки разом:
dotnet --versiondocker version --format '{{.Server.Version}}'gcloud versionpulumi versiongit --versionЯкщо docker version виводить версію клієнта, але падає на частині server, Docker daemon не запущений. Запустіть Docker Desktop або виконайте sudo systemctl start docker на Linux.
Тепер залогіньте gcloud. Тут два окремі логіни, і потрібні обидва:
# Credentials for gcloud commands themselvesgcloud auth login
# Application Default Credentials: what Pulumi, the KMS secrets provider# and the Google client libraries usegcloud auth application-default loginПерший для вас, коли ви набираєте gcloud .... Другий записує файл з credentials, який підхоплюють програми. Pulumi не використовує ваш логін gcloud; він використовує Application Default Credentials. Забута друга команда є найчастішою причиною того, що pulumi login gs://... падає з помилкою доступу.
Створіть проєкт і знайдіть ID свого платіжного акаунта:
gcloud projects create "$PROJECT_ID" --name="Sniplink course"
# Lists billing accounts you can use; copy the ACCOUNT_ID columngcloud billing accounts list
export BILLING_ACCOUNT_ID="XXXXXX-XXXXXX-XXXXXX"Створіть свій репозиторій
Section titled “Створіть свій репозиторій”Стартовий код лежить у теці starter/ репозиторію курсу. Завантажте його як zip і розпакуйте в нову теку:
curl -fLO https://learn.vladtimchenko.dev/downloads/containerize-deploy/sniplink-starter.zipunzip sniplink-starter.zip -d sniplinkcd sniplinkАбо скопіюйте теку просто з GitHub через degit: він завантажує файли без історії репозиторію курсу (потрібен Node.js):
npx degit curious-dev-learn/containerize-deploy-dotnet-on-gcp/starter sniplinkcd sniplinkВ обох випадках ви отримуєте звичайні файли, а не клон. Зробіть із них власний репозиторій з першим комітом і опублікуйте його на GitHub. Через gh:
git init -b maingit add .git commit -m "initial commit"gh repo create sniplink --public --source . --pushБез gh створіть на github.com порожній публічний репозиторій sniplink (без README і ліцензії, щоб перший push не конфліктував), потім виконайте git remote add origin https://github.com/YOUR_USER/sniplink.git і git push -u origin main. Цей клік відбувається на GitHub, а не у вашій хмарній інфраструктурі, тож правило він не порушує.
Репозиторій має бути публічним. У модулі 6 платформа питає публічний API GitHub, чи існує в ньому коміт, з якого працює ваш сервіс, а приватного репозиторію вона не бачить. Гілка називається main, бо тригер у модулі 6 збирає push-і в main.
У стартовому репозиторії є API, тестовий проєкт, порожнє місце для infra/ (ви заповните його в модулі 1) і scripts/bootstrap.sh.
Bootstrap-скрипт
Section titled “Bootstrap-скрипт”Pulumi може створити у вашому проєкті майже все, але перед першим запуском йому потрібні три речі:
- Бекенд для state: місце, де записано, які реальні ресурси належать вашій програмі. Ми використовуємо бакет Cloud Storage
gs://PROJECT_ID-pulumi-state. - Secrets provider: ключ для шифрування секретних значень конфігурації (один такий ви збережете в модулі 3). Ми використовуємо ключ Cloud KMS
pulumi/state, тож жодна passphrase не живе ні на вашому ноутбуці, ні в CI. - Проєкт із прив’язаним білінгом і увімкненими API, бо Pulumi викликає ці API.
На додачу я додаю бюджет з алертами, бо курс, у якому ви створюєте хмарні ресурси, має попереджати вас раніше, ніж здивує.
Ось повний скрипт. Прочитайте його, перш ніж запускати; він навмисно короткий.
#!/usr/bin/env bash# scripts/bootstrap.sh## One-time bootstrap for the "Containerize & Deploy" course project.# This is the only infrastructure in the course that is not managed by Pulumi:# it creates what Pulumi itself needs (state bucket, KMS key) plus billing# guard rails. Safe to run more than once: every step checks before it creates.## Usage:# export PROJECT_ID="your-course-project-id"# export BILLING_ACCOUNT_ID="XXXXXX-XXXXXX-XXXXXX"# export REGION="europe-west1" # optional, default europe-west1# export BUDGET_AMOUNT="5USD" # optional, must use your billing account's currency# ./scripts/bootstrap.sh
set -euo pipefail
: "${PROJECT_ID:?Set PROJECT_ID to your course project ID}": "${BILLING_ACCOUNT_ID:?Set BILLING_ACCOUNT_ID (see: gcloud billing accounts list)}"REGION="${REGION:-europe-west1}"BUDGET_AMOUNT="${BUDGET_AMOUNT:-5USD}"
STATE_BUCKET="gs://${PROJECT_ID}-pulumi-state"KMS_KEYRING="pulumi"KMS_KEY="state"BUDGET_NAME="${PROJECT_ID}-course-budget"
APIS=( run.googleapis.com artifactregistry.googleapis.com secretmanager.googleapis.com cloudbuild.googleapis.com cloudkms.googleapis.com iam.googleapis.com iamcredentials.googleapis.com cloudresourcemanager.googleapis.com storage.googleapis.com cloudbilling.googleapis.com billingbudgets.googleapis.com # required by 'gcloud billing budgets create')
log() { printf '\n==> %s\n' "$*"; }
# 1. Project ------------------------------------------------------------------log "Using project ${PROJECT_ID}"if ! gcloud projects describe "${PROJECT_ID}" --format='value(projectId)' >/dev/null 2>&1; then echo "Project ${PROJECT_ID} does not exist or you have no access to it." >&2 echo "Create it first: gcloud projects create ${PROJECT_ID}" >&2 exit 1figcloud config set project "${PROJECT_ID}" --quiet
# 2. Billing ------------------------------------------------------------------log "Linking billing account ${BILLING_ACCOUNT_ID}"CURRENT_BILLING="$(gcloud billing projects describe "${PROJECT_ID}" \ --format='value(billingAccountName)')"if [[ "${CURRENT_BILLING}" == "billingAccounts/${BILLING_ACCOUNT_ID}" ]]; then echo "Already linked."else gcloud billing projects link "${PROJECT_ID}" \ --billing-account="${BILLING_ACCOUNT_ID}"fi
# 3. APIs (enabling an already enabled API is a no-op) ------------------------log "Enabling APIs"gcloud services enable "${APIS[@]}" --project="${PROJECT_ID}"
# 4. Pulumi state bucket ------------------------------------------------------log "State bucket ${STATE_BUCKET}"if gcloud storage buckets describe "${STATE_BUCKET}" >/dev/null 2>&1; then echo "Bucket exists."else gcloud storage buckets create "${STATE_BUCKET}" \ --project="${PROJECT_ID}" \ --location="${REGION}" \ --uniform-bucket-level-access \ --public-access-preventionfi# Versioning lets you recover an older state file if a run corrupts it.gcloud storage buckets update "${STATE_BUCKET}" --versioning
# 5. KMS key ring and key for Pulumi secrets ----------------------------------log "KMS key ring '${KMS_KEYRING}' and key '${KMS_KEY}' in ${REGION}"if gcloud kms keyrings describe "${KMS_KEYRING}" \ --location="${REGION}" --project="${PROJECT_ID}" >/dev/null 2>&1; then echo "Key ring exists."else gcloud kms keyrings create "${KMS_KEYRING}" \ --location="${REGION}" --project="${PROJECT_ID}"fi
if gcloud kms keys describe "${KMS_KEY}" \ --keyring="${KMS_KEYRING}" --location="${REGION}" \ --project="${PROJECT_ID}" >/dev/null 2>&1; then echo "Key exists."else gcloud kms keys create "${KMS_KEY}" \ --keyring="${KMS_KEYRING}" \ --location="${REGION}" \ --purpose=encryption \ --project="${PROJECT_ID}"fi
# Grant yourself encrypt/decrypt on this one key, explicitly, so Pulumi# can use it with your Application Default Credentials. Adding an existing# binding is a no-op.ACCOUNT="$(gcloud config get-value account 2>/dev/null)"if [[ "${ACCOUNT}" == *.gserviceaccount.com ]]; then MEMBER="serviceAccount:${ACCOUNT}"else MEMBER="user:${ACCOUNT}"figcloud kms keys add-iam-policy-binding "${KMS_KEY}" \ --keyring="${KMS_KEYRING}" \ --location="${REGION}" \ --project="${PROJECT_ID}" \ --member="${MEMBER}" \ --role="roles/cloudkms.cryptoKeyEncrypterDecrypter" \ --format=none
# 6. Budget with alerts at 50 %, 90 %, 100 % ---------------------------------log "Budget '${BUDGET_NAME}' (${BUDGET_AMOUNT})"EXISTING_BUDGET="$(gcloud billing budgets list \ --billing-account="${BILLING_ACCOUNT_ID}" \ --billing-project="${PROJECT_ID}" \ --filter="displayName=${BUDGET_NAME}" \ --format='value(name)')"if [[ -n "${EXISTING_BUDGET}" ]]; then echo "Budget exists: ${EXISTING_BUDGET}"else gcloud billing budgets create \ --billing-account="${BILLING_ACCOUNT_ID}" \ --billing-project="${PROJECT_ID}" \ --display-name="${BUDGET_NAME}" \ --budget-amount="${BUDGET_AMOUNT}" \ --filter-projects="projects/${PROJECT_ID}" \ --threshold-rule=percent=0.5 \ --threshold-rule=percent=0.9 \ --threshold-rule=percent=1.0fi
# 7. Next steps ---------------------------------------------------------------SECRETS_PROVIDER="gcpkms://projects/${PROJECT_ID}/locations/${REGION}/keyRings/${KMS_KEYRING}/cryptoKeys/${KMS_KEY}"
log "Bootstrap complete"cat <<EOF
Log Pulumi in to your state bucket:
pulumi login ${STATE_BUCKET}
Secrets provider for every stack you create in this course:
${SECRETS_PROVIDER}
Example (module 1):
pulumi stack init dev --secrets-provider="${SECRETS_PROVIDER}"
EOFЩо робить кожен крок і навіщо:
- Перевірка проєкту і
gcloud config set project. Швидко падає, якщо project ID неправильний, і спрямовує всі наступні командиgcloudна курсовий проєкт. - Прив’язка білінгу. Більшість API відмовляються вмикатися без платіжного акаунта. Скрипт робить прив’язку, лише якщо проєкт ще не прив’язаний до цього акаунта.
- API. Cloud Run, Artifact Registry, Secret Manager, Cloud Build, Cloud KMS, IAM, IAM Credentials (для автентифікації без ключів та ID-токенів), Cloud Resource Manager (через нього Pulumi читає метадані проєкту), а також Cloud Storage, Cloud Billing і
billingbudgets.googleapis.com. Останній потрібен дляgcloud billing budgets create; без нього крок із бюджетом падає з помилкою «service disabled». Увімкнення вже увімкненого API нічого не робить, тож цей крок ідемпотентний сам собою. - Бакет для state. Створюється у вашому регіоні з uniform bucket-level access, тож права доступу визначаються лише через IAM і ніколи через ACL окремих об’єктів, і з public access prevention, щоб ніхто випадково не зробив ваш state публічним. Версіонування увімкнене, бо файл state якраз і є тим, що вам точно не хочеться втратити: якщо запуск, що впав, залишить його зламаним, ви відновите попередню версію.
- Key ring
pulumiі ключstateу KMS. У тому самому регіоні, що й бакет. Key ring-и та ключі в Cloud KMS не можна видалити (знищити можна лише версії ключа), тому скрипт перевіряє, перш ніж створювати. Явна прив’язкаcryptoKeyEncrypterDecrypterна цьому конкретному ключі гарантує, що ваш власний акаунт може шифрувати й розшифровувати ним, незалежно від того, яку базову роль ви маєте. - Бюджет. Бюджет на
BUDGET_AMOUNT(за замовчуванням 5 USD) лише для цього проєкту, з порогами алертів на 50 %, 90 % і 100 %.--billing-projectкажеgcloud, на який проєкт рахувати виклик Budgets API; з користувацькими credentials виклик без quota project падає. - Наступні кроки. Виводить точну команду
pulumi loginі URL secrets provider, які ви використаєте в модулі 1.
Якщо ваш платіжний акаунт у євро, задайте суму в цій валюті. Валюта має збігатися з валютою платіжного акаунта, інакше команда впаде:
export BUDGET_AMOUNT="5EUR"Запустіть:
chmod +x scripts/bootstrap.sh./scripts/bootstrap.shПотім запустіть удруге. Кожен крок має повідомити, що відповідна річ уже існує, і нічого не повинно впасти. Саме ця властивість робить скрипт прийнятним як єдиний CLI-виняток курсу: його можна відрев’юїти в git, відтворити на новому проєкті і безпечно перезапустити.
Залогіньте Pulumi у свій бакет
Section titled “Залогіньте Pulumi у свій бакет”Спрямуйте Pulumi CLI на свій бакет замість Pulumi Cloud:
pulumi login "gs://${PROJECT_ID}-pulumi-state"pulumi whoami -vpulumi whoami -v має показати URL бекенду gs://. Відтепер кожен stack, який ви створите, зберігатиме свій state у цьому бакеті. Логін діє в межах машини; у модулі 6 Cloud Build виконає такий самий pulumi login зі своєю власною ідентичністю.
Збережіть URL secrets provider, який вивів скрипт. Він має такий вигляд:
gcpkms://projects/PROJECT_ID/locations/REGION/keyRings/pulumi/cryptoKeys/stateУ модулі 1 ви створите з ним проєкт Pulumi та його stack dev (pulumi new сам виконує pulumi stack init):
pulumi new csharp --name sniplink --stack dev \ --secrets-provider="gcpkms://projects/${PROJECT_ID}/locations/${REGION}/keyRings/pulumi/cryptoKeys/state"Поки що це не запускайте; модуль 1 зробить це в новій теці infra/. Stack dev є єдиним stack-ом, який Sniplink використовує в цьому курсі: з вашого ноутбука в модулях 1-5 і з Cloud Build у модулі 6. Якщо створити stack без --secrets-provider, Pulumi відкотиться до passphrase і питатиме PULUMI_CONFIG_PASSPHRASE при кожному запуску. Це працює, але це ще один секрет, який треба зберігати й передавати в CI. Саме цього ми й уникаємо.
Запустіть Sniplink локально
Section titled “Запустіть Sniplink локально”Поверніться у свій репозиторій і запустіть тести та API:
dotnet testdotnet run --project src/Sniplink.ApiУ консольному лозі видно, де слухає застосунок. У стартовому репозиторії це http://localhost:5000. Запам’ятайте цю деталь; вона важлива в модулі 1.
У другому терміналі створіть коротке посилання, перейдіть за ним і прочитайте його метадані:
# Create a link; note the slug in the responsecurl -i -X POST http://localhost:5000/api/links \ -H "Content-Type: application/json" \ -d '{ "url": "https://learn.microsoft.com/dotnet/" }'
# Replace k3x9q2 with your slug; expect 302 and a Location headercurl -i http://localhost:5000/k3x9q2
# Metadata for the same slugcurl -s http://localhost:5000/api/links/k3x9q2
# Unknown slug: expect 404curl -i http://localhost:5000/does-not-existЗупиніть застосунок через Ctrl+C і запустіть знову: ваших посилань більше немає. Sniplink навмисно зберігає посилання в пам’яті. Персистентність є окремою темою, і їй місце в наступному курсі; тут це робить сервіс stateless, а саме цього Cloud Run і хоче.
Скільки це коштує
Section titled “Скільки це коштує”Ціни змінюються, тож сприймайте цифри як порядок величин і перевіряйте сторінки з цінами за посиланнями нижче. На момент написання:
- Free tier покриває більшу частину. Cloud Run має щомісячну безкоштовну квоту запитів, vCPU-секунд і пам’яті, яку курсовий сервіс, що масштабується до нуля, ніколи не перевищить. Secret Manager містить кілька безкоштовних активних версій секретів і операцій доступу на місяць. Cloud Build має щомісячну квоту безкоштовних хвилин збірки.
- Коштує центи. Версія ключа KMS тарифікується за кожну активну версію на місяць (близько 0,06 USD) плюс крихітна сума за кожні 10 000 операцій. Сховище Artifact Registry понад безкоштовні 0,5 GB тарифікується за GB-місяць, а .NET-образи накопичуються від модуля до модуля, тож видаляйте старі. Бакет для state у
europe-west1містить кілька кілобайт і не коштує фактично нічого. - Що може коштувати реальних грошей. Інстанси, які ніколи не масштабуються до нуля (
MinInstanceCountбільше 0 або CPU, виділений постійно), навантажувальний тест, який забули зупинити, на сервісі без обмежень, або ресурси, які ви створили вручну і забули. Правила курсу закривають усі три випадки.
Як зупинити все:
# Inside infra/, from module 1 on: removes every resource the stack ownspulumi destroy
# The nuclear option: removes the whole project and everything in itgcloud projects delete "$PROJECT_ID"pulumi destroy означає звичайне прибирання після лаби, і кожен модуль каже, коли його запускати. Видалення проєкту призначене для кінця курсу або для випадку, коли ви не впевнені, що саме працює. Видалений проєкт можна відновити протягом приблизно 30 днів, після чого він зникає назавжди. Білінг зупиняється, щойно ви його видалите, включно з версією ключа KMS.
Перед модулем 1 ви маєте вміти
Section titled “Перед модулем 1 ви маєте вміти”- Запустити
dotnet --version,docker version,gcloud versionіpulumi versionбез помилок, з .NET версії 10.0.x. - Назвати ID свого курсового проєкту, і
gcloud config get-value projectвиводить саме його. - Запустити
./scripts/bootstrap.shдвічі поспіль, причому другий запуск нічого нового не створює. - Побачити свій бюджет із трьома порогами:
gcloud billing budgets list --billing-account="$BILLING_ACCOUNT_ID". - Запустити
pulumi whoami -vі побачити свій бекендgs://PROJECT_ID-pulumi-state. - Записати URL secrets provider
gcpkms://для свого проєкту і регіону. - Створити посилання через
curlу локальному Sniplink і отримати302, перейшовши за ним. - Пояснити одним реченням, чому bootstrap-скрипт є прийнятним винятком із правила нуль ClickOps.