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

Ви увійшли як

Вхід у Curious Dev Learn

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

Модуль 0045 хв

Налаштуйте робоче середовище

Створіть окремий проєкт Google Cloud для курсу, встановіть інструменти, запустіть bootstrap-скрипт і підніміть Sniplink локально.

Цей урок проведе вас від нуля до проєкту Google Cloud, яким може керувати Pulumi, плюс Sniplink, що працює на вашому ноутбуці. Це єдиний урок, де ви налаштовуєте інфраструктуру напряму через gcloud, і наприкінці ви побачите, чому це все одно не ClickOps: це один скрипт у git, який можна прочитати і запустити двічі.

Вам потрібні два акаунти.

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:

Terminal window
export PROJECT_ID="sniplink-course-4821" # your own, globally unique
export REGION="europe-west1"

Сам проєкт ви створите в розділі про інструменти нижче, одразу після встановлення gcloud.

Встановіть ось це. Версії мають менше значення, ніж можна побоюватися, але використовуйте .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) необов’язковий, але заощадить кілька кроків нижче.

Запустіть перевірки разом:

Terminal window
dotnet --version
docker version --format '{{.Server.Version}}'
gcloud version
pulumi version
git --version

Якщо docker version виводить версію клієнта, але падає на частині server, Docker daemon не запущений. Запустіть Docker Desktop або виконайте sudo systemctl start docker на Linux.

Тепер залогіньте gcloud. Тут два окремі логіни, і потрібні обидва:

Terminal window
# Credentials for gcloud commands themselves
gcloud auth login
# Application Default Credentials: what Pulumi, the KMS secrets provider
# and the Google client libraries use
gcloud auth application-default login

Перший для вас, коли ви набираєте gcloud .... Другий записує файл з credentials, який підхоплюють програми. Pulumi не використовує ваш логін gcloud; він використовує Application Default Credentials. Забута друга команда є найчастішою причиною того, що pulumi login gs://... падає з помилкою доступу.

Створіть проєкт і знайдіть ID свого платіжного акаунта:

Terminal window
gcloud projects create "$PROJECT_ID" --name="Sniplink course"
# Lists billing accounts you can use; copy the ACCOUNT_ID column
gcloud billing accounts list
export BILLING_ACCOUNT_ID="XXXXXX-XXXXXX-XXXXXX"

Створіть свій репозиторій

Section titled “Створіть свій репозиторій”

Стартовий код лежить у теці starter/ репозиторію курсу. Завантажте його як zip і розпакуйте в нову теку:

Terminal window
curl -fLO https://learn.vladtimchenko.dev/downloads/containerize-deploy/sniplink-starter.zip
unzip sniplink-starter.zip -d sniplink
cd sniplink

Або скопіюйте теку просто з GitHub через degit: він завантажує файли без історії репозиторію курсу (потрібен Node.js):

Terminal window
npx degit curious-dev-learn/containerize-deploy-dotnet-on-gcp/starter sniplink
cd sniplink

В обох випадках ви отримуєте звичайні файли, а не клон. Зробіть із них власний репозиторій з першим комітом і опублікуйте його на GitHub. Через gh:

Terminal window
git init -b main
git 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.

Pulumi може створити у вашому проєкті майже все, але перед першим запуском йому потрібні три речі:

  1. Бекенд для state: місце, де записано, які реальні ресурси належать вашій програмі. Ми використовуємо бакет Cloud Storage gs://PROJECT_ID-pulumi-state.
  2. Secrets provider: ключ для шифрування секретних значень конфігурації (один такий ви збережете в модулі 3). Ми використовуємо ключ Cloud KMS pulumi/state, тож жодна passphrase не живе ні на вашому ноутбуці, ні в CI.
  3. Проєкт із прив’язаним білінгом і увімкненими API, бо Pulumi викликає ці API.

На додачу я додаю бюджет з алертами, бо курс, у якому ви створюєте хмарні ресурси, має попереджати вас раніше, ніж здивує.

Ось повний скрипт. Прочитайте його, перш ніж запускати; він навмисно короткий.

scripts/bootstrap.sh
#!/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 1
fi
gcloud 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-prevention
fi
# 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}"
fi
gcloud 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.0
fi
# 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.

Якщо ваш платіжний акаунт у євро, задайте суму в цій валюті. Валюта має збігатися з валютою платіжного акаунта, інакше команда впаде:

Terminal window
export BUDGET_AMOUNT="5EUR"

Запустіть:

Terminal window
chmod +x scripts/bootstrap.sh
./scripts/bootstrap.sh

Потім запустіть удруге. Кожен крок має повідомити, що відповідна річ уже існує, і нічого не повинно впасти. Саме ця властивість робить скрипт прийнятним як єдиний CLI-виняток курсу: його можна відрев’юїти в git, відтворити на новому проєкті і безпечно перезапустити.

Залогіньте Pulumi у свій бакет

Section titled “Залогіньте Pulumi у свій бакет”

Спрямуйте Pulumi CLI на свій бакет замість Pulumi Cloud:

Terminal window
pulumi login "gs://${PROJECT_ID}-pulumi-state"
pulumi whoami -v

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

Terminal window
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. Саме цього ми й уникаємо.

Section titled “Запустіть Sniplink локально”

Поверніться у свій репозиторій і запустіть тести та API:

Terminal window
dotnet test
dotnet run --project src/Sniplink.Api

У консольному лозі видно, де слухає застосунок. У стартовому репозиторії це http://localhost:5000. Запам’ятайте цю деталь; вона важлива в модулі 1.

У другому терміналі створіть коротке посилання, перейдіть за ним і прочитайте його метадані:

Terminal window
# Create a link; note the slug in the response
curl -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 header
curl -i http://localhost:5000/k3x9q2
# Metadata for the same slug
curl -s http://localhost:5000/api/links/k3x9q2
# Unknown slug: expect 404
curl -i http://localhost:5000/does-not-exist

Зупиніть застосунок через Ctrl+C і запустіть знову: ваших посилань більше немає. Sniplink навмисно зберігає посилання в пам’яті. Персистентність є окремою темою, і їй місце в наступному курсі; тут це робить сервіс stateless, а саме цього Cloud Run і хоче.

Ціни змінюються, тож сприймайте цифри як порядок величин і перевіряйте сторінки з цінами за посиланнями нижче. На момент написання:

  • 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, виділений постійно), навантажувальний тест, який забули зупинити, на сервісі без обмежень, або ресурси, які ви створили вручну і забули. Правила курсу закривають усі три випадки.

Як зупинити все:

Terminal window
# Inside infra/, from module 1 on: removes every resource the stack owns
pulumi destroy
# The nuclear option: removes the whole project and everything in it
gcloud 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.