PAUL'S BLOG

Learn. Build. Share. Repeat.

Extending Visual Studio Code Dev Container Features

2022-07-27 10 min read Tutorial

UPDATE: The dev-container-features-template repo referenced in this post has been archived. Please see my new post here or visit 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. This extension lets you use a Docker container as a development environment and is the technology that powers GitHub Codespaces. 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,or Rancher Desktop (with dockerd as the container runtime since containerd will not work with remote containers), or Docker Engine running on your local machine.

To get started with Dev Containers, take a look at this article on remote containers or this quickstart on GitHub Codespaces. 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 using Dev Container features which is currently in 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

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.

Azure CLI feature in devcontainer.json

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. 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, a Dev Container’s built-in features are sources from the script-library folder in the vscode-dev-containers repo. The 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 sample repository.

I begin my work by creating and hosting a new repo on GitHub. To do this, I navigated to the template repo 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 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 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 file, then implement the feature in the 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”:

{
  "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:

{
  "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:

#!/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:

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 is one example of how this new feature is being used.

// 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: