Packaging болон Shipping Code

Кодоо өөрийн machine дээр ажиллуулах нэг хэрэг. Харин өөр хүний machine, server, эсвэл production орчин дээр найдвартай ажиллуулах бол бүр өөр асуудал.

Code shipping гэдэг нь бичсэн кодоо өөр хүн таны laptop-ийн тохиргоог яг дуурайхгүйгээр ажиллуулж чадах, ашиглахад бэлэн хэлбэрт оруулахыг хэлнэ. Энэ нь хэл, системийн library, OS, deployment target гээд олон зүйлээс хамаарна. Library, CLI tool, web service гурав тус бүр өөр шаардлагатай. Гэхдээ гол асуулт нь адилхан: бэлэн болох artifact маань яг юу вэ, түүнийг ажиллуулахын тулд орчноос юу шаардах вэ?

Энэ лекцээр дараах сэдвүүдийг үзнэ:

Жишээнүүдийг Python экосистем дээр тайлбарлана. Бусад хэл дээр tool-уудын нэр, формат өөр байж болох ч үндсэн concept нь ихэнхдээ адилхан.

Хамаарлууд ба орчин

Орчин үеийн програм хангамжийн хөгжүүлэлтэд хийсвэрлэлийн түвшнүүд хаа сайгүй байдаг. Програмууд өөрсдийн логикийг бусад сан эсвэл үйлчилгээнүүдэд даатгах нь түгээмэл. Гэхдээ энэ нь таны програм болон түүнийг ажиллахад шаардлагатай сангуудын хооронд хамаарал (dependency) үүсгэдэг. Жишээлбэл, Python дээр вэбсайтын агуулгыг татаж авахын тулд бид ихэвчлэн дараах кодыг бичдэг:

import requests

response = requests.get("https://missing.csail.mit.edu")

Гэвч requests сан нь Python ажиллах орчинтой цуг ирдэггүй тул хэрэв бид requests-ийг суулгахгүйгээр энэ кодыг ажиллуулахыг оролдвол Python алдаа заах болно:

$ python fetch.py
Traceback (most recent call last):
  File "fetch.py", line 1, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

Энэ library-г ашиглахын тулд эхлээд pip install requests ажиллуулж суулгах хэрэгтэй. pip нь Python програмчлалын хэлнээс багцуудыг суулгахад зориулан гаргасан CLI хэрэгсэл юм. pip install requests ажиллуулахад цаана нь ерөнхийдөө ийм алхмууд хийгдэнэ:

  1. Python-ийн багцын сангаас (PyPI) requests-ийг хайна.
  2. Бидний ажиллаж буй платформд тохирох артефактыг хайна.
  3. Хамаарлуудыг шийдвэрлэнэ — requests сан нь өөрөө бусад багцуудаас хамаардаг тул суулгагч нь тэдгээр дамжих хамаарлуудын тохирох хувилбаруудыг олж, эхэлж суулгах ёстой.
  4. Артефактуудыг татаж аваад, дараа нь тэдгээрийг задалж, манай файлын системийн зөв байршлууд руу хуулна.
$ pip install requests
Collecting requests
  Downloading requests-2.32.3-py3-none-any.whl (64 kB)
Collecting charset-normalizer<4,>=2
  Downloading charset_normalizer-3.4.0-cp311-cp311-manylinux_x86_64.whl (142 kB)
Collecting idna<4,>=2.5
  Downloading idna-3.10-py3-none-any.whl (70 kB)
Collecting urllib3<3,>=1.21.1
  Downloading urllib3-2.2.3-py3-none-any.whl (126 kB)
Collecting certifi>=2017.4.17
  Downloading certifi-2024.8.30-py3-none-any.whl (167 kB)
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests
Successfully installed certifi-2024.8.30 charset-normalizer-3.4.0 idna-3.10 requests-2.32.3 urllib3-2.2.3

Үүнээс бид requests сан нь certifi болон charset-normalizer гэх мэт өөрийн гэсэн хамаарлуудтай бөгөөд requests-ийг суулгахаасоо өмнө тэдгээр нь суух шаардлагатайг харж болно. Суулгасны дараа Python runtime import хийх үед тэр library-г олж чадна.

$ python -c 'import requests; print(requests.__path__)'
['/usr/local/lib/python3.11/dist-packages/requests']

$ pip list | grep requests
requests        2.32.3

Програмчлалын хэлнүүд нь сангуудыг суулгах, нийтлэх тал дээр өөр өөр хэрэгсэл, стандарт болон арга барилуудыг ашигладаг. Rust гэх мэт зарим хэлэнд хэрэгслүүд нь нэгдсэн байдаг — cargo нь угсрах, тест хийх, хамаарлыг удирдах, болон нийтлэх үйлдлийг бүгдийг хариуцдаг. Харин Python зэрэг зарим хэлэнд нэгтгэл нь стандартын түвшинд хийгддэг — ганц хэрэгсэл ашиглахын оронд багцлах үйл явцыг тодорхойлсон стандартууд байх бөгөөд үүний ачаар даалгавар тус бүрд өрсөлдөхүйц олон хэрэгслийг ашиглах боломж бүрддэг (pip-ийн эсрэг uv, setuptools-ийн эсрэг hatch эсвэл poetry). Мөн LaTeX зэрэг зарим экосистемд TeX Live эсвэл MacTeX зэрэг багцууд нь олон мянган бэлэн суулгасан багцтайгаа цуг ирдэг.

Хамаарлуудыг нэмэх нь хамаарын зөрчилдөөнүүдийг дагуулж байдаг. Зөрчилдөөн нь програмууд ижил хамаарын хоорондоо үл нийцэх хувилбаруудыг шаардах үед үүсдэг. Жишээлбэл, хэрэв tensorflow==2.3.0 нь numpy>=1.16.0,<1.19.0-ийг шаардаж, pandas==1.2.0 нь numpy>=1.16.5-ийг шаардвал numpy>=1.16.5,<1.19.0 нөхцөлийг хангах ямар ч хувилбар тохирно. Гэвч хэрэв таны төслийн өөр нэг багц numpy>=1.19-ийг шаардвал бүх хязгаарлалтыг зэрэг хангах боломжит хувилбар олдохгүй зөрчилдөөн үүснэ.

Олон багцууд дундын хамаарлуудынхаа хоорондоо үл нийцэх хувилбаруудыг шаарддаг энэ нөхцөл байдлыг ихэвчлэн dependency hell (хамаарын там) гэж нэрлэдэг. Зөрчилдөөнийг шийдэх нэг арга бол програм бүрийн хамаарлыг өөрийнх нь тусдаа орчинд (environment) тусгаарлах юм.

Python дээр бид ингэж виртуал орчин (virtual environment) үүсгэдэг:

$ which python
/usr/bin/python
$ pwd
/home/missingsemester
$ python -m venv venv
$ source venv/bin/activate
$ which python
/home/missingsemester/venv/bin/python
$ which pip
/home/missingsemester/venv/bin/pip
$ python -c 'import requests; print(requests.__path__)'
['/home/missingsemester/venv/lib/python3.11/site-packages/requests']

$ pip list
Package Version
------- -------
pip     24.0

Орчин гэдгийг тухайн хэлний ажиллах орчны бие даасан хувилбар бөгөөд дотроо өөрийн гэсэн суулгасан багцуудтай гэж ойлгож болно. Энэ виртуал орчин буюу venv нь суулгасан хамаарлуудыг системийн үндсэн Python суулгацаас тусгаарладаг. Төсөл бүрд өөрийн гэсэн шаардлагатай хамаарлуудыг агуулсан виртуал орчин үүсгэж хэвших нь зөв дадал юм.

Орчин үеийн олон үйлдлийн системүүд Python зэрэг програмчлалын хэлний ажиллах орцтой цуг ирдэг ч тэдгээрийг өөрчлөх нь эрсдэлтэй. Учир нь үйлдлийн систем өөрийн ажиллах явцад тэдгээр дээр тулгуурладаг байж болзошгүй тул үргэлж тусдаа орчин үүсгэж ашиглахыг илүүд үзээрэй.

Зарим хэлэнд суулгах үйл явц нь тодорхой хэрэгслээр бус, харин стандартаар тодорхойлогддог. Python-д PEP 517 нь угсралтын системийн интерфейсийг тодорхойлж, PEP 621 нь төслийн мета-өгөгдлийг pyproject.toml файлд хэрхэн хадгалахыг зааж өгдөг. Энэ нь хөгжүүлэгчдэд pip-ийг улам сайжруулж, илүү хурдан ажилладаг uv гэх мэт хэрэгслүүдийг бүтээх боломжийг олгосон. uv-ийг суулгахын тулд pip install uv гэж ажиллуулахад л хангалттай.

pip-ийн оронд uv ашиглах нь ижил интерфейстэй боловч хамаагүй хурдан байдаг:

$ uv pip install requests
Resolved 5 packages in 12ms
Prepared 5 packages in 0.45ms
Installed 5 packages in 8ms
 + certifi==2024.8.30
 + charset-normalizer==3.4.0
 + idna==3.10
 + requests==2.32.3
 + urllib3==2.2.3

Боломжтой бол pip-ийн оронд uv pip ашиглаад үзээрэй. Суулгах хугацааг мэдэгдэхүйц хэмнэдэг.

Орчин нь хамаарлыг тусгаарлахаас гадна програмчлалын хэлнийхээ ажиллах орчны өөр өөр хувилбаруудыг зэрэг ашиглах боломжийг олгодог.

$ uv venv --python 3.12 venv312
Using CPython 3.12.7
Creating virtual environment at: venv312

$ source venv312/bin/activate && python --version
Python 3.12.7

$ uv venv --python 3.11 venv311
Using CPython 3.11.10
Creating virtual environment at: venv311

$ source venv311/bin/activate && python --version
Python 3.11.10

Энэ нь кодоо Python-ийн олон хувилбар дээр шалгаж үзэх эсвэл төсөл нь тодорхой нэг хувилбарыг шаардаж байгаа үед маш хэрэгтэй байдаг.

Зарим програмчлалын хэлэнд та виртуал орчныг гараар үүсгэх шаардлагагүйгээр төсөл бүр өөрийн гэсэн хамаарлын орчныг автоматаар үүсгэж авдаг ч үндсэн зарчим нь ижил юм. Орчин үеийн ихэнх хэлнүүд нэг систем дээр хэлнийхээ олон хувилбарыг удирдаж, төсөл тус бүрд аль хувилбарыг ашиглахаа зааж өгөх боломжтой механизмыг агуулдаг болсон.

Артефактууд ба багцлалт

Програм хангамжийн хөгжүүлэлтэд бид эх код (source code) болон артефакт (artifact) хоёрыг зааглаж авч үздэг. Хөгжүүлэгчид эх кодыг бичиж, уншдаг бол артефакт нь тухайн эх кодоос гарган авсан, суулгах болон байршуулахад бэлэн болсон, багцалсан бүтээгдэхүүн юм. Артефакт нь бидний ажиллуулдаг энгийн нэг кодын файл байж болохоос гадна апп ажиллахад шаардлагатай бүх зүйлийг багтаасан бүхэл бүтэн виртуал машин ч байж болно. Жишээ болгож current directory дотор байгаа greet.py Python файлыг авч үзье:

$ cat greet.py
def greet(name):
    return f"Hello, {name}!"

$ python -c "from greet import greet; print(greet('World'))"
Hello, World!

$ cd /tmp
$ python -c "from greet import greet; print(greet('World'))"
ModuleNotFoundError: No module named 'greet'

Өөр directory рүү шилжихэд import амжилтгүй болж байгаа нь Python модулиудыг зөвхөн тодорхой газраас хайдагтай холбоотой: current directory, суулгасан package-ууд, мөн PYTHONPATH доторх path-ууд. Packaging нь кодыг Python-ийн таньдаг зөв байршилд суулгаж өгснөөр энэ асуудлыг шийддэг.

Python-ийн хувьд санг багцлах гэдэг нь pip эсвэл uv зэрэг багц суулгагчид ашиглаж болохуйц артефактыг бэлтгэх үйл явц юм. Python-ийн артефактуудыг wheel гэж нэрлэдэг бөгөөд энэ нь багцыг суулгахад шаардлагатай бүх мэдээллийг агуулдаг: кодын файлууд, багцын тухай мета-өгөгдөл (нэр, хувилбар, хамаарлууд), болон файлыг хаана байршуулах тухай зааварчилгаа. Артефактыг угсрахад төслийн дэлгэрэнгүй мэдээлэл, шаардлагатай хамаарлууд, багцын хувилбар болон бусад мэдээллийг агуулсан төслийн файл (манифест гэж нэрлэгддэг) бичих шаардлагатай болдог. Python-д бид үүнд зориулж pyproject.toml файлыг ашигладаг.

pyproject.toml нь орчин үеийн бөгөөд зөвлөдөг арга зам юм. Өмнө нь ашиглагдаж байсан requirements.txt болон setup.py зэрэг аргуудыг дэмжсээр байгаа ч боломжтой бол pyproject.toml-ийг сонгох хэрэгтэй.

CLI tool гаргаж өгдөг library-ийн хамгийн энгийн pyproject.toml жишээ ийм:

[project]
name = "greeting"
version = "0.1.0"
description = "A simple greeting library"
dependencies = ["typer>=0.9"]

[project.scripts]
greet = "greeting:cli"

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

typer library нь маш бага нэмэлт кодоор CLI интерфейс үүсгэхэд өргөн хэрэглэгддэг Python package юм.

Мөн үүнд тохирох greeting.py файл:

import typer


def greet(name: str) -> str:
    return f"Hello, {name}!"


def cli():
    typer.run(greet)


if __name__ == "__main__":
    cli()

Одоо бид wheel-ийг угсарч болно:

$ uv build
Building source distribution...
Building wheel from source distribution...
Successfully built dist/greeting-0.1.0.tar.gz
Successfully built dist/greeting-0.1.0-py3-none-any.whl

$ ls dist/
greeting-0.1.0-py3-none-any.whl
greeting-0.1.0.tar.gz

Уг .whl файл нь wheel өөрөө (тодорхой бүтэцтэй zip архив) бөгөөд .tar.gz нь эх кодноос нь угсрах шаардлагатай системүүдэд зориулсан эх кодын тархац (source distribution) юм.

Багцад юу орсныг харахын тулд wheel-ийн агуулгыг шалгаж болно:

$ unzip -l dist/greeting-0.1.0-py3-none-any.whl
Archive:  dist/greeting-0.1.0-py3-none-any.whl
  Length      Date    Time    Name
---------  ---------- -----   ----
      150  2024-01-15 10:30   greeting.py
      312  2024-01-15 10:30   greeting-0.1.0.dist-info/METADATA
       92  2024-01-15 10:30   greeting-0.1.0.dist-info/WHEEL
        9  2024-01-15 10:30   greeting-0.1.0.dist-info/top_level.txt
      435  2024-01-15 10:30   greeting-0.1.0.dist-info/RECORD
---------                     -------
      998                     5 files

Хэрэв бид энэ wheel-ийг өөр хүнд өгвөл тэд дараах command-аар суулгах боломжтой:

$ uv pip install ./greeting-0.1.0-py3-none-any.whl
$ greet Alice
Hello, Alice!

Энэ нь бидний өмнө угсарсан санг, түүний доторх greet CLI хэрэгслийн хамт тэдний орчинд суулгах болно.

Энэ аргад тодорхой хязгаарлалтууд бий. Жишээ нь, манай сан GPU хурдасгуурт зориулсан CUDA гэх мэт тухайн платформд зориулсан сангуудаас хамаардаг бол манай артефакт зөвхөн тэдгээр тусгай сангууд суусан систем дээр л ажиллах бөгөөд бид өөр өөр үйлдлийн систем болон архитектуруудад зориулж тусдаа wheel-үүдийг угсрах шаардлагатай болно.

Програм хангамж суулгах үед эх кодноос суулгах (installing from source) болон бэлэн хоёртын файлыг суулгах (installing a prebuilt binary) хоёрын хооронд маш том ялгаа байдаг. Эх кодноос суулгах гэдэг нь эх кодыг татаж аваад өөрийн компьютер дээр хөрвүүлэхийг хэлнэ — үүнд хөрвүүлэгч (compiler) болон угсрах хэрэгслүүд шаардлагатай бөгөөд том төслүүдийн хувьд маш их хугацаа шаардаж болно.

Харин бэлэн хоёртын файлыг суулгах нь өөр хэн нэгний өмнө нь хөрвүүлсэн артефактыг татаж авна гэсэн үг юм — илүү хурдан бөгөөд хялбар боловч уг файл нь таны үйлдлийн систем болон архитектурт яг таарч байх ёстой. Жишээлбэл, ripgrep-ийн релиз хуудас дээр Linux, macOS, болон Windows-д зориулсан бэлэн хоёртын файлуудыг (prebuilt binaries) харуулсан байдаг.

Релизүүд ба хувилбаржуулалт

Код бичих үйл явц нь тасралтгүй үргэлжилдэг боловч тодорхой хугацааны давтамжтайгаар хувилбар болон гардаг. Програм хангамжийн хөгжүүлэлтэд хөгжүүлэлтийн орчин (dev environment) болон бодит орчны (production environment) хооронд маш тодорхой зааг бий. Кодыг production орчин руу гаргахаас өмнө хөгжүүлэлтийн орчинд зөв ажиллаж байгааг нь нотлох шаардлагатай. Релиз хийх үйл явц нь тест хийх, хамаарлыг удирдах, хувилбаржуулах, тохиргоо хийх, байршуулах, болон нийтлэх зэрэг олон алхмуудыг багтаадаг.

Програм хангамжийн сангууд нь тогтмол биш, цаг хугацааны явцад алдаа засагдаж, шинэ боломжууд нэмэгдэн хувьсаж байдаг. Бид энэ хувьслыг тухайн сангийн тодорхой цаг үеийн төлөвт харгалзах хувилбарын дугаараар тэмдэглэж хянадаг. Сангийн өөрчлөлт нь жижиг алдаа зассан засварууд (patches), түүний боломжуудыг нэмэгдүүлсэн шинэ функцууд (features), эсвэл өмнөх хувилбартай нийцэхгүй болгох томоохон өөрчлөлтүүд (breaking changes) ч байж болно. Өөрчлөлтийн бүртгэл (changelog) нь тухайн хувилбарт орсон өөрчлөлтүүдийг баримтжуулдаг — эдгээр нь хөгжүүлэгчид шинэ релизээр ямар өөрчлөлт орж байгааг мэдээлэхэд ашигладаг баримт бичиг юм.

Гэвч хамаарал бүрийн өөрчлөлтийг нэг бүрчлэн хянах нь боломжгүй бөгөөд тэдгээрийн дамжих хамаарлуудыг (transitive dependencies) тооцвол бүр ч хэцүү болно.

Та өөрийн төслийн хамаарлуудын модыг uv tree command-аар харж болно бөгөөд энэ нь бүх багцууд болон тэдгээрийн дамжих хамаарлуудыг мод хэлбэрээр харуулдаг.

Энэ асуудлыг хялбарчлахын тулд програм хангамжийг хувилбаржуулах дүрэм журмууд байдаг бөгөөд тэдгээрээс хамгийн өргөн хэрэглэгддэг нь Семантик хувилбаржуулалт буюу SemVer юм. SemVer-ийн дагуу хувилбарын дугаар нь MAJOR.MINOR.PATCH (ГОЛ.ТУСЛАХ.ЗАСВАР) гэсэн хэлбэртэй байна. Товчхондоо:

Энэ бол маш хялбаршуулсан тайлбар бөгөөд жишээ нь яагаад 0.1.3-аас 0.2.0 руу шилжихэд нийцэл эвдэрч болох эсвэл 1.0.0-rc.1 гэж юу гэсэн үг болохыг илүү сайн ойлгохын тулд SemVer-ийн бүрэн баримт бичгийг уншаад үзээрэй.

Python-ийн багцлалт нь семантик хувилбаржуулалтыг шууд дэмждэг тул бид хамаарлын хувилбаруудаа зааж өгөхдөө янз бүрийн нөхцөлүүдийг ашиглаж болно. pyproject.toml файл дотор бид хамаарлынхаа нийцтэй хувилбаруудын хүрээг дараах хэлбэрүүдээр хязгаарлах боломжтой:

[project]
dependencies = [
    "requests==2.32.3",  # Exact version - only this specific version
    "click>=8.0",        # Minimum version - 8.0 or newer
    "numpy>=1.24,<2.0",  # Range - at least 1.24 but less than 2.0
    "pandas~=2.1.0",     # Compatible release - >=2.1.0 and <2.2.0
]

Хувилбар заагчууд нь олон багц менежерүүдэд (npm, cargo г.м.) өөр өөр нарийн утгатайгаар ашиглагддаг. Python-ийн хувьзаар ~= оператор нь “нийцтэй хувилбар” гэдгийг заадаг — ~=2.1.0 гэдэг нь “2.1.0-той нийцтэй ямар ч хувилбар” буюу >=2.1.0 ба <2.2.0 гэсэн үг юм. Энэ нь npm болон cargo дээрх SemVer-ийн нийцтэй байдлыг заадаг карет (^) оператортой бараг ижил утгатай.

Бүх програм хангамж семантик хувилбаржуулалтыг ашигладаггүй. Өргөн хэрэглэгддэг өөр нэг сонголт бол Календарь хувилбаржуулалт (CalVer) бөгөөд хувилбарын дугаар нь семантик утгаас илүүтэйгээр гаргасан огноон дээр тулгуурладаг. Жишээлбэл, Ubuntu нь 24.04 (2024 оны 4-р сар) болон 24.10 (2024 оны 10-р сар) гэсэн хувилбаруудыг ашигладаг. CalVer нь хувилбар хэр хуучин болохыг харахад хялбар болгодог ч нийцтэй байдлын талаар мэдээлэл өгдөггүй. Эцэст нь хэлэхэд, семантик хувилбаржуулалт нь алдаагүй тул хааяа багц хөгжүүлэгчид андуураад жижиг эсвэл засварын релиз дээрээ нийцэл эвдэх өөрчлөлт оруулах тохиолдол гардаг.

Дахин давтагдах байдал

Орчин үеийн програм хангамжийн хөгжүүлэлтэд таны бичсэн код маш олон хийсвэрлэлийн түвшний дээр суурилж ажилладаг. Үүнд програмчлалын хэлний ажиллах орчин, гуравдагч талын сангууд, үйлдлийн систем, эсвэл бүр техник хангамж өөрөө ч багтана. Эдгээр түвшнүүдийн аль нэгэнд гарсан өчүүхэн ялгаа нь таны кодын ажиллагааг өөрчлөх эсвэл ажиллахгүй болоход хүргэж болно. Түүнчлэн техник хангамжийн ялгаа хүртэл код shipping хийхэд нөлөөлдөг.

Санг “хадах” (pinning) гэдэг нь хувилбарын муж заахын оронд яг таг хувилбарыг зааж өгөхийг хэлнэ (жишээ нь, requests>=2.0-ийн оронд requests==2.32.3).

Багц менежерийн ажлын нэг хэсэг нь хамаарал болон дамжих хамаарлуудаас заасан бүх хязгаарлалтыг тооцоолж, тэдгээрийг бүрэн хангах хувилбаруудын жагсаалтыг гаргах явдал юм. Уг хувилбаруудын жагсаалтыг дараа нь дахин яг ижилхэн суулгах боломжийг бүрдүүлэх үүднээс файлд хадгалдаг бөгөөд үүнийг лок файл (lock file) гэж нэрлэдэг.

$ uv lock
Resolved 12 packages in 45ms

$ cat uv.lock | head -20
version = 1
requires-python = ">=3.11"

[[package]]
name = "certifi"
version = "2024.8.30"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/...", hash = "sha256:..." }
wheels = [
    { url = "https://files.pythonhosted.org/...", hash = "sha256:..." },
]
...

Хамаарлын хувилбаржуулалт болон дахин давтагдах байдлыг (reproducibility) хангах үед анхаарах нэг чухал зүйл бол сан болон апп/үйлчилгээнүүдийн ялгаа юм. Сан (library) нь өөр бусад кодонд импортлогдож ашиглагдах зориулалттай бөгөөд тэдгээр нь өөрийн гэсэн хамаарлуудтай байж болдог тул санд хувилбарын хязгаарлалтыг хэт хатуу зааж өгөх нь хэрэглэгчийн бусад хамааралтай зөрчилдөх шалтгаан болдог. Харин апп болон үйлчилгээнүүд нь програм хангамжийн эцсийн хэрэглэгчид бөгөөд өөрсдийн ажиллагааг програмчлалын интерфейсээр бус, хэрэглэгчийн интерфейс (UI) эсвэл API-аар дамжуулан ил гаргадаг. Тиймээс сангуудад хувилбарын мужийг зааж өгөх нь экосистемд нийцтэй ажиллах боломжийг нэмэгдүүлдэг бол аппуудын хувьд хувилбарыг яг таг тогтоож өгөх нь дахин давтагдах байдлыг баталгаажуулдаг — өөрөөр хэлбэл аппыг ажиллуулж буй хүн бүр яг ижил хамаарлуудыг ашиглана.

Хамгийн дээд зэргийн дахин давтагдах байдлыг шаарддаг төслүүдийн хувьд Nix болон Bazel зэрэг хэрэгслүүд нь hermetic builds (битүү угсралт) олгодог — үүнд хөрвүүлэгч, системийн сангууд, тэр байтугай build орчин хүртэл тогтоогдсон байдаг. Энэ нь хэзээ, хаана угсарч байгаагаас үл хамааран байт бүрээрээ яг ижил үр дүнг гаргахыг баталгаажуулдаг.

Та NixOS-ийг ашиглан компьютерынхоо бүх тохиргоог удирдаж, хувилбарын хяналтанд байгаа тохиргооны файлуудаар дамжуулан компьютерынхоо шинэ хуулбарыг хялбархан үүсгэн ажиллуулах боломжтой.

Програм хангамжийн хөгжүүлэлтийн мөнхийн зөрчил бол шинэ хувилбарууд нь санаатай болон санамсаргүй байдлаар кодын ажиллагааг эвддэг бол хуучин хувилбарууд нь цаг хугацааны явцад аюулгүй байдлын цоорхойтой болдог явдал юм. Үүнийг бид тасралтгүй интеграцийн сувгуудыг ашиглан манай аппыг шинэ хувилбарууд дээр автоматаар шалгах (энэ талаар Кодын чанар ба CI лекцээс дэлгэрэнгүй үзнэ үү) болон Dependabot зэрэг хэрэгслүүдээр хамаарлуудын шинэ хувилбар гарах үед автоматаар илрүүлэх замаар шийдэж болно.

CI тестүүд хийгддэг байсан ч хувилбар ахиулах үед хөгжүүлэлтийн болон бодит орчны ялгаанаас болоод асуудал үүссээр байдаг. Ийм тохиолдолд хамгийн шилдэг шийдэл бол хувилбарын шинэчлэлийг буцааж, өмнөх тогтвортой хувилбарыг дахин байршуулах rollback plan (буцаах төлөвлөгөөтэй) байх явдал юм.

Виртуал машин ба контейнер

Хэрэв та илүү нарийн төвөгтэй хамаарлуудыг ашиглаж эхэлбэл кодын тань хамаарал багц менежерийн зохицуулж чадах хэмжээнээс давах магадлалтай. Үүний нэг нийтлэг шалтгаан нь системийн тусгай сангууд эсвэл техник хангамжийн драйверуудтай ажиллах шаардлага гардагтай холбоотой. Жишээлбэл, шинжлэх ухааны тооцоолол болон хиймэл оюун ухааны салбарт програмууд GPU техник хангамжийг ашиглахын тулд тусгай сан болон драйверуудыг шаарддаг. Системийн түвшний олон хамаарлууд (GPU драйверууд, хөрвүүлэгчийн тодорхой хувилбарууд, OpenSSL зэрэг дундын сангууд) нь систем даяар суулгалт хийхийг шаарддаг хэвээр байна.

Уламжлал ёсоор ийм өргөн хүрээний dependency-ийн асуудлыг виртуал машинуудаар (VM) шийддэг байсан. Виртуал машин нь компьютерийг бүхэлд нь хийсвэрлэж, өөрийн гэсэн үйлдлийн систем бүрэн тусгаарлагдсан орчныг бүрдүүлдэг. Харин орчин үеийн арга бол аппыг өөрийн хамаарал, сангууд, болон файлын системтэй хамт багцалж, компьютерийг бүхэлд нь виртуалчлахын оронд үндсэн (хост) үйлдлийн системийн цөмийг хамтран ашигладаг контейнер (container) юм. Контейнерууд нь үйлдлийн системийн цөмийг хуваалцдаг тул виртуал машинаас хамаагүй хөнгөн бөгөөд ажиллаж эхлэх хугацаа нь хурдан, нөөцийн ашиглалт нь үр ашигтай байдаг.

Хамгийн алдартай контейнер платформ бол Docker юм. Docker нь контейнерыг угсрах, түгээх, болон ажиллуулах нэгдсэн стандартыг бий болгосон. Хөшигний цаана Docker нь контейнерыг ажиллуулах систем болгон Kubernetes зэрэг бусад хэрэгслүүдийн ашигладаг салбарын стандарт болох containerd-ийг ашигладаг.

Контейнер ажиллуулах нь энгийн. Жишээлбэл, контейнер дотор Python interpreter ажиллуулахдаа docker run ашиглана (-it flag нь контейнерыг терминалтай интерактив холбож ажиллуулна. Терминалаас гарахад контейнер зогсоно).

$ docker run -it python:3.12 python
Python 3.12.7 (main, Nov  5 2024, 02:53:25) [GCC 12.2.0] on linux
>>> print("Hello from inside a container!")
Hello from inside a container!

Амьдрал дээр таны програм систем даяарх файлын бүтцээс хамаарч болно. Үүнийг шийдэхийн тулд апп ажиллахад хэрэгтэй файлын системийг бүхэлд нь artifact болгон авч явдаг container image ашигладаг. Эдгээр image-ийг кодоор тодорхойлж үүсгэдэг. Docker ашиглахдаа Dockerfile syntax-аар image дотор хэрэгтэй dependency, системийн library, тохиргоогоо зааж өгнө:

FROM python:3.12
RUN apt-get update
RUN apt-get install -y gcc
RUN apt-get install -y libpq-dev
RUN pip install numpy
RUN pip install pandas
COPY . /app
WORKDIR /app
RUN pip install .

Чухал ялгаа: Docker-ийн image нь багцалсан artifact (template шиг), харин container нь тэр image-ийн ажиллаж байгаа бодит instance юм. Нэг image-ээс олон container зэрэг ажиллуулж болно. Image нь layer-үүдээс бүрддэг бөгөөд Dockerfile доторх instruction бүр (FROM, RUN, COPY г.м.) шинэ layer үүсгэнэ. Docker эдгээр layer-ийг cache хийдэг тул Dockerfile-ийн нэг мөр өөрчлөгдвөл зөвхөн тэр мөр болон түүнээс хойших layer-үүд дахин build хийгдэнэ.

Дээрх Dockerfile хэд хэдэн сул талтай: slim хувилбарын оронд бүтэн Python image ашигласан, тусдаа RUN command-ууд ажиллуулж хэрэггүй layer-үүд үүсгэсэн, version-уудыг pin хийгээгүй, мөн package manager-ийн cache-ийг цэвэрлээгүйгээс хэрэггүй файлууд image дотор үлдсэн. Бас container-ыг root хэрэглэгчээр ажиллуулах, нууц мэдээллээ санамсаргүйгээр image layer-т хадгалах зэрэг нь түгээмэл алдаа.

Илүү сайжруулсан хувилбар:

FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc libpq-dev && \
    rm -rf /var/lib/apt/lists/*
COPY pyproject.toml uv.lock ./
RUN uv pip install --system -r uv.lock
COPY . /app

Дээрх жишээнд uv-ийг source-оос build хийхийн оронд ghcr.io/astral-sh/uv:latest image-ээс бэлэн binary-г хуулж авч байна. Үүнийг builder pattern гэж нэрлэдэг. Энэ pattern-ы ачаар кодоо build хийхэд хэрэгтэй бүх tool-ийг final image дотор үлдээх шаардлагагүй; апп ажиллахад хэрэгтэй эцсийн binary л үлдэнэ.

Docker ашиглахад анхаарах хэдэн хязгаарлалт бий. Нэгдүгээрт, container image-үүд ихэвчлэн platform-specific байдаг — linux/amd64-д зориулж build хийсэн image нь linux/arm64 (Apple Silicon Mac) дээр emulation-гүй бол ажиллахгүй, emulation-тэй бол их удаан. Хоёрдугаарт, Docker container-д Linux kernel хэрэгтэй тул macOS, Windows дээр Docker цаанаа хөнгөн Linux VM ажиллуулдаг, энэ нь нэмэлт overhead үүсгэнэ. Гуравдугаарт, Docker-ийн isolation нь VM-ээс сул — container-ууд host OS-ийн kernel-ийг хуваалцдаг тул multi-tenant орчинд security risk гарч болно.

Өнөө үед олон төслүүд бүр “систем даяарх” сангууд болон аппуудыг төсөл бүрээр удирдахын тулд nix flakes-ээр дамжуулан nix-ийг ашиглаж байна.

Тохиргоо

Програм хангамж нь уг чанараараа тохируулах боломжтой байдаг. Командын мөрийн орчин лекцээр бид програмууд нь flags, орчны хувьсагчид (environment variables) эсвэл тохиргооны файлуудаар (dotfiles) дамжуулан тохиргоо авдгийг харсан. Энэ нь нарийн төвөгтэй аппуудад ч хамаатай бөгөөд тохиргоог том хэмжээнд удирдахад ашигладаг тогтсон загварууд байдаг. Програм хангамжийн тохиргоог кодон дотор хатуу бичиж болохгүй бөгөөд харин ажиллах үед нь (runtime) зааж өгөх ёстой. Хамгийн нийтлэг хоёр арга бол орчны хувьсагчид болон тохиргооны файлууд юм.

Environment variable-аар тохиргоо авдаг аппын жишээ:

import os

DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///local.db")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
API_KEY = os.environ["API_KEY"]  # Required - will raise if not set

Мөн аппыг тохиргооны файлаар (жишээ нь, yaml.load-оор тохиргоогоо уншдаг Python програм) тохируулж болно, config.yaml:

database:
  url: "postgresql://localhost/myapp"
  pool_size: 5
server:
  host: "0.0.0.0"
  port: 8080
  debug: false

Тохиргооны талаарх хамгийн чухал алтан дүрэм бол: ижил кодын сан нь ямар ч кодын өөрчлөлгүйгээр, зөвхөн тохиргооны өөрчлөлтөөр өөр өөр орнуудад (хөгжүүлэлтийн, туршилтын, бодит) байрших боломжтой байх ёстой.

Тохиргооны сонголтуудын дунд ихэвчлэн API түлхүүрүүд гэх мэт эмзэг мэдээллүүд байдаг. Эдгээр нууц мэдээллийг (secrets) санамсаргүйгээр задлахаас сэргийлж маш болгоомжтой хандах ёстой бөгөөд тэдгээрийг хэзээ ч хувилбарын хяналтын системд оруулж болохгүй.

Үйлчилгээнүүд ба зохицуулалт

Орчин үеийн аппууд дангаараа орших нь ховор. Жишээ вэб аппд өгөгдөл хадгалах өгөгдлийн сан (database), гүйцэтгэлийг сайжруулах кэш (cache), ар талын даалгавруудыг ажиллуулах дараалал (message queue) болон бусад туслах үйлчилгээнүүд шаардлагатай болдог. Бүх зүйлийг нэг цул монолит аппд багтаахын оронд орчин үеийн архитектурууд нь функцуудыг бие даан хөгжүүлж, байршуулж, өргөтгөх боломжтой тусдаа үйлчилгээнүүдэд задлахыг илүүд үздэг.

Жишээлбэл, хэрэв бид аппдаа кэш ашиглах нь зүйтэй гэж үзвэл өөрсдөө шинээр бүтээхийн оронд Redis эсвэл Memcached зэрэг туршигдсан бэлэн шийдлүүдийг ашиглаж болно. Бид Redis-ийг контейнерынхоо нэг хэсэг болгон суулгаж болох ч энэ нь манай апп болон Redis-ийн хоорондох бүх хамаарлуудыг нэг стандартад оруулах шаардлагатай болох бөгөөд энэ нь хэцүү эсвэл бүр боломжгүй байж болно. Үүний оронд бид бүрэлдэхүүн хэсэг бүрийг өөрийн контейнер дотор тусад нь байршуулж болно. Үүнийг ихэвчлэн микросервис архитектур гэж нэрлэдэг бөгөөд бүрэлдэхүүн хэсэг бүр нь бие даасан үйлчилгээ хэлбэрээр ажиллаж, сүлжээгээр (ихэвчлэн HTTP API-аар) хоорондоо харилцдаг.

Docker Compose нь олон контейнер бүхий аппуудыг тодорхойлж, ажиллуулахад зориулагдсан хэрэгсэл юм. Контейнеруудыг нэг бүрчлэн удирдахын оронд та нэг YAML файл дотор бүх үйлчилгээнүүдээ зарлаж, тэдгээрийг хамтад нь зохицуулна. Одоо манай апп нэгээс олон контейнерыг багтаах болно:

# docker-compose.yml
services:
  web:
    build: .
    ports:
      - "8080:8080"
    environment:
      - REDIS_URL=redis://cache:6379
    depends_on:
      - cache

  cache:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  redis_data:

docker compose up ажиллуулахад хоёр үйлчилгээ хамт эхлэх бөгөөд вэб апп нь cache гэсэн хост нэрээр (Docker-ийн дотоод DNS нь үйлчилгээний нэрийг автоматаар шийдвэрлэдэг) Redis рүү холбогдох боломжтой болно. Docker Compose нь нэг буюу хэд хэдэн үйлчилгээг хэрхэн байршуулахаа тодорхойлох боломжийг олгож, тэдгээрийг хамтад нь ажиллуулах, сүлжээний холболтыг тохируулах, болон өгөгдлийг хадгалах дундын эзлэхүүнүүдийг (volumes) удирдах зэрэг зохицуулалтыг хариуцдаг.

Бодит орчинд (production) байршуулах үед та docker compose үйлчилгээнүүдээ систем ачаалах үед автоматаар эхэлж, алдаа гарвал өөрөө дахин ажиллахыг хүсэх байх. Нийтлэг хэрэглэгддэг нэг арга бол docker compose-ийг удирдахад systemd ашиглах юм:

# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target

Энэ systemd тохиргооны файл нь систем асах үед (Docker бэлэн болсны дараа) таны аппыг автоматаар эхлүүлж, systemctl start myapp, systemctl stop myapp, болон systemctl status myapp гэх мэт стандарт command-уудаар удирдах боломж өгдөг.

Байршуулалтын шаардлага илүү нарийн төвөгтэй болох тусам — олон машин дамжуулан өргөтгөх, үйлчилгээ унах үед автоматаар сэргээх, болон өндөр хүртээмжтэй байдлыг хангах шаардлагатай болоход байгууллагууд олон мянган контейнерыг зэрэг удирдах чадвартай Kubernetes (k8s) зэрэг нарийн төвөгтэй контейнер зохицуулалтын платформуудыг сонгодог. Гэсэн хэдий ч Kubernetes-ийг сурахад хүндрэлтэй бөгөөд ашиглалтын асар их зардал дагуулдаг тул жижиг төслүүдийн хувьд хэтэрхий нүсэрддэг.

Эдгээр олон контейнерт орчин ажиллах боломжтой байгаа нь орчин үеийн үйлчилгээнүүд хоорондоо стандартчилсан API буюу HTTP REST API-аар харилцдагтай холбоотой юм. Жишээлбэл, програм OpenAI эсвэл Anthropic гэх мэт LLM үйлчилгээ үзүүлэгчтэй холбогдох бүртээ цаанаа тэдний сервер рүү HTTP хүсэлт илгээж, хариуг нь задлан уншиж байдаг:

$ curl https://api.anthropic.com/v1/messages \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "content-type: application/json" \
    -H "anthropic-version: 2023-06-01" \
    -d '{"model": "claude-sonnet-4-20250514", "max_tokens": 256,
         "messages": [{"role": "user", "content": "Explain containers vs VMs in one sentence."}]}'

Нийтлэх (Publishing)

Та кодоо зөв ажиллаж байгааг нотолсныхоо дараа түүнийг бусад хүмүүс татаж аваад суулгах боломжтой байдлаар түгээхийг хүсэж болох юм. Түгээлт нь олон хэлбэртэй бөгөөд таны ашиглаж буй програмчлалын хэл болон орчноос шууд хамаарна.

Түгээлтийн хамгийн энгийн хэлбэр бол хүмүүст татаж аваад өөрсдөө суулгахад зориулж артефактыг вэбэд байршуулах юм. Энэ нь одоо ч хэрэглэгдсээр байгаа бөгөөд жишээ нь Ubuntu-ийн багцын архив-аас үндсэндээ .deb файлуудын жагсаалтыг вэбээр харуулдаг.

Өнөө үед GitHub нь эх код болон артефактуудыг нийтлэх гол платформ болсон. Эх код нь ихэвчлэн нээлттэй байдаг бөгөөд GitHub Releases нь хөгжүүлэгчдэд тодорхой хувилбаруудад бэлэн хоёртын файлууд болон бусад артефактуудыг хавсаргаж нийтлэх боломж өгдөг.

Багц менежерүүд нь заримдаа эх кодноос эсвэл бэлэн wheel-ээс шууд GitHub-аас суулгахыг дэмждэг:

# Install from source (will clone and build)
$ pip install git+https://github.com/psf/requests.git

# Install from a specific tag/branch
$ pip install git+https://github.com/psf/requests.git@v2.32.3

# Install a wheel directly from a GitHub release
$ pip install https://github.com/user/repo/releases/download/v1.0/package-1.0-py3-none-any.whl

Үнэндээ Go зэрэг зарим хэлнүүд төвлөрөөгүй түгээлтийн загварыг ашигладаг — өөрөөр хэлбэл төвлөрсөн багцын сангийн оронд Go модулиуд нь шууд өөрсдийн эх кодын репозиториос түгээгддэг. github.com/gorilla/mux гэх мэт модулийн замууд нь код хаана байгааг заах бөгөөд go get нь тэндээс шууд татаж авдаг. Гэвч pip, cargo, эсвэл brew зэрэг ихэнх package manager түгээлт болон суулгалтыг хялбар болгохын тулд бэлэн package-уудын төвлөрсөн index ашигладаг. Хэрэв бид дараах command-ыг ажиллуулбал:

$ uv pip install requests --verbose --no-cache 2>&1 | grep -F '.whl'
DEBUG Selecting: requests==2.32.5 [compatible] (requests-2.32.5-py3-none-any.whl)
DEBUG No cache entry for: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl.metadata
DEBUG No cache entry for: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl

бид requests wheel-ийг хаанаас татаж байгааг харж болно. Файлын нэрэн дэх py3-none-any гэдгийг анхаараарай — энэ нь уг wheel нь ямар ч үйлдлийн систем, ямар ч архитектур дээр ямар ч Python 3 хувилбартай ажиллана гэсэн үг юм. Харин хөрвүүлсэн код агуулсан багцуудын хувьд wheel нь тухайн платформд зориулагдсан байдаг:

$ uv pip install numpy --verbose --no-cache 2>&1 | grep -F '.whl'
DEBUG Selecting: numpy==2.2.1 [compatible] (numpy-2.2.1-cp312-cp312-macosx_14_0_arm64.whl)

Энд байгаа cp312-cp312-macosx_14_0_arm64 нь уг wheel нь зөвхөн macOS 14+ үйлдлийн системтэй, ARM64 архитектур бүхий компьютер дээрх CPython 3.12 хувилбарт зориулагдсаныг заана. Хэрэв та өөр платформ ашиглаж байвал pip өөр өөр wheel татаж авах эсвэл эх кодноос нь угсрах болно.

Эсрэгээрээ, бидний бүтээсэн багцыг хүмүүс олж суулгах боломжтой болгохын тулд бид түүнийг эдгээр регистрүүдийн аль нэг рүү нийтлэх шаардлагатай. Python-ийн хувьд үндсэн бүртгэл нь Python Package Index (PyPI) юм. Суулгахтай адилхан багцыг нийтлэх хэд хэдэн арга бий. uv publish command нь багцуудыг PyPI рүү хуулах орчин үеийн интерфейсийг олгодог:

$ uv publish --publish-url https://test.pypi.org/legacy/
Publishing greeting-0.1.0.tar.gz
Publishing greeting-0.1.0-py3-none-any.whl

Энд бид бодит PyPI-ийг бохирдуулахгүйгээр нийтлэх үйл явцаа турших зориулалттай тусдаа бүртгэл болох TestPyPI-ийг ашиглаж байна. Нэгэнт хуулагдсан бол TestPyPI-аас ингэж суулгаж болно:

$ uv pip install --index-url https://test.pypi.org/simple/ greeting

Програм хангамж нийтлэх үед анхаарах нэг чухал зүйл бол итгэлцэл юм. Хэрэглэгчид өөрсдийн татаж авсан багцыг үнэхээр танаас ирсэн бөгөөд замдаа өөрчлөгдөөгүй гэдгийг хэрхэн баталгаажуулах вэ? Багцын бүртгэлүүд нь бүрэн бүтэн байдлыг шалгахын тулд хяналтын нийлбэрийг (checksums) ашигладаг бөгөөд зарим экосистем нь зохиогчийг криптографийн аргаар батлахын тулд багц гарын үсэг зурахыг (package signing) дэмждэг.

Хэл бүр өөрийн гэсэн багцын бүртгэлтэй: Rust-ийн хувьд crates.io, JavaScript-ийн хувьд npm, Ruby-ийн хувьд RubyGems, контейнерын зургийн хувьд Docker Hub гэх мэт. Үүний зэрэгцээ, хувийн эсвэл дотоод багцуудын хувьд байгууллагууд өөрсдийн багцын санг (жишээ нь, хувийн PyPI сервер эсвэл хувийн Docker бүртгэл) байршуулж ашиглах эсвэл үүлэн үйлчилгээ үзүүлэгчдийн бэлэн шийдлийг ашигладаг.

Вэб үйлчилгээг интернетэд байршуулахад нэмэлт дэд бүтэц шаардлагатай болно: домэйн нэр бүртгүүлэх, домэйныг сервер рүүгээ чиглүүлэх DNS тохиргоо, болон HTTPS холболтыг зохицуулж, урсгалыг чиглүүлэх nginx зэрэг урвуу прокси (reverse proxy). Баримт бичиг эсвэл статик вэбсайт гэх мэт хялбар тохиолдлуудад GitHub Pages нь репозиториос шууд үнэгүй хостинг олгодог.

Дасгалууд

  1. Өөрийн орчны хувьсагчдыг printenv command-аар файлд хадгалаад, venv үүсгэн идэвхжүүлж, дахин орчны хувьсагчдыг өөр файлд хадгалаад diff before.txt after.txt command-аар шалгаарай. Орчинд юу өөрчлөгдсөн бэ? Shell яагаад venv-ийг илүүд үзэж байна вэ? (Санамж: идэвхжүүлэхийн өмнөх ба дараах $PATH хувьсагчийг хараарай.) which deactivate command-ыг ажиллуулж, deactivate bash функц юу хийж байгааг тунгаагаарай.
  2. pyproject.toml файл бүхий Python багц үүсгээд, түүнийг виртуал орчинд суулгаарай. Лок файл үүсгэж, түүнийгээ шалгана уу.
  3. Docker-ийг суулгаад, Missing Semester-ийн хичээлийн вэбсайтыг docker compose ашиглан локал орчиндоо угсарч ажиллуулаарай.
  4. Энгийн Python аппд зориулсан Dockerfile бичээрэй. Дараа нь өөрийн аппыг Redis кэштэй хамт ажиллуулдаг docker-compose.yml файлыг бэлдээрэй.
  5. TestPyPI рүү өөрийн Python багцыг нийтлээрэй (хэрэв бусадтай хуваалцах үнэ цэнэтэй биш бол бодит PyPI рүү бүү нийтлээрэй!). Дараа нь уг багцыг агуулсан Docker зураг үүсгэж, ghcr.io руу хуулаарай.
  6. GitHub Pages ашиглан өөрийн вэбсайтыг үүсгээрэй. Нэмэлтээр: түүндээ өөрийн тусгай домэйныг тохируулаарай.

Энэ хуудсыг засварлах.

Лиценз: CC BY-NC-SA.