Slide 1

Slide 1 text

Infrastructure as Code Is it really? Provision your cloud with .NET core Shahid Iqbal | Freelance Azure consultant @shahiddev

Slide 2

Slide 2 text

@shahiddev Q&A go to slido.com event #Y860 Photo by Tyler Nix on Unsplash

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

@shahiddev Q&A go to slido.com event #Y860 Agenda Popular common options for provisioning cloud infrastructure Limitations of the common approaches Pulumi Title slide photo by Oscar Nord on Unsplash

Slide 5

Slide 5 text

@shahiddev Q&A go to slido.com event #Y860 Caveats Focus on Azure & .NET but principles are same on other platforms and languages Talk is inspired by my experience dealing with Cloud infrastructure whilst working with smaller teams I don’t work for Pulumi – i.e. not a sales pitch ☺

Slide 6

Slide 6 text

@shahiddev Q&A go to slido.com event #Y860 Who am I Freelance Azure consultant specialising in Azure, Kubernetes & Cloud native technologies. Over a decade of experience as a developer (mostly .NET) Microsoft MVP Co-organise meetup in the UK (Milton Keynes .NET) https://linkedin.shahid.dev [email protected] https://blog.headforcloud.com

Slide 7

Slide 7 text

@shahiddev Q&A go to slido.com event #Y860

Slide 8

Slide 8 text

@shahiddev Q&A go to slido.com event #Y860 Portal pros • Discoverability • Wizards/options • Some people more comfortable with GUIs

Slide 9

Slide 9 text

@shahiddev Q&A go to slido.com event #Y860 Portal cons • Not easy to repeat consistently • Options can be buried deep in submenus • Provisioning lots of resources takes time (not easy to run in parallel) • You have to resolve the resource dependencies

Slide 10

Slide 10 text

@shahiddev Q&A go to slido.com event #Y860 Infrastructure as code (IaC) is the process of managing and provisioning computer data centers through machine-readable definition files, rather than physical hardware configuration or interactive configuration tools. https://en.wikipedia.org/wiki/Infrastructure_as_code

Slide 11

Slide 11 text

@shahiddev Q&A go to slido.com event #Y860 Cloud provider solutions Azure Resource Manager Google Cloud Deployment Manager

Slide 12

Slide 12 text

@shahiddev Q&A go to slido.com event #Y860 { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "storageAccountType": { "type": "string", "defaultValue": "Standard_LRS", "allowedValues": [ "Standard_LRS", "Standard_GRS", "Standard_ZRS", "Premium_LRS" ], "metadata": { "description": "Storage Account type" } }, "location": { "type": "string", "defaultValue": "[resourceGroup().location]", "metadata": { "description": "Location for all resources." } } }, "variables": { "storageAccountName": "[concat('store', uniquestring(resourceGroup().id))]" }, "resources": [ { "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2019-04-01", "name": "[variables('storageAccountName')]", "location": "[parameters('location')]", "sku": { "name": "[parameters('storageAccountType')]" }, "kind": "StorageV2", "properties": {} } ], "outputs": { "storageAccountName": { "type": "string", "value": "[variables('storageAccountName')]" } } }

Slide 13

Slide 13 text

@shahiddev Q&A go to slido.com event #Y860 "parameters": { "storageAccountType": { "type": "string", "defaultValue": "Standard_LRS", "allowedValues": [ "Standard_LRS", "Standard_GRS", "Standard_ZRS", "Premium_LRS" ], "metadata": { "description": "Storage Account type" } }, "location": { "type": "string", "defaultValue": "[resourceGroup().location]", "metadata": { "description": "Location for all resources." } } },

Slide 14

Slide 14 text

@shahiddev Q&A go to slido.com event #Y860 "parameters": { "storageAccountType": { "type": "string", "defaultValue": "Standard_LRS", "allowedValues": [ "Standard_LRS", "Standard_GRS", "Standard_ZRS", "Premium_LRS" ], "metadata": { "description": "Storage Account type" } }, "location": { "type": "string", "defaultValue": "[resourceGroup().location]", "metadata": { "description": "Location for all resources." } } },

Slide 15

Slide 15 text

@shahiddev Q&A go to slido.com event #Y860 "parameters": { "storageAccountType": { "type": "string", "defaultValue": "Standard_LRS", "allowedValues": [ "Standard_LRS", "Standard_GRS", "Standard_ZRS", "Premium_LRS" ], "metadata": { "description": "Storage Account type" } }, "location": { "type": "string", "defaultValue": "[resourceGroup().location]", "metadata": { "description": "Location for all resources." } }

Slide 16

Slide 16 text

@shahiddev Q&A go to slido.com event #Y860 "parameters": { "storageAccountType": { "type": "string", "defaultValue": "Standard_LRS", "allowedValues": [ "Standard_LRS", "Standard_GRS", "Standard_ZRS", "Premium_LRS" ], "metadata": { "description": "Storage Account type" } }, "location": { "type": "string", "defaultValue": "[resourceGroup().location]", "metadata": { "description": "Location for all resources." } }

Slide 17

Slide 17 text

@shahiddev Q&A go to slido.com event #Y860 "variables": { "storageAccountName": "[concat('store', uniquestring(resourceGroup().id))]" },

Slide 18

Slide 18 text

@shahiddev Q&A go to slido.com event #Y860 "resources": [ { "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2019-04-01", "name": "[variables('storageAccountName')]", "location": "[parameters('location')]", "sku": { "name": "[parameters('storageAccountType')]" }, "kind": "StorageV2", "properties": {} } ],

Slide 19

Slide 19 text

@shahiddev Q&A go to slido.com event #Y860 "resources": [ { "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2019-04-01", "name": "[variables('storageAccountName')]", "location": "[parameters('location')]", "sku": { "name": "[parameters('storageAccountType')]" }, "kind": "StorageV2", "properties": {} } ],

Slide 20

Slide 20 text

@shahiddev Q&A go to slido.com event #Y860 "outputs": { "storageAccountName": { "type": "string", "value": "[variables('storageAccountName')]" } }

Slide 21

Slide 21 text

@shahiddev Q&A go to slido.com event #Y860 az group deployment create --resource-group --template-file Azure CLI

Slide 22

Slide 22 text

@shahiddev Q&A go to slido.com event #Y860 Pros of Cloud templating options • Can be automated • Idempotent • Declarative – desired state • Easier to provision multiple resources • Resource dependency (mostly) automatically resolved

Slide 23

Slide 23 text

@shahiddev Q&A go to slido.com event #Y860 Cons of cloud templating • Verbose • Need to learn domain specific language • Specific to each cloud provider • Closed source

Slide 24

Slide 24 text

@shahiddev Q&A go to slido.com event #Y860 Terraform • Tool from Hashicorp • Uses their own markup language (HCL) • Multi-cloud/platform • Open source • Written in Go

Slide 25

Slide 25 text

@shahiddev Q&A go to slido.com event #Y860 resource "azurerm_resource_group" "example" { name = "example-resources" location = "West Europe" } resource "azurerm_storage_account" "example" { name = "storageaccountname" resource_group_name = azurerm_resource_group.example.name location = azurerm_resource_group.example.location account_tier = "Standard" account_replication_type = "GRS" } }

Slide 26

Slide 26 text

@shahiddev Q&A go to slido.com event #Y860 resource "azurerm_resource_group" "example" { name = "example-resources" location = "West Europe" } resource "azurerm_storage_account" "example" { name = "storageaccountname" resource_group_name = azurerm_resource_group.example.name location = azurerm_resource_group.example.location account_tier = "Standard" account_replication_type = "GRS" } }

Slide 27

Slide 27 text

@shahiddev Q&A go to slido.com event #Y860 Cons with Terraform • Special language used by Terraform only • Support in Terraform lags behind cloud provider • State management

Slide 28

Slide 28 text

@shahiddev Q&A go to slido.com event #Y860 More complex deployment scenarios • Conditional logic • Loops • Invoke 3rd party code

Slide 29

Slide 29 text

@shahiddev Q&A go to slido.com event #Y860 "parameters": { "production": { "type": "string", "allowedValues": [ "Yes", "No" ], "metadata": { "description": “Should be in production or not." } } } "resources": [ { "condition": "[equals(parameters('production'), 'Yes')]", "name": "myApp", "type": "Microsoft.Web/sites", "location": "West Europe", "apiVersion": "2015-08-01", "properties": { "serverFarmId": "hostingPlanName" } } ] ARM template snippet: Conditional logic

Slide 30

Slide 30 text

@shahiddev Q&A go to slido.com event #Y860 "parameters": { "production": { "type": "string", "allowedValues": [ "Yes", "No" ], "metadata": { "description": “Should be in production or not." } } } "resources": [ { "condition": "[equals(parameters('production'), 'Yes')]", "name": "myApp", "type": "Microsoft.Web/sites", "location": "West Europe", "apiVersion": "2015-08-01", "properties": { "serverFarmId": "hostingPlanName" } } ARM template snippet: Conditional logic

Slide 31

Slide 31 text

@shahiddev Q&A go to slido.com event #Y860 "parameters": { "production": { "type": "string", "allowedValues": [ "Yes", "No" ], "metadata": { "description": “Should be in production or not." } } } "resources": [ { "condition": "[equals(parameters('production'), 'Yes')]", "name": "myApp", "type": "Microsoft.Web/sites", "location": "West Europe", "apiVersion": "2015-08-01", "properties": { "serverFarmId": "hostingPlanName" } } ] ARM template snippet: Conditional logic

Slide 32

Slide 32 text

@shahiddev Q&A go to slido.com event #Y860 variable "IsProd" { type = bool, description = "if true this is production" } resource "azurerm_storage_account" "example" { name = "storageaccountname" count = var.IsProd ? 1 : 0 resource_group_name = azurerm_resource_group.example.name location = azurerm_resource_group.example.location account_tier = "Standard" account_replication_type = "GRS" } Terraform snippet: Conditional logic

Slide 33

Slide 33 text

@shahiddev Q&A go to slido.com event #Y860 variable "IsProd" { type = bool, description = "if true this is production" } resource "azurerm_storage_account" "example" { name = "storageaccountname" count = var.IsProd ? 1 : 0 resource_group_name = azurerm_resource_group.example.name location = azurerm_resource_group.example.location account_tier = "Standard" account_replication_type = "GRS" } Terraform snippet: Conditional logic

Slide 34

Slide 34 text

@shahiddev Q&A go to slido.com event #Y860 "parameters": { "purpose": { "type": "array", "defaultValue": [ "Production", "Development" ] } }, "resources": [ { "name": "[concat('myApp-', parameters('purpose')[copyIndex()])]", "type": "Microsoft.Web/sites", "location": "West Europe", "apiVersion": "2015-08-01", "copy": { "name": "websitescopy", "count": "[length(parameters('purpose'))]" }, "properties": { "serverFarmId": "hostingPlanName" } } ] ARM template snippet: Loops

Slide 35

Slide 35 text

@shahiddev Q&A go to slido.com event #Y860 "parameters": { "purpose": { "type": "array", "defaultValue": [ "Production", "Development" ] } }, "resources": [ { "name": "[concat('myApp-', parameters('purpose')[copyIndex()])]", "type": "Microsoft.Web/sites", "location": "West Europe", "apiVersion": "2015-08-01", "copy": { "name": "websitescopy", "count": "[length(parameters('purpose'))]" }, "properties": { "serverFarmId": "hostingPlanName" } } ] ARM template snippet: Loops

Slide 36

Slide 36 text

@shahiddev Q&A go to slido.com event #Y860 variable "environments" { description = "set of environments" type = list(string) default = ["dev", "staging", "prod"] } resource "azurerm_storage_account" "example" { count = length(var.environments) name = var.environments[count.index] resource_group_name = azurerm_resource_group.example.name location = azurerm_resource_group.example.location account_tier = "Standard" account_replication_type = "GRS" } Terraform snippet: Loops

Slide 37

Slide 37 text

@shahiddev Q&A go to slido.com event #Y860 variable "environments" { description = "set of environments" type = list(string) default = ["dev", "staging", "prod"] } resource "azurerm_storage_account" "example" { count = length(var.environments) name = var.environments[count.index] resource_group_name = azurerm_resource_group.example.name location = azurerm_resource_group.example.location account_tier = "Standard" account_replication_type = "GRS" } Terraform snippet: Loops

Slide 38

Slide 38 text

@shahiddev Q&A go to slido.com event #Y860 Infrastructure as code (IaC) is the process of managing and provisioning computer data centers through machine-readable definition files, rather than physical hardware configuration or interactive configuration tools. https://en.wikipedia.org/wiki/Infrastructure_as_code

Slide 39

Slide 39 text

@shahiddev Q&A go to slido.com event #Y860 Infrastructure as code (IaC) is the process of managing and provisioning computer data centers through machine-readable definition files, rather than physical hardware configuration or interactive configuration tools.

Slide 40

Slide 40 text

@shahiddev Q&A go to slido.com event #Y860 Infrastructure as Code Software

Slide 41

Slide 41 text

@shahiddev Q&A go to slido.com event #Y860 Pulumi • Open source* • Uses modern programming languages • Supports many platforms • Declarative *Except for the Pulumi console

Slide 42

Slide 42 text

@shahiddev Q&A go to slido.com event #Y860 Supported languages

Slide 43

Slide 43 text

@shahiddev Q&A go to slido.com event #Y860 Supported Platforms

Slide 44

Slide 44 text

@shahiddev Q&A go to slido.com event #Y860

Slide 45

Slide 45 text

@shahiddev Q&A go to slido.com event #Y860

Slide 46

Slide 46 text

@shahiddev Q&A go to slido.com event #Y860

Slide 47

Slide 47 text

@shahiddev Q&A go to slido.com event #Y860 class Program { static Task Main() { return Deployment.RunAsync(() => { // Create an Azure Resource Group var resourceGroup = new ResourceGroup("resourceGroup"); // Create an Azure Storage Account var storageAccount = new Account("storage", new AccountArgs { ResourceGroupName = resourceGroup.Name, AccountReplicationType = "LRS", AccountTier = "Standard", }); // Export the connection string for the storage account return new Dictionary { { "connectionString", storageAccount.PrimaryConnectionString }, }; }); } }

Slide 48

Slide 48 text

@shahiddev Q&A go to slido.com event #Y860 class Program { static Task Main() { return Deployment.RunAsync(() => { // Create an Azure Resource Group var resourceGroup = new ResourceGroup("resourceGroup"); // Create an Azure Storage Account var storageAccount = new Account("storage", new AccountArgs { ResourceGroupName = resourceGroup.Name, AccountReplicationType = "LRS", AccountTier = "Standard", }); // Export the connection string for the storage account return new Dictionary { { "connectionString", storageAccount.PrimaryConnectionString }, }; }); }

Slide 49

Slide 49 text

@shahiddev Q&A go to slido.com event #Y860 class Program { static Task Main() { return Deployment.RunAsync(() => { // Create an Azure Resource Group var resourceGroup = new ResourceGroup("resourceGroup"); // Create an Azure Storage Account var storageAccount = new Account("storage", new AccountArgs { ResourceGroupName = resourceGroup.Name, AccountReplicationType = "LRS", AccountTier = "Standard", }); // Export the connection string for the storage account return new Dictionary { { "connectionString", storageAccount.PrimaryConnectionString }, }; }); }

Slide 50

Slide 50 text

@shahiddev Q&A go to slido.com event #Y860

Slide 51

Slide 51 text

@shahiddev Q&A go to slido.com event #Y860

Slide 52

Slide 52 text

@shahiddev Q&A go to slido.com event #Y860

Slide 53

Slide 53 text

@shahiddev Q&A go to slido.com event #Y860

Slide 54

Slide 54 text

@shahiddev Q&A go to slido.com event #Y860

Slide 55

Slide 55 text

@shahiddev Q&A go to slido.com event #Y860

Slide 56

Slide 56 text

@shahiddev Q&A go to slido.com event #Y860 Pulumi components https://www.pulumi.com/docs/intro/concepts/how-pulumi-works/ Local disk Cloud storage Pulumi console - $

Slide 57

Slide 57 text

@shahiddev Q&A go to slido.com event #Y860

Slide 58

Slide 58 text

@shahiddev Q&A go to slido.com event #Y860 Invoking 3rd party APIs Templating and Terraform options usually require dropping into a script inside the template. • Not easy to test • Relies on 3rd party tools to be available on the machine running the template • Yet another language (bash/PowerShell)

Slide 59

Slide 59 text

@shahiddev Q&A go to slido.com event #Y860 Azure storage static website hosting Run static websites/SPAs from storage account Feature can’t be enabled from Azure Resource Manager API • ARM templates/Terraform/Pulumi cannot enable this easily • Need to invoke script/Azure CLI to enable feature Can we use Azure storage .NET SDK? (spoiler alert – Yes!) https://github.com/pulumi/examples/tree/master/azure-cs-static-website

Slide 60

Slide 60 text

@shahiddev Q&A go to slido.com event #Y860 class Program { static Task Main() { return Deployment.RunAsync(() => { // Create an Azure Resource Group var resourceGroup = new ResourceGroup("mystaticsite"); // Create an Azure Storage Account var storageAccount = new Account("mysite", new AccountArgs { ResourceGroupName = resourceGroup.Name, EnableHttpsTrafficOnly = true, AccountReplicationType = "LRS", AccountTier = "Standard", AccountKind = "StorageV2", AccessTier = "Hot", });

Slide 61

Slide 61 text

@shahiddev Q&A go to slido.com event #Y860 // The code in the Apply method must be idempotent. if (!Deployment.Instance.IsDryRun) storageAccount.PrimaryBlobConnectionString.Apply(async v => await EnableStaticSites(v) ); // Upload the files var files = new[]{"index.html", "404.html"}; foreach (var file in files) { var uploadedFile = new Blob(file, new BlobArgs { Name = file, StorageAccountName = storageAccount.Name, StorageContainerName = "$web", Type = "block", Source = $"./wwwroot/{file}", ContentType = "text/html", }); }

Slide 62

Slide 62 text

@shahiddev Q&A go to slido.com event #Y860 // The code in the Apply method must be idempotent. if (!Deployment.Instance.IsDryRun) storageAccount.PrimaryBlobConnectionString.Apply(async v => await EnableStaticSites(v) ); // Upload the files var files = new[]{"index.html", "404.html"}; foreach (var file in files) { var uploadedFile = new Blob(file, new BlobArgs { Name = file, StorageAccountName = storageAccount.Name, StorageContainerName = "$web", Type = "block", Source = $"./wwwroot/{file}", ContentType = "text/html", }); }

Slide 63

Slide 63 text

@shahiddev Q&A go to slido.com event #Y860 static async Task EnableStaticSites(string connectionString) { CloudStorageAccount sa = CloudStorageAccount.Parse(connectionString); var blobClient = sa.CreateCloudBlobClient(); ServiceProperties blobServiceProperties = new ServiceProperties(); blobServiceProperties.StaticWebsite = new StaticWebsiteProperties { Enabled = true, IndexDocument = "index.html", ErrorDocument404Path = "404.html" }; await blobClient.SetServicePropertiesAsync(blobServiceProperties); }

Slide 64

Slide 64 text

@shahiddev Q&A go to slido.com event #Y860 static async Task EnableStaticSites(string connectionString) { CloudStorageAccount sa = CloudStorageAccount.Parse(connectionString); var blobClient = sa.CreateCloudBlobClient(); ServiceProperties blobServiceProperties = new ServiceProperties(); blobServiceProperties.StaticWebsite = new StaticWebsiteProperties { Enabled = true, IndexDocument = "index.html", ErrorDocument404Path = "404.html" }; await blobClient.SetServicePropertiesAsync(blobServiceProperties); }

Slide 65

Slide 65 text

@shahiddev Q&A go to slido.com event #Y860 // The code in the Apply method must be idempotent. if (!Deployment.Instance.IsDryRun) storageAccount.PrimaryBlobConnectionString.Apply(async v => await EnableStaticSites(v) ); // Upload the files var files = new[]{"index.html", "404.html"}; foreach (var file in files) { var uploadedFile = new Blob(file, new BlobArgs { Name = file, StorageAccountName = storageAccount.Name, StorageContainerName = "$web", Type = "block", Source = $"./wwwroot/{file}", ContentType = "text/html", }); }

Slide 66

Slide 66 text

@shahiddev Q&A go to slido.com event #Y860 Changes: Type Name Operation + azure:core:ResourceGroup mystaticsite created + azure:storage:Account mysite created + pulumi:pulumi:Stack azure-cs-static-website-dev created + azure:storage:Blob index.html created + azure:storage:Blob 404.html created Resources: + created 5 Duration: 28s

Slide 67

Slide 67 text

@shahiddev Q&A go to slido.com event #Y860 Primary region Application gateway Web apps

Slide 68

Slide 68 text

@shahiddev Q&A go to slido.com event #Y860 Primary region Application gateway Web apps Secondary region Application gateway Web apps Traffic manager SQL Server Failover group Geo-replication High availability configuration

Slide 69

Slide 69 text

@shahiddev Q&A go to slido.com event #Y860 Steps for single region • Provision App service plan and n-web apps, capturing app urls • Provision Application gateway and configure routes to backend web apps (using the app urls). • Adding SSL certificates • Configure security headers • Configure healthchecks • Configure SQL server and create database, capturing server and db details

Slide 70

Slide 70 text

@shahiddev Q&A go to slido.com event #Y860 Steps for multi-region deployment • Provision into primary and secondary regions, capturing app gateway addresses • Add SQL geo-replication • Configure SQL failover group and capture the failover group connection string • Add Traffic manager and wire up backends to app gateways

Slide 71

Slide 71 text

@shahiddev Q&A go to slido.com event #Y860 Multi-region deployment code Coming soon

Slide 72

Slide 72 text

@shahiddev Q&A go to slido.com event #Y860 Challenges for smaller teams • No dedicated person/team to manage cloud resources • Complex templates or duplicated code • Often several steps that need to be coordinated • Need for team to understand not only their own app code but also cloud platform + templating language

Slide 73

Slide 73 text

@shahiddev Q&A go to slido.com event #Y860 Where Pulumi can help • Likelihood that .NET (or other languages) are more understood by team • Pulumi console means everyone sees the same picture ($)

Slide 74

Slide 74 text

@shahiddev Q&A go to slido.com event #Y860 Re-usable components • Create a stack for a group of resources that you can deploy together • Can be packaged into a Nuget package to use in my org • Compose more complex deployments by re-using stacks whilst ensuring everyone is using consistent configuration

Slide 75

Slide 75 text

@shahiddev Q&A go to slido.com event #Y860 Policy as Code Allows you to define policies which “intercept” deployments and will prevent certain things from being deployed. Better option than trying to abstract the Pulumi api and hide certain options from teams.

Slide 76

Slide 76 text

@shahiddev Q&A go to slido.com event #Y860 Working with existing resources • Flexible approach to working with existing resources • Co-exist along side previously deployed resources – no interference • Adopt existing resources into Pulumi (doesn’t generate the .NET code!) • Re-write/generate Pulumi code from existing resources • Tool to generate Pulumi code from Terraform – Tf2pulumi https://www.pulumi.com/docs/guides/adopting/

Slide 77

Slide 77 text

@shahiddev Q&A go to slido.com event #Y860 Pulumi pros • Using languages your teams are familiar with already • With the power of modern langaguages and 3rd SDKs can achieve most things directly in the code • Easy to get started • Free to use and OSS (unless you want the optional console)

Slide 78

Slide 78 text

@shahiddev Q&A go to slido.com event #Y860 Pulumi cons • Fewer platforms supported vs Terraform • A lag between feature release and support in Pulumi • Azure provider is using Terraform provider – dependency on competitor product • Still need to learn/discover the cloud provider resource API (not specific to Pulumi) • Some errors not apparent until Pulumi up • Can get into bad state – especially if you cancel mid-way* *it does warn you to not cancel mid-way!

Slide 79

Slide 79 text

@shahiddev Q&A go to slido.com event #Y860 What about SDKs for the Cloud? • Abstract REST calls behind code • Imperative • Not necessarily idempotent • Difficult to reason about current state vs new state

Slide 80

Slide 80 text

@shahiddev Q&A go to slido.com event #Y860 Transpilers • Take general purpose language and convert to cloud templating • AWS CDK – generates Cloud Formation templates • Farmer – F# code generates ARM templates.

Slide 81

Slide 81 text

@shahiddev Q&A go to slido.com event #Y860 Portals Cloud templating

Slide 82

Slide 82 text

@shahiddev Q&A go to slido.com event #Y860 Learn more https://github.com/pulumi/examples https://slack.pulumi.com/ https://blog.headforcloud.com

Slide 83

Slide 83 text

@shahiddev https://linkedin.shahid.dev [email protected] https://blog.headforcloud.com Photo by Pete Pedroza on Unsplash