diff --git a/Examples.sln b/Examples.sln index 837f53dc2..362b26053 100644 --- a/Examples.sln +++ b/Examples.sln @@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digitalocean.LoadbalancedDr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digitalocean.K8s", "digitalocean-cs-k8s\Digitalocean.K8s.csproj", "{CA1DB6A6-4B5D-41D7-AED6-43C096F5C7F4}" EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Azure.Aci", "azure-fs-aci\Azure.Aci.fsproj", "{9F646ECF-9CD3-4686-8C4C-FE6AAAD29FEC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -81,6 +83,10 @@ Global {CA1DB6A6-4B5D-41D7-AED6-43C096F5C7F4}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA1DB6A6-4B5D-41D7-AED6-43C096F5C7F4}.Release|Any CPU.ActiveCfg = Release|Any CPU {CA1DB6A6-4B5D-41D7-AED6-43C096F5C7F4}.Release|Any CPU.Build.0 = Release|Any CPU + {9F646ECF-9CD3-4686-8C4C-FE6AAAD29FEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F646ECF-9CD3-4686-8C4C-FE6AAAD29FEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F646ECF-9CD3-4686-8C4C-FE6AAAD29FEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F646ECF-9CD3-4686-8C4C-FE6AAAD29FEC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/azure-cs-botservice/Program.cs b/azure-cs-botservice/Program.cs index 2544066c3..eae0fd624 100644 --- a/azure-cs-botservice/Program.cs +++ b/azure-cs-botservice/Program.cs @@ -57,7 +57,7 @@ static Task Main() Content = new FileArchive("bot/publish") }); - var codeBlobUrl = SharedAccessSignature.SignedBlobReadUrl(blob, storageAccount); + var codeBlobUrl = Storage.SharedAccessSignature.SignedBlobReadUrl(blob, storageAccount); var appInsights = new Insights("ai", new InsightsArgs { diff --git a/azure-cs-cosmosapp-component/Azure.CosmosAppComponent.csproj b/azure-cs-cosmosapp-component/Azure.CosmosAppComponent.csproj index 9415118b1..82bff69ec 100644 --- a/azure-cs-cosmosapp-component/Azure.CosmosAppComponent.csproj +++ b/azure-cs-cosmosapp-component/Azure.CosmosAppComponent.csproj @@ -7,7 +7,8 @@ - + + diff --git a/azure-cs-cosmosapp-component/Containers.cs b/azure-cs-cosmosapp-component/Containers.cs new file mode 100644 index 000000000..725e556e4 --- /dev/null +++ b/azure-cs-cosmosapp-component/Containers.cs @@ -0,0 +1,105 @@ +// Copyright 2016-2018, Pulumi Corporation. All rights reserved. + +using System.Collections.Generic; + +using Pulumi; +using Pulumi.Azure.ContainerService; +using Pulumi.Azure.ContainerService.Inputs; +using Pulumi.Azure.Core; +using Pulumi.Docker; + +public static class Containers +{ + public static IDictionary Run() + { + // Read a list of target locations from the config file: + // Expecting a comma-separated list, e.g., "westus,eastus,westeurope" + var locations = new Pulumi.Config().Require("locations").Split(","); + + var resourceGroup = new ResourceGroup("cosmosaci-rg", new ResourceGroupArgs { Location = locations[0] }); + + var app = new CosmosApp("aci", new CosmosAppArgs + { + ResourceGroup = resourceGroup, + Locations = locations, + DatabaseName = "pricedb", + ContainerName = "prices", + Factory = global => + { + var registry = new Registry("global", new RegistryArgs + { + ResourceGroupName = resourceGroup.Name, + AdminEnabled = true, + Sku = "Premium", + }, global.Options); + + var dockerImage = new Image("node-app", new ImageArgs + { + ImageName = Output.Format($"{registry.LoginServer}/mynodeapp:v1.0.0"), + Build = "./container", + Registry = new ImageRegistry + { + Server = registry.LoginServer, + Username = registry.AdminUsername, + Password = registry.AdminPassword, + }, + }, new ComponentResourceOptions { Parent = registry }); + + return region => + { + var connectionString = global.CosmosAccount.ConnectionStrings.Apply(cs => cs[0]); + var group = new Group($"aci-{region.Location}", new GroupArgs + { + ResourceGroupName = resourceGroup.Name, + Location = region.Location, + ImageRegistryCredentials = + { + new GroupImageRegistryCredentialsArgs + { + Server = registry.LoginServer, + Username = registry.AdminUsername, + Password = registry.AdminPassword, + } + }, + OsType = "Linux", + Containers = + { + new GroupContainersArgs + { + Cpu = 0.5, + Image = dockerImage.ImageName, + Memory = 1.5, + Name = "hello-world", + Ports = + { + new GroupContainersPortsArgs + { + Port = 80, + Protocol = "TCP", + } + }, + EnvironmentVariables = + { + { "ENDPOINT", global.CosmosAccount.Endpoint }, + { "MASTER_KEY", global.CosmosAccount.PrimaryMasterKey }, + { "DATABASE", global.Database.Name }, + { "COLLECTION", global.Container.Name }, + { "LOCATION", region.Location }, + }, + }, + }, + IpAddressType = "public", + DnsNameLabel = $"acishop-{region.Location}", + }, global.Options); + + return new ExternalEndpoint(group.Fqdn); + }; + } + }); + + return new Dictionary + { + { "containersEndpoint", Output.Format($"{app.Endpoint}/cosmos") } + }; + } +} diff --git a/azure-cs-cosmosapp-component/CosmosApp.cs b/azure-cs-cosmosapp-component/CosmosApp.cs index 47383005e..d8a173921 100644 --- a/azure-cs-cosmosapp-component/CosmosApp.cs +++ b/azure-cs-cosmosapp-component/CosmosApp.cs @@ -137,24 +137,18 @@ public CosmosApp(string name, CosmosAppArgs args, ComponentResourceOptions? opti { ResourceGroupName = resourceGroup.Name, TrafficRoutingMethod = "Performance", - DnsConfigs = + DnsConfig = new TrafficManagerProfileDnsConfigArgs { - new TrafficManagerProfileDnsConfigsArgs - { - // Subdomain must be globally unique, so we default it with the full resource group name - RelativeName = Output.Format($"{name}{resourceGroup.Name}"), - Ttl = 60, - } + // Subdomain must be globally unique, so we default it with the full resource group name + RelativeName = Output.Format($"{name}{resourceGroup.Name}"), + Ttl = 60, }, - MonitorConfigs = + MonitorConfig = new TrafficManagerProfileMonitorConfigArgs { - new TrafficManagerProfileMonitorConfigsArgs - { - Protocol = "HTTP", - Port = 80, - Path = "/api/ping", - } - }, + Protocol = "HTTP", + Port = 80, + Path = "/api/ping", + } }, parentOptions); diff --git a/azure-cs-cosmosapp-component/Program.cs b/azure-cs-cosmosapp-component/Program.cs index 44e44281a..f1e428ea5 100644 --- a/azure-cs-cosmosapp-component/Program.cs +++ b/azure-cs-cosmosapp-component/Program.cs @@ -12,10 +12,12 @@ static Task Main() return Deployment.RunAsync(() => { var functions = Functions.Run(); + var containers = Containers.Run(); var vmss = VmScaleSets.Run(); return new Dictionary { { "functionsEndpoint", functions["functionsEndpoint"] }, + { "containersEndpoint", containers["containersEndpoint"] }, { "vmssEndpoint", vmss["vmssEndpoint"] }, }; }); diff --git a/azure-cs-cosmosapp-component/container/.dockerignore b/azure-cs-cosmosapp-component/container/.dockerignore new file mode 100644 index 000000000..93f136199 --- /dev/null +++ b/azure-cs-cosmosapp-component/container/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log diff --git a/azure-cs-cosmosapp-component/container/Dockerfile b/azure-cs-cosmosapp-component/container/Dockerfile new file mode 100644 index 000000000..53bbea666 --- /dev/null +++ b/azure-cs-cosmosapp-component/container/Dockerfile @@ -0,0 +1,6 @@ +FROM node:8.9.3-alpine +RUN mkdir -p /usr/src/app +COPY ./app/* /usr/src/app/ +WORKDIR /usr/src/app +RUN npm install +CMD node /usr/src/app/index.js diff --git a/azure-cs-cosmosapp-component/container/app/index.html b/azure-cs-cosmosapp-component/container/app/index.html new file mode 100644 index 000000000..d0327a673 --- /dev/null +++ b/azure-cs-cosmosapp-component/container/app/index.html @@ -0,0 +1,33 @@ + + + YEAH Welcome to Azure Container Instances! + + + + + +
+

Welcome to Azure Container Instances!

+ + + ContainerInstances_rgb_UI + + + + + + + + + + +
+ + + diff --git a/azure-cs-cosmosapp-component/container/app/index.js b/azure-cs-cosmosapp-component/container/app/index.js new file mode 100644 index 000000000..974313416 --- /dev/null +++ b/azure-cs-cosmosapp-component/container/app/index.js @@ -0,0 +1,37 @@ +const express = require('express'); +const morgan = require('morgan'); +const cosmos = require('@azure/cosmos'); + +const app = express(); +app.use(morgan('combined')); + + +app.get('/', (req, res) => { + res.sendFile(__dirname + '/index.html') +}); + +app.get('/cosmos', async (req, res) => { + const endpoint = process.env.ENDPOINT; + const key = process.env.MASTER_KEY; + const database = process.env.DATABASE; + const collection = process.env.COLLECTION; + const location = process.env.LOCATION; + + const client = new cosmos.CosmosClient({ endpoint, key, connectionPolicy: { preferredLocations: [location] } }); + const container = client.database(database).container(collection); + const response = await container.item("test", undefined).read(); + + if (response.resource && response.resource.url) { + res.send(response.resource.url); + } else { + res.status(404).end(); + } +}); + +app.get('/api/ping', (req, res) => { + res.send('Ack') +}); + +var listener = app.listen(process.env.PORT || 80, function() { + console.log('listening on port ' + listener.address().port); +}); diff --git a/azure-cs-cosmosapp-component/container/app/package.json b/azure-cs-cosmosapp-component/container/app/package.json new file mode 100644 index 000000000..98393f65a --- /dev/null +++ b/azure-cs-cosmosapp-component/container/app/package.json @@ -0,0 +1,13 @@ +{ + "name": "aci-helloworld", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": { + "@azure/cosmos": "^3.1.1", + "express": "^4.14.0", + "morgan": "^1.8.2" + }, + "devDependencies": {}, + "author": "" +} diff --git a/azure-fs-aci/.gitignore b/azure-fs-aci/.gitignore new file mode 100644 index 000000000..e64527066 --- /dev/null +++ b/azure-fs-aci/.gitignore @@ -0,0 +1,353 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ diff --git a/azure-fs-aci/Azure.Aci.fsproj b/azure-fs-aci/Azure.Aci.fsproj new file mode 100644 index 000000000..60ad319ce --- /dev/null +++ b/azure-fs-aci/Azure.Aci.fsproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + + + + + + diff --git a/azure-fs-aci/Program.fs b/azure-fs-aci/Program.fs new file mode 100644 index 000000000..c00767fd6 --- /dev/null +++ b/azure-fs-aci/Program.fs @@ -0,0 +1,61 @@ +module Program + +open Pulumi +open Pulumi.FSharp +open Pulumi.Azure.ContainerService +open Pulumi.Azure.ContainerService.Inputs +open Pulumi.Azure.Core +open Pulumi.Docker + +[] +module Helpers = + let inputLeft<'a, 'b>(v: 'a) : InputUnion<'a, 'b> = InputUnion.op_Implicit v + +let infra () = + let resourceGroup = ResourceGroup "aci-rg" + + let registry = + Registry("registry", + RegistryArgs + (ResourceGroupName = io resourceGroup.Name, + AdminEnabled = input true, + Sku = input "Premium")) + + let imageName = registry.LoginServer |> Outputs.apply(fun v -> v + "/mynodeapp:v1.0.0") |> io + let dockerImage = + Image("node-app", + ImageArgs + (ImageName = imageName, + Build = inputLeft "./app", + Registry = input( + ImageRegistry + (Server = io registry.LoginServer, + Username = io registry.AdminUsername, + Password = io registry.AdminPassword)))) + + let group = + Group("aci", + GroupArgs + (ResourceGroupName = io resourceGroup.Name, + ImageRegistryCredentials = inputList [input + (GroupImageRegistryCredentialsArgs + (Server = io registry.LoginServer, + Username = io registry.AdminUsername, + Password = io registry.AdminPassword))], + OsType = input "Linux", + Containers = inputList [input + (GroupContainersArgs + (Cpu = input 0.5, + Image = io dockerImage.ImageName, + Memory = input 1.5, + Name = input "hello-world", + Ports = inputList [input (GroupContainersPortsArgs(Port = input 80, Protocol = input "TCP"))] + ))], + IpAddressType = input "public", + DnsNameLabel = input "acifsharp")) + + dict [("endpoint", group.Fqdn :> obj)] + +[] +let main _ = + Deployment.run infra diff --git a/azure-fs-aci/Pulumi.yaml b/azure-fs-aci/Pulumi.yaml new file mode 100644 index 000000000..04aff9767 --- /dev/null +++ b/azure-fs-aci/Pulumi.yaml @@ -0,0 +1,3 @@ +name: azure-fs-aci +description: Deploys a docker container to Azure Container Instances +runtime: dotnet diff --git a/azure-fs-aci/README.md b/azure-fs-aci/README.md new file mode 100644 index 000000000..87b2529ac --- /dev/null +++ b/azure-fs-aci/README.md @@ -0,0 +1,72 @@ +[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new) + +# Custom Docker Image running in Azure Container Instances + +Starting point for building web application hosted in Azure Container Instances. + +## Deploying the App + +To deploy your infrastructure, follow the below steps. + +### Prerequisites + +1. [Install Pulumi](https://www.pulumi.com/docs/get-started/install/) +2. [Install .NET Core 3.0+](https://dotnet.microsoft.com/download) + +### Steps + +1. Create a new stack: + + ``` + $ pulumi stack init dev + ``` + +1. Login to Azure CLI (you will be prompted to do this during deployment if you forget this step): + + ``` + $ az login + ``` + +1. Configure the location to deploy the resources to: + + ``` + $ pulumi config set azure:location + ``` + +1. Run `pulumi up` to preview and deploy changes: + + ``` + $ pulumi up + Previewing changes: + ... + + Performing changes: + ... + info: 55 changes performed: + + 10 resources created + Update duration: 1m56s + ``` + +1. Check the deployed container endpoint: + + ``` + $ pulumi stack output endpoint + https://acifsharp.westeurope.azurecontainer.io + $ curl "$(pulumi stack output endpoint)" + + + Hello, Pulumi! + +

Hello, containers!

+

Made with ❤️ with Pulumi

+ + ``` + +6. From there, feel free to experiment. Simply making edits and running `pulumi up` will incrementally update your stack. + +7. Once you've finished experimenting, tear down your stack's resources by destroying and removing it: + + ```bash + $ pulumi destroy --yes + $ pulumi stack rm --yes + ``` diff --git a/azure-fs-aci/app/Dockerfile b/azure-fs-aci/app/Dockerfile new file mode 100644 index 000000000..4f9d60fb7 --- /dev/null +++ b/azure-fs-aci/app/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx +COPY content /usr/share/nginx/html diff --git a/azure-fs-aci/app/content/favicon.png b/azure-fs-aci/app/content/favicon.png new file mode 100644 index 000000000..ad4baeb6f Binary files /dev/null and b/azure-fs-aci/app/content/favicon.png differ diff --git a/azure-fs-aci/app/content/index.html b/azure-fs-aci/app/content/index.html new file mode 100644 index 000000000..15bfc0d4f --- /dev/null +++ b/azure-fs-aci/app/content/index.html @@ -0,0 +1,7 @@ + + + Hello, Pulumi! + +

Hello, containers!

+

Made with ❤️ with Pulumi

+