Extending Visual Studio Code Dev Container Features
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.
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.
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: