Extending VSCode Dev Container Features
Tags: vscode, azure, devcontainers, tutorial
Published: 2022-07-28 02:57:00

> UPDATE: The [dev-container-features-template](https://github.com/microsoft/dev-container-features-template) repo referenced in this post has been archived. Please see my new post [here](https://dev.to/azure/re-visiting-dev-container-features-205i) or visit [containers.dev](https://containers.dev) for the latest on this topic.

Have you ever wanted to try an Azure CLI extension without having to install the extension yourself on your local machine? VSCode Dev Containers may be a good option.

## What is a Dev Container?

Visual Studio Code has an extension called [Remote - Containers][remote-containers]. This extension lets you use a Docker container as a development environment and is the technology that powers [GitHub Codespaces](https://docs.github.com/en/codespaces/overview). If you don't have the option to use Codespaces, the remote container technology also allows you to open build/open the container within your local environment as long as you have [Docker Desktop](https://www.docker.com/products/docker-desktop/),or [Rancher Desktop](https://rancherdesktop.io/) (with `dockerd` as the container runtime since `containerd` will not work with remote containers), or [Docker Engine](https://docs.docker.com/engine/install/#server) running on your local machine.

To get started with Dev Containers, take a look at this article on [remote containers](https://code.visualstudio.com/docs/remote/containers) or this quickstart on [GitHub Codespaces](https://docs.github.com/en/codespaces/getting-started/quickstart). There's also a series that some friends at Microsoft put together to help you get started: https://aka.ms/BeginnerSeriesToDevContainers

This article will build on top of the quick start guides listed above and walk you through how to extend a Dev Container to include custom features. More specifically, installing preview extensions for [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/what-is-azure-cli) using [Dev Container features which is currently in preview](https://code.visualstudio.com/docs/remote/containers#_dev-container-features-preview).

## Azure CLI in Dev Containers

If you have used Dev Containers before, you may be aware of the fact that Dev Containers offer you the ability to add features with Azure CLI being one of them. Going through the container configuration wizard, you will be presented with the following list of features that you can simply "add" into your container.

![Dev Container feature list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2fzhonvtivzoavxe17zq.png)

The base VSCode "Dev Containers" containers are built and published by the VSCode team and you can customize your container based on your needs. If you need Azure CLI, simply check the checkbox; it's that easy. The base container will be built, then the feature(s) you selected will be installed on top. When you select to add features, it appends a block of `JSON` to your `devcontainer.json` configuration file. This file is what tells VSCode how it should access or create your Dev Container. You can find more details on the properties of the files [here][devcontainer-json-ref].

![Azure CLI feature in devcontainer.json](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kmzg0okmce3n8rx106cv.png)

## What about Azure CLI Extensions?

As you can see, it is really easy to add Azure CLI to your Dev container. But what if you needed to add an Azure CLI extension? That's where things get a bit fuzzy. If you don't do anything other than selecting Azure CLI, you'd have to wait for the container to be built, and then install the extension manually using the `az extension add` command.

For my use case, I needed to install the following Azure CLI extensions to test out capabilities around Azure Container Apps:

- `containerapp`
- `containerapp-compose`

Both of these extensions are relatively new and at the time of this writing, the `containerapp-compose` extension is in preview. If I wanted these extensions to be included in my Dev Container without being bothered with having to run the `az extension add` command, I need to include the install instructions my build process. There are a few options for this... Easiest thing I can do is update my `Dockerfile` to include the `az extension add` commands. This is a good option, but it requires me to write the `az extension add` command for every extension I need. I could also use the `postCreateCommand` in the devcontainer.json file, but again, I’d need to add a command to install each extension. A better way to do this is may be to use the [preview feature of Dev containers][devcontainer-features-preview]. This method can provide a smoother path for customizing your Def Container. Using this approach, I can build my own custom feature and reference them in the `features` block within the `devcontainer.json` file and add any Azure CLI extension like any other feature in the Dev Container build format.

Ready to walk through how to set this up? Let's go!

## Dev container features (preview)

> This feature is in preview and may be subject to change. If what I write below ends up changing, I will make an effort to update this post to reflect the new form and format.

As documented [here][devcontainer-features-preview], a Dev Container's built-in features are sources from the [script-library](https://github.com/microsoft/vscode-dev-containers/tree/main/script-library/docs) folder in the [vscode-dev-containers](https://github.com/microsoft/vscode-dev-containers) repo. The [Remote - Containers][remote-containers] extension and GitHub Codespaces include "preview" functionality to extend Dev Container features. You can add any custom feature by using the [dev-container-features-template][devcontainer-features-template] sample repository.

I begin my work by creating and hosting a new [repo](https://github.com/pauldotyu/devcontainer-features) on GitHub. To do this, I navigated to the [template repo][devcontainer-features-template] and clicked the "Use this template" button. Fill in the form details to create a new repo that you will own.

The template repo has a [README.md](https://github.com/microsoft/dev-container-features-template/blob/main/README.md) file that includes directions on how to work within it and add features. This repo also contains a GitHub Action workflow file called [deploy-features.yml](https://github.com/microsoft/dev-container-features-template/blob/main/.github/workflows/deploy-features.yml) which will trigger the compression and publish of your project artifacts each time a new tag is pushed.

The way this template works is that you define the data structure in the [devcontainer-features.json](https://github.com/microsoft/dev-container-features-template/blob/main/devcontainer-features.json) file, then implement the feature in the [install.sh](https://github.com/microsoft/dev-container-features-template/blob/main/install.sh) file.

Once you have created a new repo via the template, make sure you have cloned your repo then open the `devcontainer-features.json` file in your favorite code editor (hopefully, VSCode 😉). The sample includes two features `helloworld` and `color` which can be further customized using the `options` property.

Here's what it looks like "out-of-the-box":
 
```json
{
  "features": [
    {
      "id": "helloworld",
      "name": "My First Feature",
      "options": {
        "greeting": {
          "type": "string",
          "proposals": [ "hey", "hello", "hi", "howdy" ],
          "default": "hey",
          "description": "Select a pre-made greeting, or enter your own"
        }
      },
      "include": ["ubuntu"]
    },
    {
      "id": "color",
      "name": "A feature to remind you of your favorite color",
      "options": {
        "favorite": {
          "type": "string",
          "enum": [ "red", "gold", "green" ],
          "default": "red",
          "description": "Choose your favorite color."
        }
      },
      "include": ["ubuntu"]
    }
  ]
}
```

For my use case, all I need is one feature. I've called my feature `azextension` but I want to the ability to pass in a list of extension names so that I can iterate through them an run the `az extension add` command. This will give me the flexibility to install any number of Azure CLI extensions without having to add code each time I want to add a new extension.

Let's replace the `json` with the following:

```json
{
  "features": [
    {
      "id": "azextension",
      "name": "Azure CLI extension name",
      "options": {
        "names": {
          "type": "array",
          "description": "Enter the names of the Azure CLI extensions you wish to add"
      }
      },
      "include": ["ubuntu"]
    }
  ]
}
```

I want my new feature to be named `azextension` so I'll set the value of `id` to that. I also want to pass in a list of extension names, so I'll add a new object called `names` as an option. Since I want to pass in multiple extensions, I've set the `type` to be an `array`.

The actual installation of the extensions will be performed by the `install.sh` script which is also in the root of the repo.

Open the `install.sh` file then replace the contents with the following code:

```bash
#!/bin/bash
set -e

# The install.sh script is the installation entrypoint for any dev container 'features' in this repository. 
#
# The tooling will parse the devcontainer-features.json + user devcontainer, and write 
# any build-time arguments into a feature-set scoped "devcontainer-features.env"
# The author is free to source that file and use it however they would like.
set -a
. ./devcontainer-features.env
set +a

if [ ! -z ${_BUILD_ARG_AZEXTENSION} ]; then
    # Build args are exposed to this entire feature set following the pattern:  _BUILD_ARG_<FEATURE ID>_<OPTION NAME>
    NAMES="${_BUILD_ARG_AZEXTENSION_NAMES}"
    
    echo "Installing Azure CLI extensions: ${NAMES}"
    names=(`echo ${NAMES} | tr ',' ' '`)
    for i in "${names[@]}"
    do
        echo "Installing ${i}"
        su vscode -c "az extension add --name ${i} -y"
    done
fi
```

As commented in the script, the `names` value in the `devcontainer.json` file is considered to be a "build argument" and will be parsed into a file called `devcontainer-features.env` during container build time. The script will then load this file, and the value will be available as environment variables. There is a naming convention on how the values can be accessed during build time. The comment in the script above states that build args are exposed using the the following pattern:

`_BUILD_ARG_<FEATURE ID>_<OPTION NAME>` 

With my extension being called `azextension` and the option being called `names`, the variable will be:

`_BUILD_ARG_AZEXTENSION_NAMES`

Once the variable has been loaded, I can then parse the comma-separated value into an array then iterate over each value using a `for loop`.

Inside the `for loop`, the `az extension add` command is executed to install each feature. One important thing to note is that a VSCode Dev Container will run as a non-root user; therefore, I need to run the command as the `vscode` user to ensure the extension is available once my container is running.

Finally, with code in place, I commit and push it back up to my remote repo on GitHub then create a tag and push as well. This will kick-off the GitHub Action process to publish a new release.

Here's the command I ran to tag and push:

```bash
git tag v0.0.1
git push origin v0.0.1
```

## Use your new feature

Using the new feature is as easy as referencing it in the `features` block in the `devcontainer.json` file.

[Here](https://github.com/smurawski/docker-compose-examples/blob/main/.devcontainer/devcontainer.json) is one example of how this new feature is being used.

```json
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/ubuntu
{
  "name": "Ubuntu",
  "build": {
    "dockerfile": "Dockerfile",
    // Update 'VARIANT' to pick an Ubuntu version: jammy / ubuntu-22.04, focal / ubuntu-20.04, bionic /ubuntu-18.04
    // Use ubuntu-22.04 or ubuntu-18.04 on local arm64/Apple Silicon.
    "args": { "VARIANT": "ubuntu-22.04" }
  },

  // Use 'forwardPorts' to make a list of ports inside the container available locally.
  // "forwardPorts": [],

  // Use 'postCreateCommand' to run commands after the container is created.
  // "postCreateCommand": "uname -a",

  // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
  "remoteUser": "vscode",
  "features": {
    "docker-in-docker": "latest",
    "azure-cli": "latest",
    "node": "lts",
    "golang": "latest",
    "dotnet": "latest",
    "pauldotyu/devcontainer-features/azextension@v0.0.1": {
      "names": ["containerapp", "containerapp-compose"]
    }
  }
}
```

In the features block above, the custom feature is referenced using the following naming convention: `organization/repo/feature@tag`. Inside the `pauldotyu/devcontainer-features/azextension@v0.0.1` block, you can pass in extension names into the `names` property as a comma-separated list. In this case, I added the `containerapp` and `containerapp-compose` extensions, but I can add more as needed.

## Summary

To recap, I needed a way to install Azure CLI extensions as a feature of a Dev Container. The built-in features give us the ability to add Azure CLI, but Azure CLI extensions cannot be added the same way. So I chose to use the Dev Containers features capability which is currently in preview to extend and build on top of their features format. This made it really easy to add as many Azure CLI extensions without having to modify `Dockerfile` code each time I need to add/remove extensions. All I need to do now is update the `names` option and add as many extension names as needed as a comma-separated list.

This was just one use case of extending the **features** feature of Dev Containers. I'm sure you can come up with more.

Happy packaging!

## References

To learn more about extending Dev Container features, I encourage you to check out the resources below:

- [Developing inside a container][remote-containers]
- [GitHub Codespaces](https://code.visualstudio.com/docs/remote/codespaces)
- [devcontainer.json reference][devcontainer-json-ref]
- [devcontainer features (preview)][devcontainer-features-preview]
- [devcontainer features template][devcontainer-features-template]
- https://aka.ms/BeginnerSeriesToDevContainers

<!--
The following section includes URLs that are used in markdown
-->
[remote-containers]: https://code.visualstudio.com/docs/remote/containers
[devcontainer-overview]: https://code.visualstudio.com/docs/remote/containers
[devcontainer-json-ref]: https://code.visualstudio.com/docs/remote/devcontainerjson-reference
[devcontainer-features-preview]: https://code.visualstudio.com/docs/remote/containers#_dev-container-features-preview
[devcontainer-features-template]: https://github.com/microsoft/dev-container-features-template