static Task <int> Main() { return(Deployment.RunAsync(async() => { var resourceGroup = new ResourceGroup("keyvault-rg"); // Create a storage account for Blobs var storageAccount = new Account("storage", new AccountArgs { ResourceGroupName = resourceGroup.Name, AccountReplicationType = "LRS", AccountTier = "Standard", }); // The container to put our files into var storageContainer = new Container("files", new ContainerArgs { StorageAccountName = storageAccount.Name, ContainerAccessType = "private", }); // Azure SQL Server that we want to access from the application var administratorLoginPassword = new RandomPassword("password", new RandomPasswordArgs { Length = 16, Special = true }).Result; var sqlServer = new SqlServer("sqlserver", new SqlServerArgs { ResourceGroupName = resourceGroup.Name, // The login and password are required but won't be used in our application AdministratorLogin = "******", AdministratorLoginPassword = administratorLoginPassword, Version = "12.0", }); // Azure SQL Database that we want to access from the application var database = new Database("db", new DatabaseArgs { ResourceGroupName = resourceGroup.Name, ServerName = sqlServer.Name, RequestedServiceObjectiveName = "S0", }); // The connection string that has no credentials in it: authertication will come through MSI var connectionString = Output.Format($"Server=tcp:{sqlServer.Name}.database.windows.net;Database={database.Name};"); // A file in Blob Storage that we want to access from the application var textBlob = new Blob("text", new BlobArgs { StorageAccountName = storageAccount.Name, StorageContainerName = storageContainer.Name, Type = "block", Source = "./README.md", }); // A plan to host the App Service var appServicePlan = new Plan("asp", new PlanArgs { ResourceGroupName = resourceGroup.Name, Kind = "App", Sku = new PlanSkuArgs { Tier = "Basic", Size = "B1", }, }); // ASP.NET deployment package var blob = new ZipBlob("zip", new ZipBlobArgs { StorageAccountName = storageAccount.Name, StorageContainerName = storageContainer.Name, Type = "block", Content = new FileArchive("./webapp/bin/Debug/netcoreapp2.2/publish"), }); var clientConfig = await Pulumi.Azure.Core.Invokes.GetClientConfig(); var tenantId = clientConfig.TenantId; var currentPrincipal = clientConfig.ObjectId; // Key Vault to store secrets (e.g. Blob URL with SAS) var vault = new KeyVault("vault", new KeyVaultArgs { ResourceGroupName = resourceGroup.Name, SkuName = "standard", TenantId = tenantId, AccessPolicies = { new KeyVaultAccessPoliciesArgs { TenantId = tenantId, // The current principal has to be granted permissions to Key Vault so that it can actually add and then remove // secrets to/from the Key Vault. Otherwise, 'pulumi up' and 'pulumi destroy' operations will fail. ObjectId = currentPrincipal, SecretPermissions ={ "delete", "get", "list", "set" }, } }, }); // Put the URL of the zip Blob to KV var secret = new Secret("deployment-zip", new SecretArgs { KeyVaultId = vault.Id, Value = SharedAccessSignature.SignedBlobReadUrl(blob, storageAccount), }); var secretUri = Output.Format($"{secret.VaultUri}secrets/{secret.Name}/{secret.Version}"); // The application hosted in App Service var app = new AppService("app", new AppServiceArgs { ResourceGroupName = resourceGroup.Name, AppServicePlanId = appServicePlan.Id, // A system-assigned managed service identity to be used for authentication and authorization to the SQL Database and the Blob Storage Identity = new AppServiceIdentityArgs { Type = "SystemAssigned" }, AppSettings = { // Website is deployed from a URL read from the Key Vault { "WEBSITE_RUN_FROM_ZIP", Output.Format($"@Microsoft.KeyVault(SecretUri={secretUri})") }, // Note that we simply provide the URL without SAS or keys { "StorageBlobUrl", textBlob.Url }, }, ConnectionStrings = { new AppServiceConnectionStringsArgs { Name = "db", Type = "SQLAzure", Value = connectionString, }, }, }); // Work around a preview issue https://github.com/pulumi/pulumi-azure/issues/192 var principalId = app.Identity.Apply(id => id.PrincipalId ?? "11111111-1111-1111-1111-111111111111"); // Grant App Service access to KV secrets var policy = new AccessPolicy("app-policy", new AccessPolicyArgs { KeyVaultId = vault.Id, TenantId = tenantId, ObjectId = principalId, SecretPermissions = { "get" }, }); // Make the App Service the admin of the SQL Server (double check if you want a more fine-grained security model in your real app) var sqlAdmin = new ActiveDirectoryAdministrator("adadmin", new ActiveDirectoryAdministratorArgs { ResourceGroupName = resourceGroup.Name, TenantId = tenantId, ObjectId = principalId, Login = "******", ServerName = sqlServer.Name, }); // Grant access from App Service to the container in the storage var blobPermission = new Assignment("readblob", new AssignmentArgs { PrincipalId = principalId, Scope = Output.Format($"{storageAccount.Id}/blobServices/default/containers/{storageContainer.Name}"), RoleDefinitionName = "Storage Blob Data Reader", }); // Add SQL firewall exceptions var firewallRules = app.OutboundIpAddresses.Apply( ips => ips.Split(",").Select( ip => new FirewallRule($"FR{ip}", new FirewallRuleArgs { ResourceGroupName = resourceGroup.Name, StartIpAddress = ip, EndIpAddress = ip, ServerName = sqlServer.Name, }) ).ToList()); return new Dictionary <string, object> { { "endpoint", Output.Format($"https://{app.DefaultSiteHostname}") }, }; })); }
private static AppointmentApiAzureResourceBag CreateAppointmentApiAzureResources(AzureResourceBag azureResources, Config config, Input <string> registryImageName) { var tenantId = config.Require("azure-tenantid"); var appointmentApiDb = new Database("appointments-api-db", new DatabaseArgs { ResourceGroupName = azureResources.ResourceGroup.Name, Name = "appointment-api", ServerName = azureResources.SqlServer.Name, RequestedServiceObjectiveName = "S0", Tags = azureResources.Tags }); var image = new Image("appointments-api-docker-image", new ImageArgs { Build = $".{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}", Registry = new ImageRegistry { Server = azureResources.Registry.LoginServer, Username = azureResources.Registry.AdminUsername, Password = azureResources.Registry.AdminPassword }, ImageName = registryImageName }, new ComponentResourceOptions { DependsOn = new InputList <Resource> { azureResources.Registry } }); var appointmentApiIdentity = new UserAssignedIdentity("appointments-api", new UserAssignedIdentityArgs { ResourceGroupName = azureResources.ResourceGroup.Name, Name = "appointments-api", Tags = azureResources.Tags }); // AKS service principal needs to have Managed Identity Operator rights over the user assigned identity else AAD pod identity won't work var aksSpAppointmentApiAccessPolicy = new Assignment("aks-sp-appontment-api-access", new AssignmentArgs { PrincipalId = azureResources.AksServicePrincipal.ObjectId, RoleDefinitionName = "Managed Identity Operator", Scope = appointmentApiIdentity.Id }); var sqlAdmin = new ActiveDirectoryAdministrator("appointments-api-sql-access", new ActiveDirectoryAdministratorArgs { ResourceGroupName = azureResources.ResourceGroup.Name, TenantId = tenantId, ObjectId = appointmentApiIdentity.PrincipalId, Login = "******", ServerName = azureResources.SqlServer.Name }); var clientConfig = Output.Create(GetClientConfig.InvokeAsync()); var currentPrincipalTenantId = clientConfig.Apply(c => c.TenantId); var currentPrincipal = clientConfig.Apply(c => c.ObjectId); var appointmentApiKeyVault = new KeyVault("appointment-api-keyvault", new KeyVaultArgs { ResourceGroupName = azureResources.ResourceGroup.Name, Name = "appointments-api", EnabledForDiskEncryption = true, TenantId = tenantId, SkuName = "standard", AccessPolicies = new InputList <KeyVaultAccessPolicyArgs> { new KeyVaultAccessPolicyArgs { TenantId = tenantId, ObjectId = azureResources.AksServicePrincipal.ObjectId, SecretPermissions = new[] { "get", "list" } }, new KeyVaultAccessPolicyArgs { TenantId = tenantId, ObjectId = appointmentApiIdentity.PrincipalId, SecretPermissions = new[] { "get", "list" } }, new KeyVaultAccessPolicyArgs { TenantId = currentPrincipalTenantId, ObjectId = currentPrincipal, SecretPermissions = { "delete", "get", "list", "set" }, } }, NetworkAcls = new KeyVaultNetworkAclsArgs { DefaultAction = "Deny", Bypass = "******", VirtualNetworkSubnetIds = new InputList <string> { azureResources.Subnet.Id }, // Need to whitelist the local public IP address otherwise setting secrets will fail IpRules = new InputList <string> { GetMyPublicIpAddress() } }, Tags = azureResources.Tags }); var secret = new Pulumi.Azure.KeyVault.Secret("appointments-api-db-connection-string", new Pulumi.Azure.KeyVault.SecretArgs { KeyVaultId = appointmentApiKeyVault.Id, Name = "ConnectionStrings--PetDoctorContext", Value = Output.Tuple(azureResources.SqlServer.Name, azureResources.SqlServer.Name, azureResources.SqlServer.AdministratorLogin, azureResources.SqlServer.AdministratorLoginPassword).Apply( t => { var(server, database, administratorLogin, administratorLoginPassword) = t; return ($"Server=tcp:{server}.database.windows.net;Database={database};User ID={administratorLogin};Password={administratorLoginPassword}"); }) }, new CustomResourceOptions { DependsOn = new InputList <Resource> { azureResources.SqlServer, appointmentApiKeyVault } }); return(new AppointmentApiAzureResourceBag { Identity = appointmentApiIdentity, KeyVault = appointmentApiKeyVault }); }