Skip to content

Dev Containers w VS Code - jedno środowisko dla wielu projektów

Pracując nad wieloma projektami infrastrukturalnymi szybko trafiamy na ten sam problem: każdy z nich wymaga odpowiedniej wersji narzędzi, rozszerzeń edytora i konfiguracji środowiska. OpenTofu, Ansible, Vault, Go, Python, direnv, certyfikaty, pluginy VS Code - lista rośnie, a ręczne przygotowanie kolejnego laptopa lub maszyny wirtualnej zabiera czas.

Rozwiązaniem są Dev Containers: kontenery developerskie uruchamiane bezpośrednio z Visual Studio Code. Repozytorium devcontainers zbiera gotowe profile środowisk oraz helpery, które pozwalają podpinać je do kolejnych projektów.


Do czego służy Dev Containers w VS Code?

Section titled “Do czego służy Dev Containers w VS Code?”

Rozszerzenie Dev Containers pozwala uruchomić środowisko developerskie zdefiniowane w pliku .devcontainer/devcontainer.json. VS Code nadal działa na hoście, ale serwer edytora, terminal i komendy projektu uruchamiane są już w kontenerze.

Dzięki temu:

  • wersje narzędzi są zapisane w kodzie zamiast w pamięci autora projektu,
  • nowy projekt można otworzyć bez instalowania całego toolchainu na hoście,
  • rozszerzenia VS Code są dobierane do rodzaju pracy,
  • kilka projektów może używać różnych środowisk bez konfliktów,
  • aktualizację wspólnych narzędzi można wykonać w jednym miejscu.

Pod spodem nadal działa Docker. Dev Containers dodaje warstwę integracji z VS Code: buduje obraz, uruchamia kontener, montuje workspace i otwiera projekt w ścieżce wskazanej przez konfigurację.


Repozytorium rozdziela konfigurację na profile, wspólne skrypty i proste helpery:

devcontainers/
├── bin/
│   ├── devcontainer-link
│   ├── devcontainer-render
│   └── devcontainer-update
├── common/
│   ├── Dockerfile.base
│   ├── scripts/
│   └── snippets/
├── profiles/
│   ├── ansible/
│   ├── base/
│   ├── golang/
│   ├── opentofu/
│   ├── python/
│   └── vault/
└── templates/
    └── project-devcontainer/

Każdy profil zawiera dwa podstawowe pliki:

profiles/<profile>/devcontainer.json
profiles/<profile>/Dockerfile

devcontainer.json opisuje sposób uruchomienia środowiska przez VS Code: obraz, workspace, mounty, użytkownika oraz rozszerzenia edytora. Dockerfile buduje środowisko i instaluje potrzebne narzędzia. Powtarzalne kroki zostały przeniesione do common/scripts/, dzięki czemu nie trzeba kopiować tych samych instrukcji do każdego profilu.

ProfilPrzeznaczenie
baseBazowe środowisko developerskie z shellem, Gitem, direnv, Node.js i Codex CLI.
opentofuPraca z OpenTofu oraz narzędziami dla kodu infrastruktury.
ansibleŚrodowisko dla projektów automatyzowanych przez Ansible.
vaultPraca z HashiCorp Vault CLI.
golangToolchain dla projektów Go.
pythonŚrodowisko dla projektów Python, rozszerzone między innymi o conftest.

Przed rozpoczęciem pracy potrzebne są:

  • Docker,
  • Visual Studio Code,
  • rozszerzenie Dev Containers,
  • lokalna kopia repozytorium devcontainers.

Najprostszy wariant to podpięcie profilu jako symlink. Helper devcontainer-link tworzy w projekcie dowiązanie .devcontainer do wybranego profilu.

  1. cd /repo/dev.rachuna/artifacts/opentofu/gitlab-group
  2. /repo/dev.rachuna/devtools/devcontainers/bin/devcontainer-link opentofu

    Efekt odpowiada ręcznemu utworzeniu dowiązania:

    ln -s /repo/dev.rachuna/devtools/devcontainers/profiles/opentofu .devcontainer
  3. W VS Code uruchom polecenie:

    Dev Containers: Reopen in Container
  4. VS Code zbuduje obraz i ponownie otworzy projekt. Dla profilu opentofu wykona także kontrolne polecenie:

    tofu version

Od tej chwili terminal otwierany w VS Code działa wewnątrz kontenera.


Dlaczego ścieżka projektu się nie zmienia?

Section titled “Dlaczego ścieżka projektu się nie zmienia?”

Domyślnie Dev Containers montuje kod w katalogu podobnym do /workspaces/nazwa-projektu. W projektach infrastrukturalnych wygodniej jest zachować tę samą ścieżkę na hoście i w kontenerze, szczególnie gdy skrypty odwołują się do innych repozytoriów.

Profile takie jak opentofu konfigurują więc:

{
  "workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind,consistency=cached",
  "workspaceFolder": "${localWorkspaceFolder}"
}

Jeśli projekt na hoście znajduje się tutaj:

/repo/dev.rachuna/artifacts/opentofu/gitlab-group

to po otwarciu kontenera nadal pracujemy w:

/repo/dev.rachuna/artifacts/opentofu/gitlab-group

Nie trzeba zmieniać ścieżek w skryptach ani zastanawiać się, czy polecenie zostało uruchomione na hoście, czy już w kontenerze.


Repozytorium obsługuje dwa warianty pracy. Wybór zależy od tego, czy projekt ma korzystać z centralnie utrzymywanego profilu, czy przechowywać własną kopię konfiguracji.

/repo/dev.rachuna/devtools/devcontainers/bin/devcontainer-link opentofu /repo/dev.rachuna/artifacts/opentofu/gitlab-group

To najwygodniejsze rozwiązanie dla lokalnych projektów utrzymywanych na tej samej maszynie. Projekt zawsze korzysta z aktualnego profilu z repozytorium devcontainers. Po zmianie Dockerfile wystarczy w VS Code wykonać:

Dev Containers: Rebuild Container

Symlink ma jednak jedną ważną konsekwencję: projekt zależy od lokalnej ścieżki repozytorium devcontainers.

Wariant 2: renderowanie konfiguracji do projektu

Section titled “Wariant 2: renderowanie konfiguracji do projektu”

Jeżeli projekt powinien zawierać własną, samowystarczalną kopię .devcontainer, można ją wygenerować:

/repo/dev.rachuna/devtools/devcontainers/bin/devcontainer-render opentofu /repo/dev.rachuna/artifacts/opentofu/gitlab-group

Helper zapisze:

.devcontainer/
├── .profile
├── Dockerfile
├── devcontainer.json
└── common/
    └── scripts/

Renderowana konfiguracja ma lokalny build context i własną kopię wspólnych skryptów. Można ją przechowywać razem z projektem, udostępniać innym osobom oraz modyfikować niezależnie od centralnego repozytorium.

Aby później odświeżyć wygenerowany wariant, wystarczy uruchomić:

/repo/dev.rachuna/devtools/devcontainers/bin/devcontainer-update /repo/dev.rachuna/artifacts/opentofu/gitlab-group

Polecenie odczyta nazwę profilu z .devcontainer/.profile i ponownie wykona render.


Spójrzmy na najważniejsze fragmenty konfiguracji opentofu.

{
  "build": {
    "dockerfile": "Dockerfile",
    "context": "/repo/dev.rachuna/devtools/devcontainers",
    "args": {
      "USERNAME": "mrachuna"
    }
  }
}

Kontekst budowania wskazuje repozytorium devcontainers, ponieważ Dockerfile korzysta ze wspólnych skryptów:

COPY common/scripts/ /tmp/devcontainer-scripts/
{
  "remoteUser": "mrachuna",
  "mounts": [
    "source=${localEnv:HOME},target=/home/mrachuna,type=bind,consistency=cached",
    "source=/repo,target=/repo,type=bind,consistency=cached"
  ],
  "containerEnv": {
    "HOME": "/home/mrachuna"
  }
}

Kontener używa tego samego katalogu domowego i widzi drzewo /repo. Dzięki temu wewnątrz kontenera dostępne są między innymi konfiguracja shella, Git, SSH oraz sąsiednie projekty.

Profil może automatycznie doinstalować rozszerzenia właściwe dla danego rodzaju projektu:

{
  "customizations": {
    "vscode": {
      "extensions": [
        "editorconfig.editorconfig",
        "redhat.vscode-yaml",
        "gamunu.opentofu",
        "ms-azuretools.vscode-docker",
        "timonwong.shellcheck"
      ]
    }
  }
}

Po otwarciu projektu dostajemy więc nie tylko CLI, lecz także gotowy edytor.


Rozwój repozytorium polega najczęściej na aktualizacji wspólnego skryptu, zmianie istniejącego profilu albo dodaniu kolejnego profilu.

Jeżeli narzędzie ma znaleźć się w kilku środowiskach, dodaj lub zmodyfikuj skrypt w:

common/scripts/

Następnie wywołaj go w Dockerfile odpowiednich profili. Przykładowo instalacja direnv jest współdzielona przez wiele obrazów:

RUN bash /tmp/devcontainer-scripts/install-direnv

Każdy profil rozwijamy w jego katalogu:

profiles/opentofu/
├── Dockerfile
└── devcontainer.json

Zmiana w Dockerfile wpływa na zawartość obrazu. Zmiana w devcontainer.json wpływa na sposób uruchamiania kontenera przez VS Code: mounty, rozszerzenia, użytkownika, workspace i komendy startowe.

  1. mkdir -p profiles/<nazwa-profilu>
  2. Wybierz możliwie prosty obraz bazowy i wykorzystaj skrypty z common/scripts/.

  3. Skonfiguruj build, remoteUser, mounty, workspace oraz rozszerzenia VS Code dopasowane do technologii.

  4. Podepnij go do testowego projektu:

    bin/devcontainer-link <nazwa-profilu> /tmp/devcontainer-profile-test

    Następnie otwórz katalog w VS Code i wykonaj:

    Dev Containers: Rebuild and Reopen in Container

Przed wysłaniem zmian warto wykonać podstawowe testy statyczne:

jq empty profiles/*/devcontainer.json common/snippets/*.json
bash -n bin/devcontainer-render bin/devcontainer-link bin/devcontainer-update common/scripts/*

Można również sprawdzić renderowanie na katalogu tymczasowym:

mkdir -p /tmp/devcontainer-render-test
bin/devcontainer-render opentofu /tmp/devcontainer-render-test

Na końcu pozostaje test praktyczny: przebudowanie kontenera w VS Code oraz uruchomienie podstawowego polecenia dla danego profilu, na przykład:

tofu version

PotrzebaNajlepszy wybór
Kilka własnych projektów na jednej maszyniedevcontainer-link
Centralna aktualizacja środowiska bez kopiowania plikówdevcontainer-link
Konfiguracja wersjonowana razem z projektemdevcontainer-render
Udostępnienie środowiska innym osobomdevcontainer-render
Niezależne dostosowanie .devcontainer w konkretnym projekcielokalna kopia po devcontainer-render

Dev Containers dobrze rozwiązuje problem powtarzalnego warsztatu pracy. Zamiast dokumentować kolejną listę pakietów do ręcznej instalacji, utrzymujemy wykonywalną definicję środowiska. VS Code staje się wejściem do projektu, Docker zapewnia izolację, a wspólne profile pozwalają rozwijać narzędzia w jednym miejscu.