Extending VSCode Dev Container Features
UPDATE: Thedev-container-features-template repo referenced in this post has been archived. Please see my new posthere or visitcontainers.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 calledRemote - Containers. This extension lets you use a Docker container as a development environment and is the technology that powersGitHub 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 haveDocker Desktop,orRancher Desktop (withdockerd
as the container runtime sincecontainerd
will not work with remote containers), orDocker Engine running on your local machine.
To get started with Dev Containers, take a look at this article onremote containers or this quickstart onGitHub 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 forAzure CLI usingDev 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 ofJSON
to yourdevcontainer.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 fileshere.
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 theaz 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, thecontainerapp-compose
extension is in preview. If I wanted these extensions to be included in my Dev Container without being bothered with having to run theaz 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 myDockerfile
to include theaz extension add
commands. This is a good option, but it requires me to write theaz extension add
command for every extension I need. I could also use thepostCreateCommand
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 thepreview 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 thefeatures
block within thedevcontainer.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 documentedhere, a Dev Container's built-in features are sources from thescript-library folder in thevscode-dev-containers repo. TheRemote - Containers extension and GitHub Codespaces include "preview" functionality to extend Dev Container features. You can add any custom feature by using thedev-container-features-template sample repository.
I begin my work by creating and hosting a newrepo on GitHub. To do this, I navigated to thetemplate 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 aREADME.md file that includes directions on how to work within it and add features. This repo also contains a GitHub Action workflow file calleddeploy-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 thedevcontainer-features.json file, then implement the feature in theinstall.sh file.
Once you have created a new repo via the template, make sure you have cloned your repo then open thedevcontainer-features.json
file in your favorite code editor (hopefully, VSCode 😉). The sample includes two featureshelloworld
andcolor
which can be further customized using theoptions
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 featureazextension
but I want to the ability to pass in a list of extension names so that I can iterate through them an run theaz 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 thejson
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 namedazextension
so I'll set the value ofid
to that. I also want to pass in a list of extension names, so I'll add a new object callednames
as an option. Since I want to pass in multiple extensions, I've set thetype
to be anarray
.
The actual installation of the extensions will be performed by theinstall.sh
script which is also in the root of the repo.
Open theinstall.sh
file then replace the contents with the following code:
#!/bin/bashset-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.envset +aif[!-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','' '`)foriin"${names[@]}"doecho"Installing${i}" su vscode-c"az extension add --name${i} -y"donefi
As commented in the script, thenames
value in thedevcontainer.json
file is considered to be a "build argument" and will be parsed into a file calleddevcontainer-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 calledazextension
and the option being callednames
, 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 afor loop
.
Inside thefor loop
, theaz 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 thevscode
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.1git push origin v0.0.1
Use your new feature
Using the new feature is as easy as referencing it in thefeatures
block in thedevcontainer.json
file.
Here is one example of how this new feature is being used.
//Forformatdetails,seehttps://aka.ms/devcontainer.json.Forconfigoptions,seetheREADMEat://https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/ubuntu{"name":"Ubuntu","build":{"dockerfile":"Dockerfile",//Update'VARIANT'topickanUbuntuversion:jammy/ubuntu-22.04,focal/ubuntu-20.04,bionic/ubuntu-18.04//Useubuntu-22.04orubuntu-18.04onlocalarm64/AppleSilicon."args":{"VARIANT":"ubuntu-22.04"}},//Use'forwardPorts'tomakealistofportsinsidethecontaineravailablelocally.//"forwardPorts":[],//Use'postCreateCommand'toruncommandsafterthecontaineriscreated.//"postCreateCommand":"uname -a",//Commentouttoconnectasrootinstead.Moreinfo: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 thepauldotyu/devcontainer-features/azextension@v0.0.1
block, you can pass in extension names into thenames
property as a comma-separated list. In this case, I added thecontainerapp
andcontainerapp-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 modifyDockerfile
code each time I need to add/remove extensions. All I need to do now is update thenames
option and add as many extension names as needed as a comma-separated list.
This was just one use case of extending thefeatures 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:
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse