{"id":24305,"date":"2019-08-28T09:30:06","date_gmt":"2019-08-28T16:30:06","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=24305"},"modified":"2021-09-29T12:16:20","modified_gmt":"2021-09-29T19:16:20","slug":"how-the-net-team-uses-azure-pipelines-to-produce-docker-images","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/how-the-net-team-uses-azure-pipelines-to-produce-docker-images\/","title":{"rendered":"How the .NET Team uses Azure Pipelines to produce Docker Images"},"content":{"rendered":"<p><span data-contrast=\"none\">Produc<\/span><span data-contrast=\"none\">ing\u00a0<\/span><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/using-net-and-docker-together-dockercon-2019-update\/\"><span data-contrast=\"none\">Docker images for .NET<\/span><\/a><span data-contrast=\"none\">\u00a0might not seem\u00a0<\/span><span data-contrast=\"none\">like that big of a deal.\u00a0 O<\/span><span data-contrast=\"none\">nce you\u2019ve got a\u00a0<\/span><span data-contrast=\"none\">Dockerfile<\/span><span data-contrast=\"none\">\u00a0defined, just run\u00a0<\/span><span data-contrast=\"none\">&#8220;<\/span><span data-contrast=\"none\">docker build<\/span><span data-contrast=\"none\">&#8220;<\/span><span data-contrast=\"none\">\u00a0and\u00a0<\/span><span data-contrast=\"none\">&#8220;<\/span><span data-contrast=\"none\">docker\u00a0<\/span><span data-contrast=\"none\">push<\/span><span data-contrast=\"none\">&#8220;<\/span><span data-contrast=\"none\">\u00a0and you\u2019re done, right?<\/span><span data-contrast=\"none\">\u00a0\u00a0<\/span><span data-contrast=\"none\">Then<\/span><span data-contrast=\"none\">\u00a0just rinse and repeat\u00a0<\/span><span data-contrast=\"none\">when\u00a0<\/span><span data-contrast=\"none\">new versions of .NET\u00a0<\/span><span data-contrast=\"none\">are released\u00a0<\/span><span data-contrast=\"none\">and that should be all that\u2019s needed.\u00a0 Well, it\u2019s not quite that simple.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">When you factor in\u00a0<\/span><span data-contrast=\"none\">the\u00a0<\/span><span data-contrast=\"none\">number\u00a0<\/span><span data-contrast=\"none\">of Linux distro<\/span><span data-contrast=\"none\">s<\/span><span data-contrast=\"none\">\u00a0and Windows versions, different processor architectures,\u00a0<\/span><span data-contrast=\"none\">and\u00a0<\/span><span data-contrast=\"none\">different .NET versions, you end up\u00a0<\/span><span data-contrast=\"none\">with a substantial matrix of images that need to be<\/span><span data-contrast=\"none\">\u00a0built and<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">published<\/span><span data-contrast=\"none\">.\u00a0\u00a0<\/span><span data-contrast=\"none\">Then consider that some images have dependencies on others<\/span><span data-contrast=\"none\">\u00a0which implies a specific order in which to build the images<\/span><span data-contrast=\"none\">.\u00a0 And\u00a0<\/span><span data-contrast=\"none\">on top<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">of all that,\u00a0<\/span><span data-contrast=\"none\">we need to<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">ensure the\u00a0<\/span><span data-contrast=\"none\">images\u00a0<\/span><span data-contrast=\"none\">ar<\/span><span data-contrast=\"none\">e p<\/span><span data-contrast=\"none\">ublished\u00a0<\/span><span data-contrast=\"none\">as quickly as possible\u00a0<\/span><span data-contrast=\"none\">so that customers can get their hands on\u00a0<\/span><span data-contrast=\"none\">newly released product versions and security fixes.<\/span><span data-contrast=\"none\">\u00a0\u00a0<\/span><span data-contrast=\"none\">Oh, and by the way, in addition to the\u00a0<\/span><a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\"><span data-contrast=\"none\">.NET Core<\/span><\/a><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">images\u00a0<\/span><span data-contrast=\"none\">we also produce .NET Core\u00a0<\/span><a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/tree\/nightly\"><span data-contrast=\"none\">nightly<\/span><\/a><span data-contrast=\"none\">\u00a0images for preview releases,\u00a0<\/span><a href=\"https:\/\/github.com\/dotnet\/dotnet-buildtools-prereqs-docker\"><span data-contrast=\"none\">images<\/span><\/a><span data-contrast=\"none\">\u00a0for developers of .NET Core, as well as images for\u00a0<\/span><a href=\"https:\/\/github.com\/Microsoft\/dotnet-framework-docker\"><span data-contrast=\"none\">.NET Framework<\/span><\/a><span data-contrast=\"none\">.<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">This is starting to look a little daunting.<\/span><span data-contrast=\"none\">\u00a0 Let\u2019s dive into what goes into producing the .NET Docker images.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">To keep things \u201csimple\u201d, let\u2019s just consider the Docker images for\u00a0<\/span><span data-contrast=\"none\">.NET Core<\/span><span data-contrast=\"none\">.\u00a0\u00a0<\/span><span data-contrast=\"none\">The same infrastructure is used amongst all the types of images we produce<\/span><span data-contrast=\"none\">\u00a0but keep in mind that the scope of the work is greater than described here.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">T<\/span><span data-contrast=\"none\">he full set of .NET Core images<\/span><span data-contrast=\"none\">\u00a0are derived from the following matrix<\/span><span data-contrast=\"none\">:<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<ul>\n<li data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"2\" aria-setsize=\"-1\" data-aria-posinset=\"1\" data-aria-level=\"1\"><span data-contrast=\"none\">Linux: 3 distros \/ 7 versions<\/span><span data-ccp-props=\"{&quot;134233279&quot;:true,&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/li>\n<li data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"2\" aria-setsize=\"-1\" data-aria-posinset=\"2\" data-aria-level=\"1\"><span data-contrast=\"none\">Windows: 4 versions<\/span><span data-ccp-props=\"{&quot;134233279&quot;:true,&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/li>\n<li data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"2\" aria-setsize=\"-1\" data-aria-posinset=\"3\" data-aria-level=\"1\"><span data-contrast=\"none\">Architectures: AMD64, ARM32, ARM64<\/span><span data-ccp-props=\"{&quot;134233279&quot;:true,&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/li>\n<li data-leveltext=\"\uf0b7\" data-font=\"Symbol\" data-listid=\"2\" aria-setsize=\"-1\" data-aria-posinset=\"4\" data-aria-level=\"1\"><span data-contrast=\"none\">.NET Core: 3 versions<\/span><span data-ccp-props=\"{&quot;134233279&quot;:true,&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/li>\n<\/ul>\n<p><span data-contrast=\"none\">In total,\u00a0<\/span><span data-contrast=\"none\">119 distinct images with\u00a0<\/span><span data-contrast=\"none\">309\u00a0<\/span><span data-contrast=\"none\">tags<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">(281 simple and 28 shared)\u00a0<\/span><span data-contrast=\"none\">are\u00a0<\/span><span data-contrast=\"none\">being\u00a0<\/span><span data-contrast=\"none\">produced<\/span><span data-contrast=\"none\">\u00a0today<\/span><span data-contrast=\"none\">.\u00a0<\/span><span data-contrast=\"none\">This matrix is constantly evolving as new OS and .NET versions are released.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<h2 aria-level=\"2\"><span data-contrast=\"none\">Anatomy of our Pipeline<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559738&quot;:40,&quot;335559739&quot;:0,&quot;335559740&quot;:259}\">\u00a0<\/span><\/h2>\n<p><span data-contrast=\"none\">Our<\/span><span data-contrast=\"none\">\u00a0CI\/CD<\/span><span data-contrast=\"none\">\u00a0pipeline is implemented using\u00a0<\/span><a href=\"https:\/\/azure.microsoft.com\/services\/devops\/pipelines\"><span data-contrast=\"none\">Azure Pipelines<\/span><\/a><span data-contrast=\"none\">\u00a0with the core YAML<\/span><span data-contrast=\"none\">-based\u00a0<\/span><span data-contrast=\"none\">source located\u00a0<\/span><a href=\"https:\/\/github.com\/dotnet\/docker-tools\/tree\/master\/eng\/common\/templates\"><span data-contrast=\"none\">here<\/span><\/a><span data-contrast=\"none\">. It&#8217;s divided into three stages: build, test, and publish. Build and test each run multiple jobs in parallel. This parallelism\u00a0<\/span><span data-contrast=\"none\">dramatically reduces\u00a0<\/span><span data-contrast=\"none\">the\u00a0<\/span><span data-contrast=\"none\">pipeline&#8217;s<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">execution time from start to finish\u00a0<\/span><span data-contrast=\"none\">by an order of magnitude<\/span><span data-contrast=\"none\">\u00a0versus running the jobs sequentially<\/span><span data-contrast=\"none\">.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<h3 aria-level=\"3\"><span style=\"font-size: 14pt;\"><span style=\"font-size: 18pt;\">Build Agents<\/span>\u00a0<\/span><\/h3>\n<p><span data-contrast=\"none\">Since we&#8217;ve got jobs running in parallel, we also need\u00a0a number of\u00a0build agents that can fulfill the execution of those jobs.\u00a0<\/span><span data-contrast=\"none\">T<\/span><span data-contrast=\"none\">here is a\u00a0<\/span><span data-contrast=\"none\">self-hosted\u00a0<\/span><span data-contrast=\"none\">agent pool<\/span><span data-contrast=\"none\">\u00a0that we use\u00a0<\/span><span data-contrast=\"none\">for<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">producing\u00a0<\/span><span data-contrast=\"none\">.NET\u00a0<\/span><span data-contrast=\"none\">images\u00a0<\/span><span data-contrast=\"none\">which\u00a0<\/span><span data-contrast=\"none\">consists of a variety of\u00a0<\/span><span data-contrast=\"none\">virtual machines\u00a0<\/span><span data-contrast=\"none\">and physical hardware to meet our<\/span><span data-contrast=\"none\">\u00a0platform and<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">perf demands<\/span><span data-contrast=\"none\">.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">For Linux AMD64 builds,\u00a0<\/span><span data-contrast=\"none\">we use the Hosted Ubuntu 1604 pool provided by Azure DevOp<\/span><span data-contrast=\"none\">s. That pool meets our performance needs and makes things simple from an operations standpoint.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">For Windows AMD64 builds, we have\u00a0<\/span><span data-contrast=\"none\">custom Azure VMs configured as Azure Pipeline<\/span><span data-contrast=\"none\">\u00a0self-hosted<\/span><span data-contrast=\"none\">\u00a0agents that are<\/span><span data-contrast=\"none\">\u00a0running four different Windows versions (five agents for each version).<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">For\u00a0<\/span><span data-contrast=\"none\">ARM builds<\/span><span data-contrast=\"none\">, things get a bit\u00a0<\/span><span data-contrast=\"none\">trickier<\/span><span data-contrast=\"none\">.\u00a0<\/span><span data-contrast=\"none\">We need to build and test the Docker images on ARM-<\/span><span data-contrast=\"none\">based\u00a0<\/span><span data-contrast=\"none\">hardware.\u00a0\u00a0<\/span><span data-contrast=\"none\">Since the\u00a0<\/span><span data-contrast=\"none\">Azure Pipeline agent software<\/span><span data-contrast=\"none\">\u2019s support for ARM is limited to Linux\/ARM32, we<\/span><span data-contrast=\"none\">\u00a0use AMD-based Linux machines as the agents\u00a0<\/span><span data-contrast=\"none\">that send commands to\u00a0<\/span><span data-contrast=\"none\">remote\u00a0<\/span><span data-contrast=\"none\">Linux<\/span><span data-contrast=\"none\">\u00a0and Windows ARM devices.<\/span><span data-contrast=\"none\">\u00a0\u00a0<\/span><span data-contrast=\"none\">Each of those devices runs a Docker daemon<\/span><span data-contrast=\"none\">.\u00a0 The agent machines act as proxies to send Docker commands to the\u00a0<\/span><span data-contrast=\"none\">remote\u00a0<\/span><span data-contrast=\"none\">daemon<\/span><span data-contrast=\"none\">s<\/span><span data-contrast=\"none\">\u00a0running on the\u00a0<\/span><span data-contrast=\"none\">ARM\u00a0<\/span><span data-contrast=\"none\">device<\/span><span data-contrast=\"none\">s<\/span><span data-contrast=\"none\">.<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">For Linux, we use\u00a0<\/span><a href=\"https:\/\/www.nvidia.com\/en-us\/autonomous-machines\/embedded-systems\"><span data-contrast=\"none\">NVIDA Jetson<\/span><\/a><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">devices that run on the AArch64 architecture which\u00a0<\/span><span data-contrast=\"none\">are<\/span><span data-contrast=\"none\">\u00a0capable of building images that target\u00a0<\/span><span data-contrast=\"none\">either ARM32 or ARM64<\/span><span data-contrast=\"none\">.<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">For Windows,\u00a0<\/span><span data-contrast=\"none\">we have<\/span><span data-contrast=\"none\">\u00a0<\/span><a href=\"https:\/\/www.solid-run.com\/nxp-family\/hummingboard\/\"><span data-contrast=\"none\">SolidRun<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">HummingBoard<\/span><\/a><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">ARM device<\/span><span data-contrast=\"none\">s.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<h3 aria-level=\"3\"><span style=\"font-size: 18pt;\">Image\u00a0Matrix Generation\u00a0<\/span><\/h3>\n<p><span data-contrast=\"none\">One of the key features of\u00a0<\/span><a href=\"https:\/\/azure.microsoft.com\/services\/devops\/pipelines\"><span data-contrast=\"none\">Azure Pipelines<\/span><\/a><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">that we\u00a0<\/span><span data-contrast=\"none\">rely on\u00a0<\/span><span data-contrast=\"none\">is\u00a0<\/span><span data-contrast=\"none\">the\u00a0<\/span><a href=\"https:\/\/docs.microsoft.com\/azure\/devops\/pipelines\/yaml-schema?view=azure-devops&amp;tabs=schema#matrix\"><span data-contrast=\"none\">matrix<\/span><\/a><span data-contrast=\"none\">\u00a0strategy for build jobs.\u00a0<\/span><span data-contrast=\"none\">It\u00a0<\/span><span data-contrast=\"none\">allows a variable number of build jobs to be generated based on a<\/span><span data-contrast=\"none\">n image\u00a0<\/span><span data-contrast=\"none\">matrix that is defined by our pipeline.\u00a0 An\u00a0<\/span><span data-contrast=\"none\">illustration<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">of\u00a0<\/span><span data-contrast=\"none\">a very simplified\u00a0<\/span><span data-contrast=\"none\">matrix is the following<\/span><span data-contrast=\"none\">\u00a0YAML<\/span><span data-contrast=\"none\">:<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">3.0-runtime-deps-disco-graph:<\/span>\n<span data-contrast=\"auto\">\u00a0\u00a0imageBuilderPaths: 3.0\/runtime-deps\/disco<\/span><span data-contrast=\"auto\">\u00a0<\/span><span data-contrast=\"auto\">3.0\/runtime\/disco<\/span><span data-contrast=\"auto\">\u00a0<\/span><span data-contrast=\"auto\">3.0\/aspnet\/disco<\/span>\n<span data-contrast=\"auto\">\u00a0\u00a0osType:\u00a0linux<\/span>\n<span data-contrast=\"auto\">\u00a0 architecture: amd64<\/span>\n<span data-contrast=\"auto\">3.0-sdk-disco:<\/span>\n<span data-contrast=\"auto\">\u00a0\u00a0imageBuilderPaths: &#8211;path 3.0\/sdk\/disco<\/span>\n<span data-contrast=\"auto\">\u00a0\u00a0osType:\u00a0linux<\/span>\n<span data-contrast=\"auto\">\u00a0 architecture: amd64<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">This matrix<\/span><span data-contrast=\"none\">\u00a0would cause two\u00a0<\/span><span data-contrast=\"none\">build jobs to execute in parallel, each running the same set of steps but with different inputs.\u00a0\u00a0<\/span><span data-contrast=\"none\">The inputs consist of variables defined by the matrix.\u00a0\u00a0<\/span><span data-contrast=\"none\">The first job<\/span><span data-contrast=\"none\">,<\/span><span data-contrast=\"none\">\u00a0as identified by\u00a0<\/span><span data-contrast=\"auto\">3.0-runtime-deps-disco-graph<\/span><span data-contrast=\"none\">,\u00a0<\/span><span data-contrast=\"none\">has a variable named\u00a0<\/span><span data-contrast=\"auto\">imageBuilderPaths<\/span><span data-contrast=\"none\">\u00a0that indicates to the build steps that<\/span><span data-contrast=\"none\">\u00a0the<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">.<\/span><span data-contrast=\"none\">NET Core 3.0\u00a0<\/span><span data-contrast=\"none\">Docker images f<\/span><span data-contrast=\"none\">or\u00a0<\/span><span data-contrast=\"auto\">runtime-deps<\/span><span data-contrast=\"none\">,\u00a0<\/span><span data-contrast=\"auto\">runtime<\/span><span data-contrast=\"none\">, and\u00a0<\/span><span data-contrast=\"auto\">aspnet<\/span><span data-contrast=\"none\">\u00a0on Ubuntu Disco<\/span><span data-contrast=\"none\">\u00a0are to be built<\/span><span data-contrast=\"none\">.<\/span><span data-contrast=\"none\">\u00a0 The reason those\u00a0<\/span><span data-contrast=\"none\">images are built in a single job is because the<\/span><span data-contrast=\"none\">r<\/span><span data-contrast=\"none\">e<\/span><span data-contrast=\"none\">\u00a0are<\/span><span data-contrast=\"none\">\u00a0dependencies amongst them.\u00a0\u00a0<\/span><span data-contrast=\"none\">The\u00a0<\/span><span data-contrast=\"auto\">runtime<\/span><span data-contrast=\"none\">\u00a0image depends on\u00a0<\/span><span data-contrast=\"auto\">runtime-deps<\/span><span data-contrast=\"none\">\u00a0and the\u00a0<\/span><span data-contrast=\"auto\">aspnet<\/span><span data-contrast=\"none\">\u00a0image depends on the\u00a0<\/span><span data-contrast=\"auto\">runtime<\/span><span data-contrast=\"none\">\u00a0image; there\u2019s no parallelism that can be done within this graph.\u00a0 The\u00a0<\/span><span data-contrast=\"auto\">sdk<\/span><span data-contrast=\"none\">\u00a0image, however, can be built in parallel with the others because it doesn\u2019t depend on them; it depends on\u00a0<\/span><a href=\"https:\/\/github.com\/docker-library\/buildpack-deps\/blob\/master\/disco\/scm\/Dockerfile\"><span data-contrast=\"none\">buildpa<\/span><span data-contrast=\"none\">ck-deps:disco-scm<\/span><\/a><span data-contrast=\"none\">, an\u00a0<\/span><span data-contrast=\"none\">official Docker\u00a0image<\/span><span data-contrast=\"none\">.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">The goal is to produce a matrix that\u00a0<\/span><span data-contrast=\"none\">splits things apart s<\/span><span data-contrast=\"none\">uch that operations are executed in\u00a0<\/span><span data-contrast=\"none\">parallel whenever possible.\u00a0 You<\/span><span data-contrast=\"none\">\u00a0might<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">be\u00a0<\/span><span data-contrast=\"none\">thinking th<\/span><span data-contrast=\"none\">at such a matrix has got to be a real headache to maintain.<\/span><span data-contrast=\"none\">\u00a0 And you\u2019d be right.\u00a0 That\u2019s why we don\u2019t maintain a statically defined matrix.\u00a0 It\u2019s generated for us\u00a0<\/span><span data-contrast=\"none\">dynamically at build time\u00a0<\/span><span data-contrast=\"none\">by a multi-purpo<\/span><span data-contrast=\"none\">se\u00a0<\/span><span data-contrast=\"none\">tool<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">we\u2019ve created\u00a0<\/span><span data-contrast=\"none\">called\u00a0<\/span><a href=\"https:\/\/github.com\/dotnet\/docker-tools\"><span data-contrast=\"none\">Image Builder<\/span><\/a><span data-contrast=\"none\">.<\/span><span data-contrast=\"none\">\u00a0With this\u00a0<\/span><span data-contrast=\"none\">tool<\/span><span data-contrast=\"none\">,\u00a0<\/span><span data-contrast=\"none\">we can execute a command that will consume a\u00a0<\/span><span data-contrast=\"none\">custom\u00a0<\/span><span data-contrast=\"none\">manifest file and outputs a matrix\u00a0<\/span><span data-contrast=\"none\">that is consumed by\u00a0<\/span><a href=\"https:\/\/azure.microsoft.com\/services\/devops\/pipelines\"><span data-contrast=\"none\">Azure Pipelines<\/span><\/a><span data-contrast=\"none\">.\u00a0 The\u00a0<\/span><a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/master\/manifest.json\"><span data-contrast=\"none\">manifest file<\/span><\/a><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">contains\u00a0<\/span><span data-contrast=\"none\">a bunch of metadata about all the images we need to produce\u00a0<\/span><span data-contrast=\"none\">and includes information like the file paths to the\u00a0<\/span><span data-contrast=\"none\">Dockerfiles<\/span><span data-contrast=\"none\">\u00a0and the tags to be assigned to the images.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">We don\u2019t just generate one matrix either.\u00a0 Separate matrices are generated based on the\u00a0<\/span><span data-contrast=\"none\">platform and architecture.\u00a0 For example, there are separate matrices for Linux<\/span><span data-contrast=\"none\">\/<\/span><span data-contrast=\"none\">AMD64, Linux<\/span><span data-contrast=\"none\">\/<\/span><span data-contrast=\"none\">ARM32,\u00a0<\/span><span data-contrast=\"none\">Windows Nano Server 1809<\/span><span data-contrast=\"none\">\/<\/span><span data-contrast=\"none\">ARM32, etc.\u00a0 The output from Image Builder labels each matrix with its corresponding platform<\/span><span data-contrast=\"none\">\/architecture identifier.\u00a0 This identifier determines which build agents will run that\u00a0<\/span><span data-contrast=\"none\">particular matrix<\/span><span data-contrast=\"none\">.<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">As an example, t<\/span><span data-contrast=\"none\">he pipeline is configured to run\u00a0<\/span><span data-contrast=\"none\">the Linux\/AMD<\/span><span data-contrast=\"none\">64 matrix on the Hosted Ubuntu 1604 agent pool.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<h3 aria-level=\"3\"><span data-contrast=\"none\">Build Stage<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559738&quot;:40,&quot;335559739&quot;:0,&quot;335559740&quot;:259}\">\u00a0<\/span><\/h3>\n<p><span data-contrast=\"none\">The\u00a0<\/span><span data-contrast=\"none\">build stage<\/span><span data-contrast=\"none\">\u00a0of the pipeline<\/span><span data-contrast=\"none\">\u00a0is responsible for\u00a0<\/span><span data-contrast=\"none\">build<\/span><span data-contrast=\"none\">ing<\/span><span data-contrast=\"none\">\u00a0the<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">Docker images<\/span><span data-contrast=\"none\">.\u00a0<\/span><span data-contrast=\"none\">T<\/span><span data-contrast=\"none\">here are 64 jobs that are executed which account for the different platform and product version combinations a<\/span><span data-contrast=\"none\">s well as<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">image\u00a0<\/span><span data-contrast=\"none\">dependencies<\/span><span data-contrast=\"none\">.\u00a0<\/span><span data-contrast=\"none\">Examples of job names include \u201cBuild-2.2-aspnet-Windows-NanoServer1809-AMD64\u201d, \u201c<\/span><span data-contrast=\"none\">Build<\/span><span data-contrast=\"none\">-2.<\/span><span data-contrast=\"none\">1-runtime-deps-graph<\/span><span data-contrast=\"none\">-Linux-<\/span><span data-contrast=\"none\">bionic<\/span><span data-contrast=\"none\">-A<\/span><span data-contrast=\"none\">RM32v7<\/span><span data-contrast=\"none\">\u201d, and \u201c<\/span><span data-contrast=\"none\">Build<\/span><span data-contrast=\"none\">-3.0<\/span><span data-contrast=\"none\">-sdk<\/span><span data-contrast=\"none\">-Linux-bionic-A<\/span><span data-contrast=\"none\">MD64<\/span><span data-contrast=\"none\">\u201d.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">The first step of this process is to call\u00a0<\/span><a href=\"https:\/\/github.com\/dotnet\/docker-tools\"><span data-contrast=\"none\">Image Builder<\/span><\/a><span data-contrast=\"none\">\u00a0to generate the build matri<\/span><span data-contrast=\"none\">ces<\/span><span data-contrast=\"none\">.\u00a0\u00a0<\/span><span data-contrast=\"none\">Each matrix produces a set of jobs that build the set of Docker images as described by their portion of the matrix.\u00a0\u00a0<\/span><span data-contrast=\"none\">Remember the\u00a0<\/span><span data-contrast=\"auto\">imageBuilderPaths<\/span><span data-contrast=\"none\">\u00a0variable contained in the matrix example mentioned earlier? This value is fed into Image Builder<\/span><span data-contrast=\"none\">\u00a0so that it knows which Docker images it should b<\/span><span data-contrast=\"none\">uild<\/span><span data-contrast=\"none\">.\u00a0<\/span><span data-contrast=\"none\">It also uses the metadata in the\u00a0<\/span><a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/master\/manifest.json\"><span data-contrast=\"none\">manifest file<\/span><\/a><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">to know which tags should be defined for these images.<\/span><span data-contrast=\"none\">\u00a0This includes the\u00a0<\/span><span data-contrast=\"none\">definition\u00a0<\/span><span data-contrast=\"none\">of simple tags<\/span><span data-contrast=\"none\">\u00a0(<\/span><span data-contrast=\"none\">platform-specific and<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">map to a single image<\/span><span data-contrast=\"none\">)<\/span><span data-contrast=\"none\">\u00a0and shared tags<\/span><span data-contrast=\"none\">\u00a0(<\/span><span data-contrast=\"none\">not platform-specific and\u00a0<\/span><span data-contrast=\"none\">can map to multiple images<\/span><span data-contrast=\"none\">)<\/span><span data-contrast=\"none\">.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">Because a build agent begins a job in a clean state<\/span><span data-contrast=\"none\">\u00a0and has no state from its previous run<\/span><span data-contrast=\"none\">, there needs to be an external storage mechanism for the Docker images that are produced<\/span><span data-contrast=\"none\">.\u00a0 For that reason,\u00a0<\/span><span data-contrast=\"none\">each job pushe<\/span><span data-contrast=\"none\">s the images it has built<\/span><span data-contrast=\"none\">\u00a0to\u00a0<\/span><span data-contrast=\"none\">a<\/span><span data-contrast=\"none\">\u00a0staging location in an<\/span><span data-contrast=\"none\">\u00a0<\/span><a href=\"https:\/\/azure.microsoft.com\/services\/container-registry\/\"><span data-contrast=\"none\">Azure Container Registry (ACR)<\/span><\/a><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">so<\/span><span data-contrast=\"none\">\u00a0they can\u00a0<\/span><span data-contrast=\"none\">later be pulled\u00a0<\/span><span data-contrast=\"none\">by the agents running\u00a0<\/span><span data-contrast=\"none\">in\u00a0<\/span><span data-contrast=\"none\">the test stage<\/span><span data-contrast=\"none\">\u00a0and eventually published<\/span><span data-contrast=\"none\">.<\/span><span data-contrast=\"none\">\u00a0\u00a0<\/span><span data-contrast=\"none\">In some cases, a given image may be used by\u00a0<\/span><span data-contrast=\"none\">multiple test jobs so having it available\u00a0<\/span><span data-contrast=\"none\">to be pulled from an external source is necessary.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<h3 aria-level=\"3\"><span data-contrast=\"none\">Test Stage<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559738&quot;:40,&quot;335559739&quot;:0,&quot;335559740&quot;:259}\">\u00a0<\/span><\/h3>\n<p><span data-contrast=\"none\">Now that\u00a0<\/span><span data-contrast=\"none\">all<\/span><span data-contrast=\"none\">\u00a0the images have been built it&#8217;s time to test them. This is done with a set of smoke tests that verify the basics, such as being able to create and build a project with the SDK image and run it\u00a0<\/span><span data-contrast=\"none\">with<\/span><span data-contrast=\"none\">\u00a0the runtime image.\u00a0<\/span><span data-contrast=\"none\">Even though these tests are very basic, they have sometimes caught product issues and enabled us to halt publishing a .NET Core update<\/span><span data-contrast=\"none\">.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">Like<\/span><span data-contrast=\"none\">\u00a0the build stage, the test stage is split into a set of\u00a0<\/span><span data-contrast=\"none\">34 jobs that run in parallel<\/span><span data-contrast=\"none\">.\u00a0<\/span><span data-contrast=\"none\">Each test job is responsible for testing a\u00a0<\/span><span data-contrast=\"none\">specific<\/span><span data-contrast=\"none\">\u00a0.NET Core version\u00a0<\/span><span data-contrast=\"none\">on a specific operating system version on a specific architecture.\u00a0 Examples of job names include\u00a0<\/span><span data-contrast=\"none\">\u201c<\/span><span data-contrast=\"none\">Test-<\/span><span data-contrast=\"none\">2.1-Windows-NanoServer1809-AMD64\u201d,\u00a0<\/span><span data-contrast=\"none\">\u201c<\/span><span data-contrast=\"none\">Test-<\/span><span data-contrast=\"none\">2.2-<\/span><span data-contrast=\"none\">Linux-<\/span><span data-contrast=\"none\">alpine3.9-AMD64<\/span><span data-contrast=\"none\">\u201d<\/span><span data-contrast=\"none\">,\u00a0<\/span><span data-contrast=\"none\">and\u00a0<\/span><span data-contrast=\"none\">\u201c<\/span><span data-contrast=\"none\">Test-<\/span><span data-contrast=\"none\">3.0-<\/span><span data-contrast=\"none\">Linux-<\/span><span data-contrast=\"none\">bionic<\/span><span data-contrast=\"none\">-ARM64v8<\/span><span data-contrast=\"none\">\u201d<\/span><span data-contrast=\"none\">.<\/span><span data-contrast=\"none\">\u00a0\u00a0<\/span><span data-contrast=\"none\">Notice that<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">the\u00a0<\/span><span data-contrast=\"none\">breakdown\u00a0<\/span><span data-contrast=\"none\">of jobs\u00a0<\/span><span data-contrast=\"none\">is different<\/span><span data-contrast=\"none\">\u00a0compared to the build stage\u00a0<\/span><span data-contrast=\"none\">as the tests have dependencies on images that are different than the build jobs. For example, even though an SDK image might be able to be built independently of the runtime image, both images are needed together in order to test them<\/span><span data-contrast=\"none\">\u00a0because of how our test scenarios are authored<\/span><span data-contrast=\"none\">.\u00a0<\/span><span data-contrast=\"none\">T<\/span><span data-contrast=\"none\">here are not separate jobs that test just the runtime image and just the SDK image; rather, there is one job that tests them both for a given platform\/architecture\/.NET version. That means each test job selectively pulls down only the images it requires from\u00a0<\/span><span data-contrast=\"none\">the staging location in\u00a0<\/span><span data-contrast=\"none\">ACR.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<h3 aria-level=\"3\"><span data-contrast=\"none\">Publish Stage<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559738&quot;:40,&quot;335559739&quot;:0,&quot;335559740&quot;:259}\">\u00a0<\/span><\/h3>\n<p><span data-contrast=\"none\">Once it&#8217;s known that\u00a0<\/span><span data-contrast=\"none\">all\u00a0<\/span><span data-contrast=\"none\">the images are in a good state from the test stage, we can move on to publishing them to<\/span><span data-contrast=\"none\">\u00a0<\/span><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/net-core-container-images-now-published-to-microsoft-container-registry\/\"><span data-contrast=\"none\">Microsoft Container Registry (MCR)<\/span><\/a><span data-contrast=\"none\">. Publishing runs relatively quickly (<\/span><span data-contrast=\"none\">the entire<\/span><span data-contrast=\"none\">\u00a0stage only takes about 3 minutes) because the images are\u00a0<\/span><span data-contrast=\"none\">efficiently\u00a0<\/span><span data-contrast=\"none\">transferred<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">from\u00a0<\/span><span data-contrast=\"none\">ACR\u00a0<\/span><span data-contrast=\"none\">to MCR within shared Azure infrastructure<\/span><span data-contrast=\"none\">. MCR detects th<\/span><span data-contrast=\"none\">is transfer<\/span><span data-contrast=\"none\">\u00a0and makes\u00a0<\/span><span data-contrast=\"none\">the<\/span><span data-contrast=\"none\">\u00a0images<\/span><span data-contrast=\"none\">\u00a0available<\/span><span data-contrast=\"none\">\u00a0for public consumption<\/span><span data-contrast=\"none\">.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">Included with publishing the images are a\u00a0<\/span><span data-contrast=\"none\">few other supplemental\u00a0<\/span><span data-contrast=\"none\">steps<\/span><span data-contrast=\"none\">.\u00a0 The first is<\/span><span data-contrast=\"none\">\u00a0to publish the\u00a0<\/span><a href=\"https:\/\/blog.docker.com\/2017\/11\/multi-arch-all-the-things\/\"><span data-contrast=\"none\">image manifests<\/span><\/a><span data-contrast=\"none\">\u00a0to support multi-arch<\/span><span data-contrast=\"none\">\u00a0<\/span><span data-contrast=\"none\">using the Docker\u00a0<\/span><a href=\"https:\/\/github.com\/estesp\/manifest-tool\"><span data-contrast=\"none\">manifest tool<\/span><\/a><span data-contrast=\"none\">.\u00a0\u00a0<\/span><span data-contrast=\"none\">Next, the<\/span><span data-contrast=\"none\">\u00a0README files on\u00a0<\/span><a href=\"https:\/\/hub.docker.com\/\"><span data-contrast=\"none\">Docker Hub<\/span><\/a><span data-contrast=\"none\">\u00a0are update<\/span><span data-contrast=\"none\">d to reflect the latest content from the repo\u2019s README files.\u00a0 Lastly, a<\/span><span data-contrast=\"none\">\u00a0JSON<\/span><span data-contrast=\"none\">\u00a0file\u00a0<\/span><span data-contrast=\"none\">is updated that keeps track of\u00a0<\/span><span data-contrast=\"none\">metadata about the latest images that have been published.\u00a0 This file serves several purposes<\/span><span data-contrast=\"none\">,\u00a0<\/span><span data-contrast=\"none\">one of\u00a0<\/span><span data-contrast=\"none\">which\u00a0<\/span><span data-contrast=\"none\">is\u00a0<\/span><span data-contrast=\"none\">to provide a way to determine when we need to\u00a0<\/span><span data-contrast=\"none\">re-build an image due\u00a0<\/span><span data-contrast=\"none\">to\u00a0<\/span><span data-contrast=\"none\">it<\/span><span data-contrast=\"none\">s base image being updated.\u00a0 More on th<\/span><span data-contrast=\"none\">at<\/span><span data-contrast=\"none\">\u00a0in a future blog post.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<h2 aria-level=\"2\"><span data-contrast=\"none\">Conclusion<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559738&quot;:40,&quot;335559739&quot;:0,&quot;335559740&quot;:259}\">\u00a0<\/span><\/h2>\n<p><span data-contrast=\"none\">It is a testament to the power and flexibility of Azure Pipelines\u00a0<\/span><span data-contrast=\"none\">to\u00a0<\/span><span data-contrast=\"none\">enable us\u00a0<\/span><span data-contrast=\"none\">to produce Docker images at\u00a0<\/span><span data-contrast=\"none\">the scale and breadth of platforms that\u00a0<\/span><span data-contrast=\"none\">we require<\/span><span data-contrast=\"none\">.\u00a0<\/span><span data-contrast=\"none\">If you\u2019re interested<\/span><span data-contrast=\"none\">\u00a0in the nitty-gritty details<\/span><span data-contrast=\"none\">, check out\u00a0<\/span><span data-contrast=\"none\">our<\/span><span data-contrast=\"none\">\u00a0<\/span><a href=\"https:\/\/github.com\/dotnet\/docker-tools\/tree\/master\/eng\/common\/templates\"><span data-contrast=\"none\">pipeline infrastructure<\/span><\/a><span data-contrast=\"none\">.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">What are the systems that you have in place for producing your organization\u2019s Docker image<\/span><span data-contrast=\"none\">s<\/span><span data-contrast=\"none\">?\u00a0 Did this post spark any ideas on changes you could make to your\u00a0<\/span><span data-contrast=\"none\">process?\u00a0 Let us know in the comments.<\/span><span data-contrast=\"none\">\u00a0And if you\u2019re a consumer of our Docker images, let us know how we\u2019re doing either\u00a0<\/span><span data-contrast=\"none\">in the comments\u00a0<\/span><span data-contrast=\"none\">or at\u00a0<\/span><span data-contrast=\"none\">our<\/span><span data-contrast=\"none\">\u00a0<\/span><a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\"><span data-contrast=\"none\">GitHub repo<\/span><\/a><span data-contrast=\"none\">.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"none\">Happy containerizing!<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:160,&quot;335559740&quot;:259}\">\u00a0<\/span><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Producing\u00a0Docker images for .NET\u00a0might not seem\u00a0like that big of a deal.\u00a0 Once you\u2019ve got a\u00a0Dockerfile\u00a0defined, just run\u00a0&#8220;docker build&#8220;\u00a0and\u00a0&#8220;docker\u00a0push&#8220;\u00a0and you\u2019re done, right?\u00a0\u00a0Then\u00a0just rinse and repeat\u00a0when\u00a0new versions of .NET\u00a0are released\u00a0and that should be all that\u2019s needed.\u00a0 Well, it\u2019s not quite that simple.\u00a0 When you factor in\u00a0the\u00a0number\u00a0of Linux distros\u00a0and Windows versions, different processor architectures,\u00a0and\u00a0different .NET versions, you end [&hellip;]<\/p>\n","protected":false},"author":7041,"featured_media":58792,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685],"tags":[4],"class_list":["post-24305","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","tag-net"],"acf":[],"blog_post_summary":"<p>Producing\u00a0Docker images for .NET\u00a0might not seem\u00a0like that big of a deal.\u00a0 Once you\u2019ve got a\u00a0Dockerfile\u00a0defined, just run\u00a0&#8220;docker build&#8220;\u00a0and\u00a0&#8220;docker\u00a0push&#8220;\u00a0and you\u2019re done, right?\u00a0\u00a0Then\u00a0just rinse and repeat\u00a0when\u00a0new versions of .NET\u00a0are released\u00a0and that should be all that\u2019s needed.\u00a0 Well, it\u2019s not quite that simple.\u00a0 When you factor in\u00a0the\u00a0number\u00a0of Linux distros\u00a0and Windows versions, different processor architectures,\u00a0and\u00a0different .NET versions, you end [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/24305","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/7041"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=24305"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/24305\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/58792"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=24305"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=24305"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=24305"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}