diff --git a/README.md b/README.md index 8f1b9b304..facf2df1f 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,7 @@ Cluster. [Azure Container Instance](azure-cs-aci) | Run Azure Container Instances on Linux. [Azure Kubernetes Service](azure-cs-aks) | Create an Azure Kubernetes Service (AKS) Cluster. [AKS web app with .NET 5](azure-cs-net5-aks-webapp) | Create an Azure Kubernetes Service (AKS) cluster and deploy a web app to it using .NET 5 and C# 9. +[AKS + Cosmos DB](azure-cs-aks-cosmos-helm) | A Helm chart deployed to AKS that stores TODOs in an Azure Cosmos DB MongoDB API. [Azure App Service](azure-cs-appservice) | Build a web application hosted in App Service and provision Azure SQL Database and Azure Application Insights. [Azure App Service with Docker](azure-cs-appservice-docker) | Build a web application hosted in App Service from Docker images. [Azure Cosmos DB and LogicApp](azure-cs-cosmosdb-logicapp) | Define Cosmos DB, API connections, and link them to a logic app. diff --git a/azure-cs-aks-cosmos-helm/.gitignore b/azure-cs-aks-cosmos-helm/.gitignore new file mode 100644 index 000000000..e64527066 --- /dev/null +++ b/azure-cs-aks-cosmos-helm/.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-cs-aks-cosmos-helm/AksCluster.cs b/azure-cs-aks-cosmos-helm/AksCluster.cs new file mode 100644 index 000000000..aa40486c6 --- /dev/null +++ b/azure-cs-aks-cosmos-helm/AksCluster.cs @@ -0,0 +1,127 @@ +// Copyright 2016-2021, Pulumi Corporation. All rights reserved. + +using System; +using System.Text; + +using Pulumi; +using Pulumi.AzureAD; +using Pulumi.AzureNative.ContainerService; +using Pulumi.AzureNative.ContainerService.Inputs; +using Pulumi.AzureNative.Resources; +using Pulumi.Random; +using Pulumi.Tls; +using K8s = Pulumi.Kubernetes; + +public class AksCluster : ComponentResource +{ + public Output ClusterName { get; set; } + public Output KubeConfig { get; set; } + public K8s.Provider Provider { get; set; } + + public AksCluster(string name, AksClusterArgs args) + : base("example:component:AksCluster", name) + { + var adApp = new Application("app", new ApplicationArgs + { + DisplayName = "aks-cosmos" + }, new CustomResourceOptions { Parent = this }); + + var adSp = new ServicePrincipal("service-principal", new ServicePrincipalArgs + { + ApplicationId = adApp.ApplicationId + }, new CustomResourceOptions { Parent = this }); + + var pw = new RandomPassword("pw", new RandomPasswordArgs + { + Length = 20, + Special = true + }, new CustomResourceOptions { Parent = this }); + + var adSpPassword = new ServicePrincipalPassword("sp-password", new ServicePrincipalPasswordArgs + { + ServicePrincipalId = adSp.Id, + Value = pw.Result, + EndDate = "2099-01-01T00:00:00Z" + }, new CustomResourceOptions { Parent = this }); + + var keyPair = new PrivateKey("ssh-key", new PrivateKeyArgs + { + Algorithm = "RSA", + RsaBits = 4096 + }, new CustomResourceOptions { Parent = this }); + + var k8sCluster = new ManagedCluster(name, new ManagedClusterArgs + { + ResourceGroupName = args.ResourceGroupName, + AddonProfiles = + { + ["KubeDashboard"] = new ManagedClusterAddonProfileArgs { Enabled = true } + }, + AgentPoolProfiles = + { + new ManagedClusterAgentPoolProfileArgs + { + Count = args.NodeCount, + VmSize = args.NodeSize, + MaxPods = 110, + Mode = "System", + Name = "agentpool", + OsDiskSizeGB = 30, + OsType = "Linux", + Type = "VirtualMachineScaleSets" + } + }, + DnsPrefix = args.ResourceGroupName, + EnableRBAC = true, + KubernetesVersion = args.KubernetesVersion, + LinuxProfile = new ContainerServiceLinuxProfileArgs + { + AdminUsername = "testuser", + Ssh = new ContainerServiceSshConfigurationArgs + { + PublicKeys = new ContainerServiceSshPublicKeyArgs + { + KeyData = keyPair.PublicKeyOpenssh + } + } + }, + NodeResourceGroup = $"{name}-node-rg", + ServicePrincipalProfile = new ManagedClusterServicePrincipalProfileArgs + { + ClientId = adApp.ApplicationId, + Secret = adSpPassword.Value + } + }, new CustomResourceOptions { Parent = this }); + + this.ClusterName = k8sCluster.Name; + + this.KubeConfig = Output.Tuple(k8sCluster.Name, args.ResourceGroupName.ToOutput()) + .Apply(pair => + { + var k8sClusterName = pair.Item1; + var resourceGroupName = pair.Item2; + + return ListManagedClusterUserCredentials.InvokeAsync(new ListManagedClusterUserCredentialsArgs + { + ResourceGroupName = resourceGroupName, + ResourceName = k8sClusterName + }); + }) + .Apply(x => x.Kubeconfigs[0].Value) + .Apply(Convert.FromBase64String) + .Apply(Encoding.UTF8.GetString); + + this.Provider = new K8s.Provider("k8s-provider", new K8s.ProviderArgs + { + KubeConfig = this.KubeConfig + }, new CustomResourceOptions { Parent = this }); + } +} + +public class AksClusterArgs +{ + public Input ResourceGroupName { get; set; } + public string KubernetesVersion { get; set; } + public int NodeCount { get; set; } + public string NodeSize { get; set; } +} diff --git a/azure-cs-aks-cosmos-helm/AksCosmosStack.csproj b/azure-cs-aks-cosmos-helm/AksCosmosStack.csproj new file mode 100644 index 000000000..f77dfe35f --- /dev/null +++ b/azure-cs-aks-cosmos-helm/AksCosmosStack.csproj @@ -0,0 +1,17 @@ + + + + Exe + net5.0 + enable + + + + + + + + + + + diff --git a/azure-cs-aks-cosmos-helm/CosmosDBMongoDB.cs b/azure-cs-aks-cosmos-helm/CosmosDBMongoDB.cs new file mode 100644 index 000000000..8d0c4edad --- /dev/null +++ b/azure-cs-aks-cosmos-helm/CosmosDBMongoDB.cs @@ -0,0 +1,129 @@ +// Copyright 2016-2021, Pulumi Corporation. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Pulumi; +using Pulumi.AzureNative.DocumentDB; +using Pulumi.AzureNative.DocumentDB.Inputs; + +public class CosmosDBMongoDB : ComponentResource +{ + public Output AccountName { get; set; } + + public Output DatabaseName { get; set; } + + public CosmosDBMongoDB(string name, CosmosDBMongoDBArgs args) + : base("example:component:CosmosDBMongoDB", name) + { + var config = new Config("azure-native"); + var location = config.Require("location"); + + var databaseAccount = new DatabaseAccount("cosmos-mongodb", new DatabaseAccountArgs + { + ResourceGroupName = args.ResourceGroupName, + DatabaseAccountOfferType = DatabaseAccountOfferType.Standard, + Kind = DatabaseAccountKind.MongoDB, + ConsistencyPolicy = new ConsistencyPolicyArgs + { + DefaultConsistencyLevel = DefaultConsistencyLevel.BoundedStaleness, + MaxIntervalInSeconds = 10, + MaxStalenessPrefix = 200, + }, + Locations = + { + new LocationArgs + { + FailoverPriority = 0, + LocationName = location + } + } + }, new CustomResourceOptions { Parent = this }); + this.AccountName = databaseAccount.Name; + + var database = new MongoDBResourceMongoDBDatabase(args.DatabaseName, new MongoDBResourceMongoDBDatabaseArgs + { + ResourceGroupName = args.ResourceGroupName, + AccountName = databaseAccount.Name, + Resource = new MongoDBDatabaseResourceArgs { Id = args.DatabaseName } + }, new CustomResourceOptions { Parent = this }); + this.DatabaseName = database.Name; + } + + public static Output> KubernetesSecretData(Output resourceGroupName, Output accountName, Output databaseName) + { + return Output.Tuple(resourceGroupName, accountName, databaseName).Apply(async values => + { + var conn = await ListDatabaseAccountConnectionStrings.InvokeAsync( + new ListDatabaseAccountConnectionStringsArgs + { + ResourceGroupName = values.Item1, + AccountName = values.Item2 + }); + return parseConnString(conn.ConnectionStrings[0].ConnectionString, values.Item3); + } + ); + } + + private static ImmutableDictionary parseConnString(string conn, string database) + { + // Per the official docs[1], the format of this connection string is: + // + // mongodb://username:password@host:port/[database]?ssl=true + // + // For instance: + // + // mongodb://cosmos-mongodb965c15ec:9mrcqzY98o53WiehJ9FZncTS4auyU2BUG6E2Aq9kn8PUi6XsISj6fVhJJXGzfTpZcFGsgIzKw1unveMMQW8Mtw==@cosmos-mongodb965c15ec.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@cosmos-mongodb965c15ec@ + // + // Where these could have the following values: + // + // { + // username: "cosmosdb93a4133a", + // password: "23maXrWsrzZ1LmPe4w6XNGRJJTHsqGZPDTjyVQNbPaw119KCoCNpStH0DQms5MKdyAecisBM9uWbpV7lUnyNeQ==", + // host: "cosmosdb93a4133a.documents.azure.com", + // port: "10255", + // database: "mydatabase" + // } + // + // There are a few subtleties involved in getting the Bitnami node Chart to actually be able to + // use this: + // + // 1. The `database` field is optional, we default to `test`, as the API expects. + // 2. The node Chart expects the components of this connection string to be parsed and + // presented in files in a `Secret`. The CosmosDb API doesn't natively expose this, so we + // must parse it ourselves. + // 3. The node Chart uses mongoose to speak the MongoDB wire protocol to CosmosDB. Mongoose + // fails to parse base64-encoded passwords because it doesn't like the `=` character. This + // means we have to (1) URI-encode the password component ourselves, and (2) base64-encode + // that URI-encoded password, because this is the format Kubernetes expects. + // + // [1]: https://docs.microsoft.com/en-us/azure/cosmos-db/connect-mongodb-account + + var noProtocol = conn.Replace("mongodb://", ""); + var parts = noProtocol.Split(":", 3); + var userName = parts[0]; + var subParts = parts[1].Split("@", 2); + var (password, host) = (subParts[0], subParts[1]); + var port = parts[2].Split("/", 2)[0]; + return new Dictionary + { + {"host", toBase64(host)}, + {"port", toBase64(port)}, + {"username", toBase64(userName)}, + {"password", toBase64(Uri.EscapeDataString(password))}, + {"database", toBase64(database)}, + }.ToImmutableDictionary(); + + static string toBase64(string plainText) + { + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); + return System.Convert.ToBase64String(plainTextBytes); + } + } +} + +public class CosmosDBMongoDBArgs +{ + public Input ResourceGroupName { get; set; } + public string DatabaseName { get; set; } +} diff --git a/azure-cs-aks-cosmos-helm/Program.cs b/azure-cs-aks-cosmos-helm/Program.cs new file mode 100644 index 000000000..d37815b36 --- /dev/null +++ b/azure-cs-aks-cosmos-helm/Program.cs @@ -0,0 +1,63 @@ +// Copyright 2016-2021, Pulumi Corporation. All rights reserved. + +using System.Threading.Tasks; +using Pulumi; +using Pulumi.AzureNative.Resources; +using Pulumi.Kubernetes.Core.V1; +using Pulumi.Kubernetes.Helm; +using Pulumi.Kubernetes.Helm.V3; +using Pulumi.Kubernetes.Types.Inputs.Core.V1; +using Pulumi.Kubernetes.Types.Inputs.Meta.V1; + +await Pulumi.Deployment.RunAsync(); + +class CosmosStack : Stack +{ + public CosmosStack() + { + var resourceGroup = new ResourceGroup("cosmosrg"); + + var mongo = new CosmosDBMongoDB("mongo-todos", new CosmosDBMongoDBArgs + { + ResourceGroupName = resourceGroup.Name, + DatabaseName = "todos" + }); + + var myCluster = new AksCluster("demoaks", new AksClusterArgs + { + ResourceGroupName = resourceGroup.Name, + KubernetesVersion = "1.18.14", + NodeCount = 1, + NodeSize = "Standard_D2_v2" + }); + + var secretName = "mongo-secrets"; + var mongoConnectionSecret = new Secret(secretName, new SecretArgs + { + Metadata = new ObjectMetaArgs {Name = secretName}, + Data = CosmosDBMongoDB.KubernetesSecretData(resourceGroup.Name, mongo.AccountName, mongo.DatabaseName) + }, new CustomResourceOptions {Provider = myCluster.Provider}); + + var chart = new Chart("node", new ChartArgs + { + Chart = "node", + Version = "15.2.3", + FetchOptions = new ChartFetchArgs + { + Repo = "https://charts.bitnami.com/bitnami" + }, + Values = + { + {"service", new {type = "LoadBalancer"}}, + {"mongodb", new {enabled = false}}, + {"externaldb", new {enabled = true, ssl = true, secretName = secretName}} + } + }, new ComponentResourceOptions {Provider = myCluster.Provider}); + + var ip = chart.GetResource("node") + .Apply(svc => svc.Status.Apply(s => s.LoadBalancer.Ingress[0].Ip)); + this.Endpoint = Output.Format($"http://{ip}"); + } + + [Output] public Output Endpoint { get; set; } +} diff --git a/azure-cs-aks-cosmos-helm/Pulumi.yaml b/azure-cs-aks-cosmos-helm/Pulumi.yaml new file mode 100644 index 000000000..c676ed6c7 --- /dev/null +++ b/azure-cs-aks-cosmos-helm/Pulumi.yaml @@ -0,0 +1,3 @@ +name: azure-cs-aks-cosmos-helm +runtime: dotnet +description: A Helm chart deployed to AKS that stores TODOs in an Azure Cosmos DB MongoDB API diff --git a/azure-cs-aks-cosmos-helm/README.md b/azure-cs-aks-cosmos-helm/README.md new file mode 100644 index 000000000..2c2519dc8 --- /dev/null +++ b/azure-cs-aks-cosmos-helm/README.md @@ -0,0 +1,103 @@ +[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new) + +# A Helm chart deployed to AKS that stores TODOs in an Azure Cosmos DB MongoDB API + +Stands up an Azure Kubernetes Service (AKS) cluster and a MongoDB-flavored instance of +Azure Cosmos DB. On top of the AKS cluster, we also deploy a Helm Chart with a simple +Node.js TODO app `bitnami/node`, swapping out the usual in-cluster MongoDB instance +with our managed Cosmos DB instance. + +## Prerequisites + +- Install [Pulumi](https://www.pulumi.com/docs/get-started/install/). + +- Install [.NET 5](https://dotnet.microsoft.com/download) + +- We will be deploying to Azure, so you will need an Azure account. If + you do not have an account, [sign up for free here](https://azure.microsoft.com/en-us/free/). + +- Setup and authenticate the [native Azure provider for Pulumi](https://www.pulumi.com/docs/intro/cloud-providers/azure/setup/). + + +## Running the Example + +In this example we will provision a Kubernetes cluster running a +public Apache web server, verify we can access it, and clean up when +done. + +1. Get the code: + + ```bash + $ git clone git@github.com:pulumi/examples.git + $ cd examples/azure-cs-aks-cosmos-helm + ``` + +2. Create a new stack, which is an isolated deployment target for this example: + + ```bash + $ pulumi stack init + ``` + +3. Set the required configuration variables for this program: + + ```bash + $ pulumi config set azure-native:location westus2 + ``` + +4. Deploy everything with the `pulumi up` command. This provisions + all the Azure resources necessary, including an Active Directory + service principal, AKS cluster, and then deploys the Apache Helm + Chart, all in a single gesture (takes 5-10 min): + + ```bash + $ pulumi up + + Type Name Status Info + + pulumi:pulumi:Stack azure-cs-aks-cosmos-helm-dev created 1 warning + + ├─ kubernetes:helm.sh/v3:Chart node created + + │ ├─ kubernetes:core/v1:Service node created + + │ └─ kubernetes:apps/v1:Deployment node created + + ├─ example:component:CosmosDBMongoDB mongo-todos created + + │ ├─ azure-native:documentdb:DatabaseAccount cosmos-mongodb created + + │ └─ azure-native:documentdb:MongoDBResourceMongoDBDatabase todos created + + ├─ example:component:AksCluster demoaks created + + │ ├─ azuread:index:Application app created + + │ ├─ random:index:RandomPassword pw created + + │ ├─ tls:index:PrivateKey ssh-key created + + │ ├─ azuread:index:ServicePrincipal service-principal created + + │ ├─ azuread:index:ServicePrincipalPassword sp-password created + + │ ├─ azure-native:containerservice:ManagedCluster demoaks created + + │ └─ pulumi:providers:kubernetes k8s-provider created + + ├─ azure-native:resources:ResourceGroup cosmosrg created + + └─ kubernetes:core/v1:Secret mongo-secrets created + + Outputs: + Endpoint: "http://20.73.205.163" + ``` + +5. Now your database, your cluster, and application are ready. An output + variable will be printed to provide the application endpoint. + + ```bash + $ curl $(pulumi stack output Endpoint) + + + + + + + + + + Node/Angular Todo App + ... + ``` + +6. Once you are done, you can destroy all of the resources, and the + stack: + + ```bash + $ pulumi destroy + $ pulumi stack rm + $ rm kubeconfig.yaml + ```