From 73042330a4244a450fe66e94fc5e61b72d5d0ec1 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 13 Sep 2022 16:46:08 -0700 Subject: [PATCH 01/25] Adds AKS bicep infra --- .vscode/launch.json | 28 +- cli/azd/pkg/azure/resource_ids.go | 5 + cli/azd/pkg/environment/environment.go | 3 + cli/azd/pkg/exec/command_runner.go | 11 +- cli/azd/pkg/exec/runargs.go | 8 + cli/azd/pkg/infra/azure_resource_types.go | 6 + .../pkg/project/framework_service_docker.go | 4 +- cli/azd/pkg/project/service_config.go | 9 +- cli/azd/pkg/project/service_target.go | 8 +- cli/azd/pkg/project/service_target_aks.go | 177 ++ .../project/service_target_containerapp.go | 6 +- cli/azd/pkg/tools/azcli/azcli.go | 26 +- cli/azd/pkg/tools/azcli/container_registry.go | 4 +- cli/azd/pkg/tools/azcli/container_service.go | 39 + cli/azd/pkg/tools/docker/docker.go | 33 +- cli/azd/pkg/tools/kubectl/kube_config.go | 145 ++ cli/azd/pkg/tools/kubectl/kubectl.go | 211 +++ cli/azd/pkg/tools/kubectl/kubectl_test.go | 74 + cli/azd/pkg/tools/kubectl/models.go | 14 + go.mod | 4 + go.sum | 2 + .../bicep/core/host/aks/acragentpool.bicep | 19 + .../bicep/core/host/aks/aksagentpool.bicep | 80 + .../bicep/core/host/aks/aksmetricalerts.bicep | 753 ++++++++ .../bicep/core/host/aks/aksnetcontrib.bicep | 44 + .../infra/bicep/core/host/aks/appgw.bicep | 196 ++ .../bicep/core/host/aks/bicepconfig.json | 55 + .../bicep/core/host/aks/calcAzFwIp.bicep | 10 + .../infra/bicep/core/host/aks/dnsZone.bicep | 47 + .../bicep/core/host/aks/dnsZoneRbac.bicep | 26 + .../infra/bicep/core/host/aks/firewall.bicep | 324 ++++ .../infra/bicep/core/host/aks/keyvault.bicep | 80 + .../bicep/core/host/aks/keyvaultkey.bicep | 24 + .../bicep/core/host/aks/keyvaultrbac.bicep | 173 ++ .../infra/bicep/core/host/aks/main.bicep | 1627 +++++++++++++++++ .../infra/bicep/core/host/aks/network.bicep | 506 +++++ .../core/host/aks/networksubnetrbac.bicep | 19 + .../core/host/aks/networkwatcherflowlog.bicep | 46 + .../infra/bicep/core/host/aks/nsg.bicep | 283 +++ .../nodejs-mongo-aks/.repo/bicep/azure.yaml | 15 + .../.repo/bicep/infra/main.bicep | 102 ++ .../.repo/bicep/infra/main.parameters.json | 15 + .../nodejs-mongo-aks/.repo/bicep/repo.yaml | 202 ++ .../nodejs-mongo-aks/.vscode/launch.json | 47 + .../nodejs-mongo-aks/.vscode/tasks.json | 92 + .../todo/projects/nodejs-mongo-aks/README.md | 210 +++ .../nodejs-mongo-aks/assets/resources.png | Bin 0 -> 123324 bytes .../src/api/manifests/deployment.yaml | 34 + .../src/api/manifests/ingress.yaml | 19 + .../src/api/manifests/service.yaml | 11 + .../src/web/manifests/deployment.yaml | 28 + .../src/web/manifests/ingress.yaml | 16 + .../src/web/manifests/service.yaml | 10 + 53 files changed, 5884 insertions(+), 46 deletions(-) create mode 100644 cli/azd/pkg/project/service_target_aks.go create mode 100644 cli/azd/pkg/tools/azcli/container_service.go create mode 100644 cli/azd/pkg/tools/kubectl/kube_config.go create mode 100644 cli/azd/pkg/tools/kubectl/kubectl.go create mode 100644 cli/azd/pkg/tools/kubectl/kubectl_test.go create mode 100644 cli/azd/pkg/tools/kubectl/models.go create mode 100644 templates/common/infra/bicep/core/host/aks/acragentpool.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/aksagentpool.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/aksmetricalerts.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/aksnetcontrib.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/appgw.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/bicepconfig.json create mode 100644 templates/common/infra/bicep/core/host/aks/calcAzFwIp.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/dnsZone.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/dnsZoneRbac.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/firewall.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/keyvault.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/keyvaultkey.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/keyvaultrbac.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/main.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/network.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/networksubnetrbac.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/networkwatcherflowlog.bicep create mode 100644 templates/common/infra/bicep/core/host/aks/nsg.bicep create mode 100644 templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml create mode 100644 templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep create mode 100644 templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.parameters.json create mode 100644 templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml create mode 100644 templates/todo/projects/nodejs-mongo-aks/.vscode/launch.json create mode 100644 templates/todo/projects/nodejs-mongo-aks/.vscode/tasks.json create mode 100644 templates/todo/projects/nodejs-mongo-aks/README.md create mode 100644 templates/todo/projects/nodejs-mongo-aks/assets/resources.png create mode 100644 templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml create mode 100644 templates/todo/projects/nodejs-mongo-aks/src/api/manifests/ingress.yaml create mode 100644 templates/todo/projects/nodejs-mongo-aks/src/api/manifests/service.yaml create mode 100644 templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml create mode 100644 templates/todo/projects/nodejs-mongo-aks/src/web/manifests/ingress.yaml create mode 100644 templates/todo/projects/nodejs-mongo-aks/src/web/manifests/service.yaml diff --git a/.vscode/launch.json b/.vscode/launch.json index 76ff9e18248..4c54285757e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,15 +1,15 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Attach to Process", - "type": "go", - "request": "attach", - "mode": "local", - "processId": "${command:pickGoProcess}" - } - ] -} + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Process", + "type": "go", + "request": "attach", + "mode": "local", + "processId": "${command:pickGoProcess}" + } + ] +} \ No newline at end of file diff --git a/cli/azd/pkg/azure/resource_ids.go b/cli/azd/pkg/azure/resource_ids.go index c5b056adf66..a02b221373e 100644 --- a/cli/azd/pkg/azure/resource_ids.go +++ b/cli/azd/pkg/azure/resource_ids.go @@ -51,6 +51,11 @@ func WebsiteRID(subscriptionId, resourceGroupName, websiteName string) string { return returnValue } +func AksRID(subscriptionId, resourceGroupName, clusterName string) string { + returnValue := fmt.Sprintf("%s/providers/Microsoft.ContainerService/managedClusters/%s", ResourceGroupRID(subscriptionId, resourceGroupName), clusterName) + return returnValue +} + func ContainerAppRID(subscriptionId, resourceGroupName, containerAppName string) string { returnValue := fmt.Sprintf( "%s/providers/Microsoft.App/containerApps/%s", diff --git a/cli/azd/pkg/environment/environment.go b/cli/azd/pkg/environment/environment.go index a07d31cf8fd..16a013d2fe7 100644 --- a/cli/azd/pkg/environment/environment.go +++ b/cli/azd/pkg/environment/environment.go @@ -36,6 +36,9 @@ const TenantIdEnvVarName = "AZURE_TENANT_ID" // to. const ContainerRegistryEndpointEnvVarName = "AZURE_CONTAINER_REGISTRY_ENDPOINT" +// ContainerRegistryEndpointEnvVarName is the name of they key used to store the endpoint of the container registry to push to. +const AksClusterEnvVarName = "AZURE_AKS_CLUSTER_NAME" + // ResourceGroupEnvVarName is the name of the azure resource group that should be used for deployments const ResourceGroupEnvVarName = "AZURE_RESOURCE_GROUP" diff --git a/cli/azd/pkg/exec/command_runner.go b/cli/azd/pkg/exec/command_runner.go index bd4961e8d12..585b331ba65 100644 --- a/cli/azd/pkg/exec/command_runner.go +++ b/cli/azd/pkg/exec/command_runner.go @@ -58,7 +58,14 @@ func (r *commandRunner) Run(ctx context.Context, args RunArgs) (RunResult, error cmd.Dir = args.Cwd - var stdin, stdout, stderr bytes.Buffer + var stdin io.Reader + if args.StdIn != nil { + stdin = args.StdIn + } else { + stdin = new(bytes.Buffer) + } + + var stdout, stderr bytes.Buffer cmd.Env = appendEnv(args.Env) @@ -67,7 +74,7 @@ func (r *commandRunner) Run(ctx context.Context, args RunArgs) (RunResult, error cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr } else { - cmd.Stdin = &stdin + cmd.Stdin = stdin cmd.Stdout = &stdout cmd.Stderr = &stderr diff --git a/cli/azd/pkg/exec/runargs.go b/cli/azd/pkg/exec/runargs.go index 7c121c5fbe3..ee370a14bdf 100644 --- a/cli/azd/pkg/exec/runargs.go +++ b/cli/azd/pkg/exec/runargs.go @@ -29,6 +29,9 @@ type RunArgs struct { // When set will attach commands to std input/output Interactive bool + + // When set will call the command with the specified StdIn + StdIn io.Reader } // NewRunArgs creates a new instance with the specified cmd and args @@ -81,3 +84,8 @@ func (b RunArgs) WithDebug(debug bool) RunArgs { b.Debug = debug return b } + +func (b RunArgs) WithStdIn(stdIn io.Reader) RunArgs { + b.StdIn = stdIn + return b +} diff --git a/cli/azd/pkg/infra/azure_resource_types.go b/cli/azd/pkg/infra/azure_resource_types.go index c372a3ef5d2..e74fb446342 100644 --- a/cli/azd/pkg/infra/azure_resource_types.go +++ b/cli/azd/pkg/infra/azure_resource_types.go @@ -25,6 +25,8 @@ const ( AzureResourceTypeResourceGroup AzureResourceType = "Microsoft.Resources/resourceGroups" AzureResourceTypeStorageAccount AzureResourceType = "Microsoft.Storage/storageAccounts" AzureResourceTypeStaticWebSite AzureResourceType = "Microsoft.Web/staticSites" + AzureResourceTypeContainerRegistry AzureResourceType = "Microsoft.ContainerRegistry/registries" + AzureResourceTypeManagedCluster AzureResourceType = "Microsoft.ContainerService/managedClusters" AzureResourceTypeServicePlan AzureResourceType = "Microsoft.Web/serverfarms" AzureResourceTypeSqlServer AzureResourceType = "Microsoft.Sql/servers" AzureResourceTypeVirtualNetwork AzureResourceType = "Microsoft.Network/virtualNetworks" @@ -78,6 +80,10 @@ func GetResourceTypeDisplayName(resourceType AzureResourceType) string { return "Load Tests" case AzureResourceTypeVirtualNetwork: return "Virtual Network" + case AzureResourceTypeContainerRegistry: + return "Container Registry" + case AzureResourceTypeManagedCluster: + return "AKS Managed Cluster" } return "" diff --git a/cli/azd/pkg/project/framework_service_docker.go b/cli/azd/pkg/project/framework_service_docker.go index 5b3a51a51c7..73ff59d4c3c 100644 --- a/cli/azd/pkg/project/framework_service_docker.go +++ b/cli/azd/pkg/project/framework_service_docker.go @@ -23,7 +23,7 @@ type DockerProjectOptions struct { type dockerProject struct { config *ServiceConfig env *environment.Environment - docker *docker.Docker + docker docker.Docker framework FrameworkService } @@ -66,7 +66,7 @@ func (p *dockerProject) Initialize(ctx context.Context) error { func NewDockerProject( config *ServiceConfig, env *environment.Environment, - docker *docker.Docker, + docker docker.Docker, framework FrameworkService, ) FrameworkService { return &dockerProject{ diff --git a/cli/azd/pkg/project/service_config.go b/cli/azd/pkg/project/service_config.go index 5a5e3526117..0051567639d 100644 --- a/cli/azd/pkg/project/service_config.go +++ b/cli/azd/pkg/project/service_config.go @@ -17,6 +17,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/tools" "github.com/azure/azure-dev/cli/azd/pkg/tools/azcli" "github.com/azure/azure-dev/cli/azd/pkg/tools/docker" + "github.com/azure/azure-dev/cli/azd/pkg/tools/kubectl" "github.com/azure/azure-dev/cli/azd/pkg/tools/swa" ) @@ -124,6 +125,12 @@ func (sc *ServiceConfig) GetServiceTarget( target, err = NewFunctionAppTarget(sc, env, resource, azCli) case string(StaticWebAppTarget): target, err = NewStaticWebAppTarget(sc, env, resource, azCli, swa.NewSwaCli(commandRunner)) + case string(AksTarget): + containerService, err := azCli.ContainerService(ctx, env.GetSubscriptionId()) + if err != nil { + return nil, err + } + target = NewAksTarget(sc, env, resource, azCli, containerService, kubectl.NewKubectl(commandRunner), docker.NewDocker(commandRunner)) default: return nil, fmt.Errorf("unsupported host '%s' for service '%s'", sc.Host, sc.Name) } @@ -154,7 +161,7 @@ func (sc *ServiceConfig) GetFrameworkService( } // For containerized applications we use a nested framework service - if sc.Host == string(ContainerAppTarget) { + if sc.Host == string(ContainerAppTarget) || sc.Host == string(AksTarget) { sourceFramework := frameworkService frameworkService = NewDockerProject(sc, env, docker.NewDocker(commandRunner), sourceFramework) } diff --git a/cli/azd/pkg/project/service_target.go b/cli/azd/pkg/project/service_target.go index 71cecbd61f1..d38c4761304 100644 --- a/cli/azd/pkg/project/service_target.go +++ b/cli/azd/pkg/project/service_target.go @@ -20,6 +20,7 @@ const ( ContainerAppTarget ServiceTargetKind = "containerapp" AzureFunctionTarget ServiceTargetKind = "function" StaticWebAppTarget ServiceTargetKind = "staticwebapp" + AksTarget ServiceTargetKind = "aks" ) type ServiceDeploymentResult struct { @@ -89,10 +90,5 @@ func resourceTypeMismatchError( // As an example, ContainerAppTarget is able to provision the container app as part of deployment, // and thus returns true. func (st ServiceTargetKind) SupportsDelayedProvisioning() bool { - return st == ContainerAppTarget + return st == ContainerAppTarget || st == AksTarget } - -var _ ServiceTarget = &appServiceTarget{} -var _ ServiceTarget = &containerAppTarget{} -var _ ServiceTarget = &functionAppTarget{} -var _ ServiceTarget = &staticWebAppTarget{} diff --git a/cli/azd/pkg/project/service_target_aks.go b/cli/azd/pkg/project/service_target_aks.go new file mode 100644 index 00000000000..a906b7e1907 --- /dev/null +++ b/cli/azd/pkg/project/service_target_aks.go @@ -0,0 +1,177 @@ +package project + +import ( + "context" + "fmt" + "log" + "path/filepath" + "time" + + "github.com/azure/azure-dev/cli/azd/pkg/azure" + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" + "github.com/azure/azure-dev/cli/azd/pkg/tools" + "github.com/azure/azure-dev/cli/azd/pkg/tools/azcli" + "github.com/azure/azure-dev/cli/azd/pkg/tools/docker" + "github.com/azure/azure-dev/cli/azd/pkg/tools/kubectl" +) + +type aksTarget struct { + config *ServiceConfig + env *environment.Environment + scope *environment.TargetResource + containerService azcli.ContainerServiceClient + az azcli.AzCli + docker docker.Docker + kubectl kubectl.KubectlCli +} + +func (t *aksTarget) RequiredExternalTools() []tools.ExternalTool { + return []tools.ExternalTool{t.docker} +} + +func (t *aksTarget) Deploy(ctx context.Context, azdCtx *azdcontext.AzdContext, path string, progress chan<- string) (ServiceDeploymentResult, error) { + // Login to AKS cluster + namespace := t.config.Project.Name + clusterName, has := t.env.Values[environment.AksClusterEnvVarName] + if !has { + return ServiceDeploymentResult{}, fmt.Errorf("could not determine AKS cluster, ensure %s is set as an output of your infrastructure", environment.AksClusterEnvVarName) + } + + log.Printf("getting AKS credentials %s\n", clusterName) + progress <- "Getting AKS credentials" + credentials, err := t.containerService.GetAdminCredentials(ctx, t.scope.ResourceGroupName(), clusterName) + if err != nil { + return ServiceDeploymentResult{}, err + } + + kubeConfigManager, err := kubectl.NewKubeConfigManager(t.kubectl) + if err != nil { + return ServiceDeploymentResult{}, err + } + + kubeConfig, err := kubectl.ParseKubeConfig(ctx, credentials.Kubeconfigs[0].Value) + if err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("failed parsing kube config: %w", err) + } + + if err := kubeConfigManager.SaveKubeConfig(ctx, clusterName, kubeConfig); err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("failed saving kube config: %w", err) + } + + if err := kubeConfigManager.MergeConfigs(ctx, "config", "config", clusterName); err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("failed merging kube configs: %w", err) + } + + if _, err := t.kubectl.ConfigUseContext(ctx, clusterName, nil); err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("failed using kube context '%s', %w", clusterName, err) + } + + kubeFlags := kubectl.KubeCliFlags{ + Namespace: namespace, + DryRun: "client", + Output: "yaml", + } + + progress <- "Creating k8s namespace" + namespaceResult, err := t.kubectl.CreateNamespace(ctx, namespace, &kubectl.KubeCliFlags{DryRun: "client", Output: "yaml"}) + if err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("failed creating kube namespace: %w", err) + } + + _, err = t.kubectl.ApplyPipe(ctx, *namespaceResult, nil) + if err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("failed applying kube namespace: %w", err) + } + + progress <- "Creating k8s secrets" + secrets := t.env.Environ() + secretResult, err := t.kubectl.CreateSecretGenericFromLiterals(ctx, "azd", secrets, &kubeFlags) + if err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("failed setting kube secrets: %w", err) + } + + _, err = t.kubectl.ApplyPipe(ctx, *secretResult, nil) + if err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("failed applying kube secrets: %w", err) + } + + // Login to container registry. + loginServer, has := t.env.Values[environment.ContainerRegistryEndpointEnvVarName] + if !has { + return ServiceDeploymentResult{}, fmt.Errorf("could not determine container registry endpoint, ensure %s is set as an output of your infrastructure", environment.ContainerRegistryEndpointEnvVarName) + } + + log.Printf("logging into registry %s\n", loginServer) + + progress <- "Logging into container registry" + if err := t.az.LoginAcr(ctx, t.docker, t.env.GetSubscriptionId(), loginServer); err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("logging into registry '%s': %w", loginServer, err) + } + + resourceName := t.scope.ResourceName() + if resourceName == "" { + resourceName = t.config.Name + } + + tags := []string{ + fmt.Sprintf("%s/%s/%s:azdev-deploy-%d", loginServer, t.config.Project.Name, resourceName, time.Now().Unix()), + fmt.Sprintf("%s/%s/%s:latest", loginServer, t.config.Project.Name, resourceName), + } + + for _, tag := range tags { + // Tag image. + log.Printf("tagging image %s as %s", path, tag) + progress <- "Tagging image" + if err := t.docker.Tag(ctx, t.config.Path(), path, tag); err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("tagging image: %w", err) + } + + // Push image. + progress <- "Pushing container image" + if err := t.docker.Push(ctx, t.config.Path(), tag); err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("pushing image: %w", err) + } + } + + progress <- "Applying k8s manifests" + _, err = t.kubectl.ApplyFiles(ctx, filepath.Join(t.config.RelativePath, "manifests"), &kubectl.KubeCliFlags{Namespace: namespace}) + if err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("failed applying kube manifests: %w", err) + } + + endpoints, err := t.Endpoints(ctx) + if err != nil { + return ServiceDeploymentResult{}, err + } + + return ServiceDeploymentResult{ + TargetResourceId: azure.ContainerAppRID(t.env.GetSubscriptionId(), t.scope.ResourceGroupName(), t.scope.ResourceName()), + Kind: ContainerAppTarget, + Details: nil, + Endpoints: endpoints, + }, nil +} + +func (t *aksTarget) Endpoints(ctx context.Context) ([]string, error) { + // TODO Update + return []string{"https://aks.azure.com/sample"}, nil + // containerAppProperties, err := t.cli.GetContainerAppProperties(ctx, t.env.GetSubscriptionId(), t.scope.ResourceGroupName(), t.scope.ResourceName()) + // if err != nil { + // return nil, fmt.Errorf("fetching service properties: %w", err) + // } + + // return []string{fmt.Sprintf("https://%s/", containerAppProperties.Properties.Configuration.Ingress.Fqdn)}, nil +} + +func NewAksTarget(config *ServiceConfig, env *environment.Environment, scope *environment.TargetResource, azCli azcli.AzCli, containerService azcli.ContainerServiceClient, kubectlCli kubectl.KubectlCli, docker docker.Docker) ServiceTarget { + return &aksTarget{ + config: config, + env: env, + scope: scope, + az: azCli, + containerService: containerService, + docker: docker, + kubectl: kubectlCli, + } +} diff --git a/cli/azd/pkg/project/service_target_containerapp.go b/cli/azd/pkg/project/service_target_containerapp.go index a5c96c4a71e..d985dd8d75b 100644 --- a/cli/azd/pkg/project/service_target_containerapp.go +++ b/cli/azd/pkg/project/service_target_containerapp.go @@ -31,7 +31,7 @@ type containerAppTarget struct { env *environment.Environment resource *environment.TargetResource cli azcli.AzCli - docker *docker.Docker + docker docker.Docker console input.Console commandRunner exec.CommandRunner accountManager account.Manager @@ -70,7 +70,7 @@ func (at *containerAppTarget) Deploy( log.Printf("logging into registry %s", loginServer) progress <- "Logging into container registry" - if err := at.cli.LoginAcr(ctx, at.commandRunner, at.env.GetSubscriptionId(), loginServer); err != nil { + if err := at.cli.LoginAcr(ctx, at.docker, at.env.GetSubscriptionId(), loginServer); err != nil { return ServiceDeploymentResult{}, fmt.Errorf("logging into registry '%s': %w", loginServer, err) } @@ -234,7 +234,7 @@ func NewContainerAppTarget( env *environment.Environment, resource *environment.TargetResource, azCli azcli.AzCli, - docker *docker.Docker, + docker docker.Docker, console input.Console, commandRunner exec.CommandRunner, accountManager account.Manager, diff --git a/cli/azd/pkg/tools/azcli/azcli.go b/cli/azd/pkg/tools/azcli/azcli.go index add99aa95b7..8548de6c067 100644 --- a/cli/azd/pkg/tools/azcli/azcli.go +++ b/cli/azd/pkg/tools/azcli/azcli.go @@ -16,8 +16,8 @@ import ( azdinternal "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/pkg/azsdk" "github.com/azure/azure-dev/cli/azd/pkg/azure" - "github.com/azure/azure-dev/cli/azd/pkg/exec" "github.com/azure/azure-dev/cli/azd/pkg/httputil" + "github.com/azure/azure-dev/cli/azd/pkg/tools/docker" ) var ( @@ -37,7 +37,7 @@ type AzCli interface { // UserAgent gets the currently configured user agent UserAgent() string - LoginAcr(ctx context.Context, commandRunner exec.CommandRunner, subscriptionId string, loginServer string) error + LoginAcr(ctx context.Context, dockerCli docker.Docker, subscriptionId string, loginServer string) error GetContainerRegistries(ctx context.Context, subscriptionId string) ([]*armcontainerregistry.Registry, error) GetSubscriptionDeployment( ctx context.Context, @@ -165,6 +165,9 @@ type AzCli interface { environmentName string, ) (*AzCliStaticWebAppEnvironmentProperties, error) GetAccessToken(ctx context.Context) (*AzCliAccessToken, error) + + // AKS + ContainerService(ctx context.Context, subscriptionId string) (ContainerServiceClient, error) } type AzCliDeployment struct { @@ -344,3 +347,22 @@ func clientOptionsBuilder(httpClient httputil.HttpClient, userAgent string) *azs WithTransport(httpClient). WithPerCallPolicy(azsdk.NewUserAgentPolicy(userAgent)) } + +func (cli *azCli) GetAksCredentials(ctx context.Context, resourceGroupName string, clusterName string) error { + // _, err := cli.runAzCommand(ctx, + // "aks", "get-credentials", + // "--resource-group", resourceGroupName, + // "--name", clusterName, + // ) + + // if err != nil { + // return fmt.Errorf("getting AKS credentials: %w", err) + // } + + return nil +} + +func (cli *azCli) ContainerService(ctx context.Context, subscriptionId string) (ContainerServiceClient, error) { + options := cli.createDefaultClientOptionsBuilder(ctx).BuildArmClientOptions() + return NewContainerServiceClient(subscriptionId, cli.credential, options) +} diff --git a/cli/azd/pkg/tools/azcli/container_registry.go b/cli/azd/pkg/tools/azcli/container_registry.go index db6c02030fb..80d874219c3 100644 --- a/cli/azd/pkg/tools/azcli/container_registry.go +++ b/cli/azd/pkg/tools/azcli/container_registry.go @@ -7,7 +7,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry" "github.com/azure/azure-dev/cli/azd/pkg/azure" - "github.com/azure/azure-dev/cli/azd/pkg/exec" "github.com/azure/azure-dev/cli/azd/pkg/tools/docker" "golang.org/x/exp/slices" ) @@ -37,7 +36,7 @@ func (cli *azCli) GetContainerRegistries( } func (cli *azCli) LoginAcr(ctx context.Context, - commandRunner exec.CommandRunner, subscriptionId string, loginServer string, + dockerCli docker.Docker, subscriptionId string, loginServer string, ) error { client, err := cli.createRegistriesClient(ctx, subscriptionId) if err != nil { @@ -62,7 +61,6 @@ func (cli *azCli) LoginAcr(ctx context.Context, username := *credResponse.Username // Login to docker with ACR credentials to allow push operations - dockerCli := docker.NewDocker(commandRunner) err = dockerCli.Login(ctx, loginServer, username, *credResponse.Passwords[0].Value) if err != nil { return fmt.Errorf("failed logging into docker for username '%s' and server %s: %w", loginServer, username, err) diff --git a/cli/azd/pkg/tools/azcli/container_service.go b/cli/azd/pkg/tools/azcli/container_service.go new file mode 100644 index 00000000000..acfdefbf82d --- /dev/null +++ b/cli/azd/pkg/tools/azcli/container_service.go @@ -0,0 +1,39 @@ +package azcli + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v2" +) + +type ContainerServiceClient interface { + GetAdminCredentials(ctx context.Context, resourceGroupName string, resourceName string) (*armcontainerservice.CredentialResults, error) +} + +type containerServiceClient struct { + client *armcontainerservice.ManagedClustersClient + subscriptionId string +} + +func NewContainerServiceClient(subscriptionId string, credential azcore.TokenCredential, options *arm.ClientOptions) (ContainerServiceClient, error) { + azureClient, err := armcontainerservice.NewManagedClustersClient(subscriptionId, credential, options) + if err != nil { + return nil, err + } + + return &containerServiceClient{ + subscriptionId: subscriptionId, + client: azureClient, + }, nil +} + +func (cs *containerServiceClient) GetAdminCredentials(ctx context.Context, resourceGroupName string, resourceName string) (*armcontainerservice.CredentialResults, error) { + creds, err := cs.client.ListClusterAdminCredentials(ctx, resourceGroupName, resourceName, nil) + if err != nil { + return nil, err + } + + return &creds.CredentialResults, nil +} diff --git a/cli/azd/pkg/tools/docker/docker.go b/cli/azd/pkg/tools/docker/docker.go index 76b62f00e8d..eb0edc8687f 100644 --- a/cli/azd/pkg/tools/docker/docker.go +++ b/cli/azd/pkg/tools/docker/docker.go @@ -13,17 +13,25 @@ import ( "github.com/blang/semver/v4" ) -func NewDocker(commandRunner exec.CommandRunner) *Docker { - return &Docker{ +type Docker interface { + tools.ExternalTool + Login(ctx context.Context, loginServer string, username string, password string) error + Build(ctx context.Context, cwd string, dockerFilePath string, platform string, buildContext string) (string, error) + Tag(ctx context.Context, cwd string, imageName string, tag string) error + Push(ctx context.Context, cwd string, tag string) error +} + +func NewDocker(commandRunner exec.CommandRunner) Docker { + return &docker{ commandRunner: commandRunner, } } -type Docker struct { +type docker struct { commandRunner exec.CommandRunner } -func (d *Docker) Login(ctx context.Context, loginServer string, username string, password string) error { +func (d *docker) Login(ctx context.Context, loginServer string, username string, password string) error { _, err := d.executeCommand(ctx, ".", "login", "--username", username, "--password", password, @@ -40,7 +48,7 @@ func (d *Docker) Login(ctx context.Context, loginServer string, username string, // it defaults to amd64. If the build // is successful, the function // returns the image id of the built image. -func (d *Docker) Build( +func (d *docker) Build( ctx context.Context, cwd string, dockerFilePath string, @@ -59,7 +67,7 @@ func (d *Docker) Build( return strings.TrimSpace(res.Stdout), nil } -func (d *Docker) Tag(ctx context.Context, cwd string, imageName string, tag string) error { +func (d *docker) Tag(ctx context.Context, cwd string, imageName string, tag string) error { res, err := d.executeCommand(ctx, cwd, "tag", imageName, tag) if err != nil { return fmt.Errorf("tagging image: %s: %w", res.String(), err) @@ -68,7 +76,7 @@ func (d *Docker) Tag(ctx context.Context, cwd string, imageName string, tag stri return nil } -func (d *Docker) Push(ctx context.Context, cwd string, tag string) error { +func (d *docker) Push(ctx context.Context, cwd string, tag string) error { res, err := d.executeCommand(ctx, cwd, "push", tag) if err != nil { return fmt.Errorf("pushing image: %s: %w", res.String(), err) @@ -77,7 +85,7 @@ func (d *Docker) Push(ctx context.Context, cwd string, tag string) error { return nil } -func (d *Docker) versionInfo() tools.VersionInfo { +func (d *docker) versionInfo() tools.VersionInfo { return tools.VersionInfo{ MinimumVersion: semver.Version{ Major: 17, @@ -162,8 +170,7 @@ func isSupportedDockerVersion(cliOutput string) (bool, error) { // If we reach this point, we don't understand how to validate the version based on its scheme. return false, fmt.Errorf("could not determine version from docker version string: %s", version) } - -func (d *Docker) CheckInstalled(ctx context.Context) (bool, error) { +func (d *docker) CheckInstalled(ctx context.Context) (bool, error) { found, err := tools.ToolInPath("docker") if !found { return false, err @@ -182,15 +189,15 @@ func (d *Docker) CheckInstalled(ctx context.Context) (bool, error) { return true, nil } -func (d *Docker) InstallUrl() string { +func (d *docker) InstallUrl() string { return "https://aka.ms/azure-dev/docker-install" } -func (d *Docker) Name() string { +func (d *docker) Name() string { return "Docker" } -func (d *Docker) executeCommand(ctx context.Context, cwd string, args ...string) (exec.RunResult, error) { +func (d *docker) executeCommand(ctx context.Context, cwd string, args ...string) (exec.RunResult, error) { runArgs := exec.NewRunArgs("docker", args...). WithCwd(cwd). WithEnrichError(true) diff --git a/cli/azd/pkg/tools/kubectl/kube_config.go b/cli/azd/pkg/tools/kubectl/kube_config.go new file mode 100644 index 00000000000..f9af9ffc513 --- /dev/null +++ b/cli/azd/pkg/tools/kubectl/kube_config.go @@ -0,0 +1,145 @@ +package kubectl + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/azure/azure-dev/cli/azd/pkg/osutil" + "gopkg.in/yaml.v3" +) + +type KubeConfig struct { + ApiVersion string `yaml:"apiVersion"` + Clusters []*KubeCluster `yaml:"clusters"` + Contexts []*KubeContext `yaml:"contexts"` + Users []*KubeUser `yaml:"users"` + Kind string `yaml:"kind"` + CurrentContext string `yaml:"current-context"` + Preferences KubePreferences `yaml:"preferences"` +} + +type KubeCluster struct { + Name string `yaml:"name"` + Cluster KubeClusterData `yaml:"cluster"` +} + +type KubeClusterData struct { + CertificateAuthorityData string `yaml:"certificate-authority-data"` + Server string `yaml:"server"` +} + +type KubeContext struct { + Name string `yaml:"name"` + Context KubeContextData `yaml:"context"` +} + +type KubeContextData struct { + Cluster string `yaml:"cluster"` + User string `yaml:"user"` +} + +type KubeUser struct { + Name string `yaml:"name"` + KubeUserData KubeUserData `yaml:"user"` +} + +type KubeUserData map[string]any +type KubePreferences map[string]any + +type KubeConfigManager struct { + cli KubectlCli + configPath string +} + +func NewKubeConfigManager(cli KubectlCli) (*KubeConfigManager, error) { + kubeConfigDir, err := getKubeConfigDir() + if err != nil { + return nil, err + } + + return &KubeConfigManager{ + cli: cli, + configPath: kubeConfigDir, + }, nil +} + +func ParseKubeConfig(ctx context.Context, raw []byte) (*KubeConfig, error) { + var existing KubeConfig + if err := yaml.Unmarshal(raw, &existing); err != nil { + return nil, fmt.Errorf("failed unmarshalling Kube Config YAML: %w", err) + } + + return &existing, nil +} + +func (kcm *KubeConfigManager) SaveKubeConfig(ctx context.Context, configName string, config *KubeConfig) error { + kubeConfigRaw, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("failed marshalling KubeConfig to yaml: %w", err) + } + + outFilePath := filepath.Join(kcm.configPath, configName) + err = os.WriteFile(outFilePath, kubeConfigRaw, osutil.PermissionFile) + if err != nil { + return fmt.Errorf("failed write kube config file: %w", err) + } + + return nil +} + +func (kcm *KubeConfigManager) DeleteKubeConfig(ctx context.Context, configName string) error { + kubeConfigPath := filepath.Join(kcm.configPath, configName) + err := os.Remove(kubeConfigPath) + if err != nil { + return fmt.Errorf("failed deleting kube config file: %w", err) + } + + return nil +} + +func (kcm *KubeConfigManager) MergeConfigs(ctx context.Context, newConfigName string, path ...string) error { + fullConfigPaths := []string{} + for _, kubeConfigName := range path { + fullConfigPaths = append(fullConfigPaths, filepath.Join(kcm.configPath, kubeConfigName)) + } + + kcm.cli.SetEnv(fmt.Sprintf("KUBECONFIG=%s", strings.Join(fullConfigPaths, string(os.PathListSeparator)))) + res, err := kcm.cli.ConfigView(ctx, true, true, nil) + if err != nil { + return fmt.Errorf("kubectl config view failed: %w", err) + } + + kubeConfigRaw := []byte(res.Stdout) + outFilePath := filepath.Join(kcm.configPath, newConfigName) + err = os.WriteFile(outFilePath, kubeConfigRaw, osutil.PermissionFile) + if err != nil { + return fmt.Errorf("failed writing new kube config: %w", err) + } + + return nil +} + +func (kcm *KubeConfigManager) AddOrUpdateContext(ctx context.Context, contextName string, newKubeConfig *KubeConfig) error { + err := kcm.SaveKubeConfig(ctx, contextName, newKubeConfig) + if err != nil { + return fmt.Errorf("failed write new kube context file: %w", err) + } + + err = kcm.MergeConfigs(ctx, "config", contextName) + if err != nil { + return fmt.Errorf("failed merging KUBE configs: %w", err) + } + + return nil +} + +func getKubeConfigDir() (string, error) { + userHomeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("cannot get user home directory: %w", err) + } + return filepath.Join(userHomeDir, ".kube"), nil +} diff --git a/cli/azd/pkg/tools/kubectl/kubectl.go b/cli/azd/pkg/tools/kubectl/kubectl.go new file mode 100644 index 00000000000..60c73ceb84d --- /dev/null +++ b/cli/azd/pkg/tools/kubectl/kubectl.go @@ -0,0 +1,211 @@ +package kubectl + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/tools" +) + +type KubectlCli interface { + tools.ExternalTool + Cwd(cwd string) + SetEnv(env ...string) + GetNodes(ctx context.Context, flags *KubeCliFlags) ([]Node, error) + ApplyFiles(ctx context.Context, path string, flags *KubeCliFlags) (*exec.RunResult, error) + ApplyKustomize(ctx context.Context, path string, flags *KubeCliFlags) (*exec.RunResult, error) + ConfigView(ctx context.Context, merge bool, flatten bool, flags *KubeCliFlags) (*exec.RunResult, error) + ConfigUseContext(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) + CreateNamespace(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) + CreateSecretGenericFromLiterals(ctx context.Context, name string, secrets []string, flags *KubeCliFlags) (*exec.RunResult, error) + ApplyPipe(ctx context.Context, result exec.RunResult, flags *KubeCliFlags) (*exec.RunResult, error) +} + +type kubectlCli struct { + tools.ExternalTool + commandRunner exec.CommandRunner + env []string + cwd string +} + +func (cli *kubectlCli) CheckInstalled(ctx context.Context) (bool, error) { + return true, nil +} + +func (cli *kubectlCli) InstallUrl() string { + return "https://aka.ms/azure-dev/kubectl-install" +} + +func (cli *kubectlCli) Name() string { + return "kubectl" +} + +func (cli *kubectlCli) SetEnv(env ...string) { + cli.env = env +} + +func (cli *kubectlCli) Cwd(cwd string) { + cli.cwd = cwd +} + +func (cli *kubectlCli) ConfigUseContext(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) { + res, err := cli.executeCommand(ctx, flags, "config", "use-context", name) + if err != nil { + return nil, fmt.Errorf("failed setting kubectl context: %w", err) + } + + return &res, nil +} + +func (cli *kubectlCli) ConfigView(ctx context.Context, merge bool, flatten bool, flags *KubeCliFlags) (*exec.RunResult, error) { + kubeConfigDir, err := getKubeConfigDir() + if err != nil { + return nil, err + } + + args := []string{"config", "view"} + if merge { + args = append(args, "--merge") + } + if flatten { + args = append(args, "--flatten") + } + + runArgs := exec.NewRunArgs("kubectl", args...). + WithCwd(kubeConfigDir). + WithEnv(cli.env) + + res, err := cli.executeCommandWithArgs(ctx, runArgs, flags) + if err != nil { + return nil, fmt.Errorf("kubectl config view: %w", err) + } + + return &res, nil +} + +func (cli *kubectlCli) GetNodes(ctx context.Context, flags *KubeCliFlags) ([]Node, error) { + res, err := cli.executeCommand(ctx, flags, + "get", "nodes", + "-o", "json", + ) + if err != nil { + return nil, fmt.Errorf("kubectl get nodes: %w", err) + } + + var listResult ListResult + if err := json.Unmarshal([]byte(res.Stdout), &listResult); err != nil { + return nil, fmt.Errorf("unmarshaling json: %w", err) + } + + nodes := []Node{} + for _, item := range listResult.Items { + metadata := item["metadata"].(map[string]any) + + nodes = append(nodes, Node{ + Name: metadata["name"].(string), + }) + } + + return nodes, nil +} + +func (cli *kubectlCli) ApplyPipe(ctx context.Context, result exec.RunResult, flags *KubeCliFlags) (*exec.RunResult, error) { + runArgs := exec. + NewRunArgs("kubectl", "apply", "-f", "-"). + WithStdIn(strings.NewReader(result.Stdout)) + + res, err := cli.executeCommandWithArgs(ctx, runArgs, flags) + if err != nil { + return nil, fmt.Errorf("kubectl apply -f: %w", err) + } + + return &res, nil +} + +func (cli *kubectlCli) ApplyFiles(ctx context.Context, path string, flags *KubeCliFlags) (*exec.RunResult, error) { + + res, err := cli.executeCommand(ctx, flags, "apply", "-f", path) + if err != nil { + return nil, fmt.Errorf("kubectl apply -f: %w", err) + } + + return &res, nil +} + +func (cli *kubectlCli) ApplyKustomize(ctx context.Context, path string, flags *KubeCliFlags) (*exec.RunResult, error) { + res, err := cli.executeCommand(ctx, flags, "apply", "-k", path) + if err != nil { + return nil, fmt.Errorf("kubectl apply -k: %w", err) + } + + return &res, nil +} + +func (cli *kubectlCli) CreateSecretGenericFromLiterals(ctx context.Context, name string, secrets []string, flags *KubeCliFlags) (*exec.RunResult, error) { + args := []string{"create", "secret", "generic", name} + for _, secret := range secrets { + args = append(args, fmt.Sprintf("--from-literal=%s", secret)) + } + + res, err := cli.executeCommand(ctx, flags, args...) + if err != nil { + return nil, fmt.Errorf("kubectl create secret generic --from-env-file: %w", err) + } + + return &res, nil +} + +type KubeCliFlags struct { + Namespace string + DryRun string + Output string +} + +func (cli *kubectlCli) CreateNamespace(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) { + args := []string{"create", "namespace", name} + + res, err := cli.executeCommand(ctx, flags, args...) + if err != nil { + return nil, fmt.Errorf("kubectl create namespace: %w", err) + } + + return &res, nil +} + +func (cli *kubectlCli) executeCommand(ctx context.Context, flags *KubeCliFlags, args ...string) (exec.RunResult, error) { + runArgs := exec. + NewRunArgs("kubectl"). + AppendParams(args...) + + return cli.executeCommandWithArgs(ctx, runArgs, flags) +} + +func (cli *kubectlCli) executeCommandWithArgs(ctx context.Context, args exec.RunArgs, flags *KubeCliFlags) (exec.RunResult, error) { + args = args.WithEnrichError(true) + if cli.cwd != "" { + args = args.WithCwd(cli.cwd) + } + + if flags != nil { + if flags.DryRun != "" { + args = args.AppendParams(fmt.Sprintf("--dry-run=%s", flags.DryRun)) + } + if flags.Namespace != "" { + args = args.AppendParams("-n", flags.Namespace) + } + if flags.Output != "" { + args = args.AppendParams("-o", flags.Output) + } + } + + return cli.commandRunner.Run(ctx, args) +} + +func NewKubectl(commandRunner exec.CommandRunner) KubectlCli { + return &kubectlCli{ + commandRunner: commandRunner, + } +} diff --git a/cli/azd/pkg/tools/kubectl/kubectl_test.go b/cli/azd/pkg/tools/kubectl/kubectl_test.go new file mode 100644 index 00000000000..9642a7e8e1e --- /dev/null +++ b/cli/azd/pkg/tools/kubectl/kubectl_test.go @@ -0,0 +1,74 @@ +package kubectl + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/test/mocks" + "github.com/stretchr/testify/require" +) + +func Test_MergeKubeConfig(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + commandRunner := exec.NewCommandRunner(os.Stdin, os.Stdout, os.Stderr) + cli := NewKubectl(commandRunner) + kubeConfigManager, err := NewKubeConfigManager(cli) + require.NoError(t, err) + + config1 := createTestCluster("cluster1", "user1") + config2 := createTestCluster("cluster2", "user2") + config3 := createTestCluster("cluster3", "user3") + + defer func() { + err := kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config1") + require.NoError(t, err) + err = kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config2") + require.NoError(t, err) + err = kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config3") + require.NoError(t, err) + }() + + err = kubeConfigManager.SaveKubeConfig(*mockContext.Context, "config1", config1) + require.NoError(t, err) + err = kubeConfigManager.SaveKubeConfig(*mockContext.Context, "config2", config2) + require.NoError(t, err) + err = kubeConfigManager.SaveKubeConfig(*mockContext.Context, "config3", config3) + require.NoError(t, err) + + err = kubeConfigManager.MergeConfigs(*mockContext.Context, "config", "config1", "config2", "config3") + require.NoError(t, err) +} + +func createTestCluster(clusterName, username string) *KubeConfig { + return &KubeConfig{ + ApiVersion: "v1", + Kind: "Config", + CurrentContext: clusterName, + Preferences: KubePreferences{}, + Clusters: []*KubeCluster{ + { + Name: clusterName, + Cluster: KubeClusterData{ + Server: fmt.Sprintf("https://%s.eastus2.azmk8s.io:443", clusterName), + }, + }, + }, + Users: []*KubeUser{ + { + Name: fmt.Sprintf("%s_%s", clusterName, username), + }, + }, + Contexts: []*KubeContext{ + { + Name: clusterName, + Context: KubeContextData{ + Cluster: clusterName, + User: fmt.Sprintf("%s_%s", clusterName, username), + }, + }, + }, + } +} diff --git a/cli/azd/pkg/tools/kubectl/models.go b/cli/azd/pkg/tools/kubectl/models.go new file mode 100644 index 00000000000..7074775f22b --- /dev/null +++ b/cli/azd/pkg/tools/kubectl/models.go @@ -0,0 +1,14 @@ +package kubectl + +type Node struct { + Name string + Status string + Roles []string + Version string +} + +type ListResult struct { + ApiVersion string `json:"apiVersion"` + Kind string `json: "kind"` + Items []map[string]any `json:"items"` +} diff --git a/go.mod b/go.mod index d8216bbe049..ec408d6df7f 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,10 @@ require ( ) +require ( + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v2 v2.2.0 // indirect + github.com/kr/text v0.2.0 // indirect +) require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 // indirect diff --git a/go.sum b/go.sum index 4157d383f99..a048f1b13df 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthoriza github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization v1.0.0/go.mod h1:lPneRe3TwsoDRKY4O6YDLXHhEWrD+TIRa8XrV/3/fqw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.6.0 h1:Z5/bDxQL2Zc9t6ZDwdRU60bpLHZvoKOeuaM7XVbf2z0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.6.0/go.mod h1:0FPu3oDRGPvuX1H8TtHJ5XGA0KrXLunomcixR+PQGGA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v2 v2.2.0 h1:3L+gX5ssCABAToH0VQ64/oNz7rr+ShW+2sB+sonzIlY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v2 v2.2.0/go.mod h1:4gUds0dEPFIld6DwHfbo0cLBljyIyI5E5ciPb5MLi3Q= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0 h1:lMW1lD/17LUA5z1XTURo7LcVG2ICBPlyMHjIUrcFZNQ= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.0.0 h1:Jc2KcpCDMu7wJfkrzn7fs/53QMDXH78GuqnH4HOd7zs= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.0.0/go.mod h1:PFVgFsclKzPqYRT/BiwpfUN22cab0C7FlgXR3iWpwMo= diff --git a/templates/common/infra/bicep/core/host/aks/acragentpool.bicep b/templates/common/infra/bicep/core/host/aks/acragentpool.bicep new file mode 100644 index 00000000000..4bba2c43484 --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/acragentpool.bicep @@ -0,0 +1,19 @@ +param location string = resourceGroup().location +param acrName string +param acrPoolSubnetId string = '' + +resource acr 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' existing = { + name: acrName +} + +resource acrPool 'Microsoft.ContainerRegistry/registries/agentPools@2019-06-01-preview' = { + name: 'private-pool' + location: location + parent: acr + properties: { + count: 1 + os: 'Linux' + tier: 'S1' + virtualNetworkSubnetResourceId: acrPoolSubnetId + } +} diff --git a/templates/common/infra/bicep/core/host/aks/aksagentpool.bicep b/templates/common/infra/bicep/core/host/aks/aksagentpool.bicep new file mode 100644 index 00000000000..100992aaf88 --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/aksagentpool.bicep @@ -0,0 +1,80 @@ +param AksName string + +param PoolName string + +@description('The zones to use for a node pool') +param availabilityZones array = [] + +@description('OS disk type') +param osDiskType string + +@description('VM SKU') +param agentVMSize string + +@description('Disk size in GB') +param osDiskSizeGB int = 0 + +@description('The number of agents for the user node pool') +param agentCount int = 1 + +@description('The maximum number of nodes for the user node pool') +param agentCountMax int = 3 +var autoScale = agentCountMax > agentCount + +@description('The maximum number of pods per node.') +param maxPods int = 30 + +@description('Any taints that should be applied to the node pool') +param nodeTaints array = [] + +@description('Any labels that should be applied to the node pool') +param nodeLabels object = {} + +@description('The subnet the node pool will use') +param subnetId string + +@description('OS Type for the node pool') +@allowed(['Linux','Windows']) +param osType string + +@allowed(['Ubuntu','Windows2019','Windows2022']) +param osSKU string + +@description('Assign a public IP per node') +param enableNodePublicIP bool = false + +@description('Apply a default sku taint to Windows node pools') +param autoTaintWindows bool = false + +var taints = autoTaintWindows ? union(nodeTaints, ['sku=Windows:NoSchedule']) : nodeTaints + +resource aks 'Microsoft.ContainerService/managedClusters@2021-10-01' existing = { + name: AksName +} + +resource userNodepool 'Microsoft.ContainerService/managedClusters/agentPools@2021-10-01' = { + parent: aks + name: PoolName + properties: { + mode: 'User' + vmSize: agentVMSize + count: agentCount + minCount: autoScale ? agentCount : json('null') + maxCount: autoScale ? agentCountMax : json('null') + enableAutoScaling: autoScale + availabilityZones: !empty(availabilityZones) ? availabilityZones : null + osDiskType: osDiskType + osSKU: osSKU + osDiskSizeGB: osDiskSizeGB + osType: osType + maxPods: maxPods + type: 'VirtualMachineScaleSets' + vnetSubnetID: !empty(subnetId) ? subnetId : json('null') + upgradeSettings: { + maxSurge: '33%' + } + nodeTaints: taints + nodeLabels: nodeLabels + enableNodePublicIP: enableNodePublicIP + } +} diff --git a/templates/common/infra/bicep/core/host/aks/aksmetricalerts.bicep b/templates/common/infra/bicep/core/host/aks/aksmetricalerts.bicep new file mode 100644 index 00000000000..d1ed20d9a01 --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/aksmetricalerts.bicep @@ -0,0 +1,753 @@ +@description('The name of the AKS Cluster to configure the alerts on') +param clusterName string + +@description('The name of the Log Analytics workspace to log metric data to') +param logAnalyticsWorkspaceName string + +@description('The location of the Log Analytics workspace') +param logAnalyticsWorkspaceLocation string = resourceGroup().location + +@description('Select the frequency on how often the alert rule should be run. Selecting frequency smaller than granularity of datapoints grouping will result in sliding window evaluation') +@allowed([ + 'PT1M' + 'PT15M' +]) +param evalFrequency string = 'PT1M' + +@description('Create the metric alerts as either enabled or disabled') +param metricAlertsEnabled bool = true + +@description('Defines the interval over which datapoints are grouped using the aggregation type function') +@allowed([ + 'PT5M' + 'PT1H' +]) +param windowSize string = 'PT5M' + +@allowed([ + 'Critical' + 'Error' + 'Warning' + 'Informational' + 'Verbose' +]) +param alertSeverity string = 'Informational' + +var alertServerityLookup = { + Critical: 0 + Error: 1 + Warning: 2 + Informational: 3 + Verbose: 4 +} +var alertSeverityNumber = alertServerityLookup[alertSeverity] + +var AksResourceId = resourceId('Microsoft.ContainerService/managedClusters', clusterName) + +resource Node_CPU_utilization_high_for_clusterName_CI_1 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Node CPU utilization high for ${clusterName} CI-1' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'host' + operator: 'Include' + values: [ + '*' + ] + } + ] + metricName: 'cpuUsagePercentage' + metricNamespace: 'Insights.Container/nodes' + name: 'Metric1' + operator: 'GreaterThan' + threshold: 80 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'Node CPU utilization across the cluster.' + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'microsoft.containerservice/managedclusters' + windowSize: windowSize + } +} + +resource Node_working_set_memory_utilization_high_for_clusterName_CI_2 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Node working set memory utilization high for ${clusterName} CI-2' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'host' + operator: 'Include' + values: [ + '*' + ] + } + ] + metricName: 'memoryWorkingSetPercentage' + metricNamespace: 'Insights.Container/nodes' + name: 'Metric1' + operator: 'GreaterThan' + threshold: 80 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'Node working set memory utilization across the cluster.' + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'microsoft.containerservice/managedclusters' + windowSize: windowSize + } +} + +resource Jobs_completed_more_than_6_hours_ago_for_clusterName_CI_11 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Jobs completed more than 6 hours ago for ${clusterName} CI-11' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'controllerName' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'kubernetes namespace' + operator: 'Include' + values: [ + '*' + ] + } + ] + metricName: 'completedJobsCount' + metricNamespace: 'Insights.Container/pods' + name: 'Metric1' + operator: 'GreaterThan' + threshold: 0 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'This alert monitors completed jobs (more than 6 hours ago).' + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'microsoft.containerservice/managedclusters' + windowSize: windowSize + } +} + +resource Container_CPU_usage_high_for_clusterName_CI_9 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Container CPU usage high for ${clusterName} CI-9' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'controllerName' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'kubernetes namespace' + operator: 'Include' + values: [ + '*' + ] + } + ] + metricName: 'cpuExceededPercentage' + metricNamespace: 'Insights.Container/containers' + name: 'Metric1' + operator: 'GreaterThan' + threshold: 90 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'This alert monitors container CPU utilization.' + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'microsoft.containerservice/managedclusters' + windowSize: windowSize + } +} + +resource Container_working_set_memory_usage_high_for_clusterName_CI_10 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Container working set memory usage high for ${clusterName} CI-10' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'controllerName' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'kubernetes namespace' + operator: 'Include' + values: [ + '*' + ] + } + ] + metricName: 'memoryWorkingSetExceededPercentage' + metricNamespace: 'Insights.Container/containers' + name: 'Metric1' + operator: 'GreaterThan' + threshold: 90 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'This alert monitors container working set memory utilization.' + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'microsoft.containerservice/managedclusters' + windowSize: windowSize + } +} + +resource Pods_in_failed_state_for_clusterName_CI_4 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Pods in failed state for ${clusterName} CI-4' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'phase' + operator: 'Include' + values: [ + 'Failed' + ] + } + ] + metricName: 'podCount' + metricNamespace: 'Insights.Container/pods' + name: 'Metric1' + operator: 'GreaterThan' + threshold: 0 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'Pod status monitoring.' + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'microsoft.containerservice/managedclusters' + windowSize: windowSize + } +} + +resource Disk_usage_high_for_clusterName_CI_5 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Disk usage high for ${clusterName} CI-5' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'host' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'device' + operator: 'Include' + values: [ + '*' + ] + } + ] + metricName: 'DiskUsedPercentage' + metricNamespace: 'Insights.Container/nodes' + name: 'Metric1' + operator: 'GreaterThan' + threshold: 80 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'This alert monitors disk usage for all nodes and storage devices.' + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'microsoft.containerservice/managedclusters' + windowSize: windowSize + } +} + +resource Nodes_in_not_ready_status_for_clusterName_CI_3 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Nodes in not ready status for ${clusterName} CI-3' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'status' + operator: 'Include' + values: [ + 'NotReady' + ] + } + ] + metricName: 'nodesCount' + metricNamespace: 'Insights.Container/nodes' + name: 'Metric1' + operator: 'GreaterThan' + threshold: 0 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'Node status monitoring.' + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'microsoft.containerservice/managedclusters' + windowSize: windowSize + } +} + +resource Containers_getting_OOM_killed_for_clusterName_CI_6 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Containers getting OOM killed for ${clusterName} CI-6' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'kubernetes namespace' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'controllerName' + operator: 'Include' + values: [ + '*' + ] + } + ] + metricName: 'oomKilledContainerCount' + metricNamespace: 'Insights.Container/pods' + name: 'Metric1' + operator: 'GreaterThan' + threshold: 0 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'This alert monitors number of containers killed due to out of memory (OOM) error.' + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'microsoft.containerservice/managedclusters' + windowSize: windowSize + } +} + +resource Persistent_volume_usage_high_for_clusterName_CI_18 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Persistent volume usage high for ${clusterName} CI-18' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'podName' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'kubernetesNamespace' + operator: 'Include' + values: [ + '*' + ] + } + ] + metricName: 'pvUsageExceededPercentage' + metricNamespace: 'Insights.Container/persistentvolumes' + name: 'Metric1' + operator: 'GreaterThan' + threshold: 80 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'This alert monitors persistent volume utilization.' + enabled: false + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'microsoft.containerservice/managedclusters' + windowSize: windowSize + } +} + +resource Pods_not_in_ready_state_for_clusterName_CI_8 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Pods not in ready state for ${clusterName} CI-8' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'controllerName' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'kubernetes namespace' + operator: 'Include' + values: [ + '*' + ] + } + ] + metricName: 'PodReadyPercentage' + metricNamespace: 'Insights.Container/pods' + name: 'Metric1' + operator: 'LessThan' + threshold: 80 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'This alert monitors for excessive pods not in the ready state.' + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'microsoft.containerservice/managedclusters' + windowSize: windowSize + } +} + +resource Restarting_container_count_for_clusterName_CI_7 'Microsoft.Insights/metricAlerts@2018-03-01' = { + name: 'Restarting container count for ${clusterName} CI-7' + location: 'global' + properties: { + criteria: { + allOf: [ + { + criterionType: 'StaticThresholdCriterion' + dimensions: [ + { + name: 'kubernetes namespace' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'controllerName' + operator: 'Include' + values: [ + '*' + ] + } + ] + metricName: 'restartingContainerCount' + metricNamespace: 'Insights.Container/pods' + name: 'Metric1' + operator: 'GreaterThan' + threshold: 0 + timeAggregation: 'Average' + skipMetricValidation: true + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + description: 'This alert monitors number of containers restarting across the cluster.' + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + AksResourceId + ] + severity: alertSeverityNumber + targetResourceType: 'Microsoft.ContainerService/managedClusters' + windowSize: windowSize + } +} + +resource Container_CPU_usage_violates_the_configured_threshold_for_clustername_CI_19 'microsoft.insights/metricAlerts@2018-03-01' = { + name: 'Container CPU usage violates the configured threshold for ${clusterName} CI-19' + location: 'global' + properties: { + description: 'This alert monitors container CPU usage. It uses the threshold defined in the config map.' + severity: alertSeverityNumber + enabled: true + scopes: [ + AksResourceId + ] + evaluationFrequency: evalFrequency + windowSize: windowSize + criteria: { + allOf: [ + { + threshold: 0 + name: 'Metric1' + metricNamespace: 'Insights.Container/containers' + metricName: 'cpuThresholdViolated' + dimensions: [ + { + name: 'controllerName' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'kubernetes namespace' + operator: 'Include' + values: [ + '*' + ] + } + ] + operator: 'GreaterThan' + timeAggregation: 'Average' + skipMetricValidation: true + criterionType: 'StaticThresholdCriterion' + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + } +} + +resource Container_working_set_memory_usage_violates_the_configured_threshold_for_clustername_CI_20 'microsoft.insights/metricAlerts@2018-03-01' = { + name: 'Container working set memory usage violates the configured threshold for ${clusterName} CI-20' + location: 'global' + properties: { + description: 'This alert monitors container working set memory usage. It uses the threshold defined in the config map.' + severity: alertSeverityNumber + enabled: metricAlertsEnabled + scopes: [ + AksResourceId + ] + evaluationFrequency: evalFrequency + windowSize: windowSize + criteria: { + allOf: [ + { + threshold: 0 + name: 'Metric1' + metricNamespace: 'Insights.Container/containers' + metricName: 'memoryWorkingSetThresholdViolated' + dimensions: [ + { + name: 'controllerName' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'kubernetes namespace' + operator: 'Include' + values: [ + '*' + ] + } + ] + operator: 'GreaterThan' + timeAggregation: 'Average' + skipMetricValidation: true + criterionType: 'StaticThresholdCriterion' + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + } +} + + +resource PV_usage_violates_the_configured_threshold_for_clustername_CI_21 'microsoft.insights/metricAlerts@2018-03-01' = { + name: 'PV usage violates the configured threshold for ${clusterName} CI-21' + location: 'global' + properties: { + description: 'This alert monitors PV usage. It uses the threshold defined in the config map.' + severity: alertSeverityNumber + enabled: metricAlertsEnabled + scopes: [ + AksResourceId + ] + evaluationFrequency: evalFrequency + windowSize: windowSize + criteria: { + allOf: [ + { + threshold: 0 + name: 'Metric1' + metricNamespace: 'Insights.Container/persistentvolumes' + metricName: 'pvUsageThresholdViolated' + dimensions: [ + { + name: 'podName' + operator: 'Include' + values: [ + '*' + ] + } + { + name: 'kubernetesNamespace' + operator: 'Include' + values: [ + '*' + ] + } + ] + operator: 'GreaterThan' + timeAggregation: 'Average' + skipMetricValidation: true + criterionType: 'StaticThresholdCriterion' + } + ] + 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' + } + } +} + + +resource Daily_law_datacap 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'Daily data cap breached for workspace ${logAnalyticsWorkspaceName} CIQ-1' + location: logAnalyticsWorkspaceLocation + properties: { + displayName: 'Daily data cap breached for workspace ${logAnalyticsWorkspaceName} CIQ-1' + description: 'This alert monitors daily data cap defined on a workspace and fires when the daily data cap is breached.' + severity: 1 + enabled: metricAlertsEnabled + evaluationFrequency: evalFrequency + scopes: [ + resourceId('microsoft.operationalinsights/workspaces', logAnalyticsWorkspaceName) + ] + windowSize: windowSize + autoMitigate: false + criteria: { + allOf: [ + { + query: '_LogOperation | where Operation == "Data collection Status" | where Detail contains "OverQuota"' + timeAggregation: 'Count' + operator: 'GreaterThan' + threshold: 0 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + muteActionsDuration: 'P1D' + } +} diff --git a/templates/common/infra/bicep/core/host/aks/aksnetcontrib.bicep b/templates/common/infra/bicep/core/host/aks/aksnetcontrib.bicep new file mode 100644 index 00000000000..bae871c898d --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/aksnetcontrib.bicep @@ -0,0 +1,44 @@ +//using a seperate module file as Byo subnet scenario caters from where the subnet is in the another resource group +//name/rg required to new up an existing reference and form a dependency +//principalid required as it needs to be used to establish a unique roleassignment name +param byoAKSSubnetId string +param user_identity_principalId string + +@allowed([ + 'Subnet' + 'Vnet' +]) +param rbacAssignmentScope string = 'Subnet' + +var networkContributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7') + +var existingAksSubnetName = !empty(byoAKSSubnetId) ? split(byoAKSSubnetId, '/')[10] : '' +var existingAksVnetName = !empty(byoAKSSubnetId) ? split(byoAKSSubnetId, '/')[8] : '' + +resource existingvnet 'Microsoft.Network/virtualNetworks@2021-02-01' existing = { + name: existingAksVnetName +} +resource existingAksSubnet 'Microsoft.Network/virtualNetworks/subnets@2020-08-01' existing = { + parent: existingvnet + name: existingAksSubnetName +} + +resource subnetRbac 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (rbacAssignmentScope == 'subnet') { + name: guid(user_identity_principalId, networkContributorRole, existingAksSubnetName) + scope: existingAksSubnet + properties: { + roleDefinitionId: networkContributorRole + principalId: user_identity_principalId + principalType: 'ServicePrincipal' + } +} + +resource existingVnetRbac 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (rbacAssignmentScope != 'subnet') { + name: guid(user_identity_principalId, networkContributorRole, existingAksVnetName) + scope: existingvnet + properties: { + roleDefinitionId: networkContributorRole + principalId: user_identity_principalId + principalType: 'ServicePrincipal' + } +} diff --git a/templates/common/infra/bicep/core/host/aks/appgw.bicep b/templates/common/infra/bicep/core/host/aks/appgw.bicep new file mode 100644 index 00000000000..82c0e2a250c --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/appgw.bicep @@ -0,0 +1,196 @@ +param resourceName string +param location string +param appGwSubnetId string +param privateIpApplicationGateway string +param availabilityZones array +param userAssignedIdentity string +param workspaceId string +param appGWcount int +param appGWmaxCount int + +var appgwName = 'agw-${resourceName}' +var appgwResourceId = resourceId('Microsoft.Network/applicationGateways', '${appgwName}') + +resource appgwpip 'Microsoft.Network/publicIPAddresses@2020-07-01' = { + name: 'pip-agw-${resourceName}' + location: location + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +var frontendPublicIpConfig = { + properties: { + publicIPAddress: { + id: '${appgwpip.id}' + } + } + name: 'appGatewayFrontendIP' +} + +var frontendPrivateIpConfig = { + properties: { + privateIPAllocationMethod: 'Static' + privateIPAddress: privateIpApplicationGateway + subnet: { + id: appGwSubnetId + } + } + name: 'appGatewayPrivateIP' +} + +var tier = 'WAF_v2' +var appGWsku = union({ + name: tier + tier: tier +}, appGWmaxCount == 0 ? { + capacity: appGWcount +} : {}) + +// ugh, need to create a variable with the app gateway properies, because of the conditional key 'autoscaleConfiguration' +var appgwProperties = union({ + sku: appGWsku + gatewayIPConfigurations: [ + { + name: 'besubnet' + properties: { + subnet: { + id: appGwSubnetId + } + } + } + ] + frontendIPConfigurations: empty(privateIpApplicationGateway) ? array(frontendPublicIpConfig) : concat(array(frontendPublicIpConfig), array(frontendPrivateIpConfig)) + frontendPorts: [ + { + name: 'appGatewayFrontendPort' + properties: { + port: 80 + } + } + ] + backendAddressPools: [ + { + name: 'defaultaddresspool' + } + ] + backendHttpSettingsCollection: [ + { + name: 'defaulthttpsetting' + properties: { + port: 80 + protocol: 'Http' + cookieBasedAffinity: 'Disabled' + requestTimeout: 30 + pickHostNameFromBackendAddress: true + } + } + ] + httpListeners: [ + { + name: 'hlisten' + properties: { + frontendIPConfiguration: { + id: '${appgwResourceId}/frontendIPConfigurations/appGatewayFrontendIP' + } + frontendPort: { + id: '${appgwResourceId}/frontendPorts/appGatewayFrontendPort' + } + protocol: 'Http' + } + } + ] + requestRoutingRules: [ + { + name: 'appGwRoutingRuleName' + properties: { + ruleType: 'Basic' + httpListener: { + id: '${appgwResourceId}/httpListeners/hlisten' + } + backendAddressPool: { + id: '${appgwResourceId}/backendAddressPools/defaultaddresspool' + } + backendHttpSettings: { + id: '${appgwResourceId}/backendHttpSettingsCollection/defaulthttpsetting' + } + } + } + ] +}, appGWmaxCount > 0 ? { + autoscaleConfiguration: { + minCapacity: appGWcount + maxCapacity: appGWmaxCount + } +} : {}) + +var appGwZones = !empty(availabilityZones) ? availabilityZones : [] + +// 'identity' is always set until this is fixed: +// https://github.com/Azure/bicep/issues/387#issuecomment-885671296 +resource appgw 'Microsoft.Network/applicationGateways@2020-07-01' = if (!empty(userAssignedIdentity)) { + name: appgwName + location: location + zones: appGwZones + identity: !empty(userAssignedIdentity) ? { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity}': {} + } + } : {} + properties: appgwProperties +} + +param agicPrincipleId string +var contributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') +// https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal +resource appGwAGICContrib 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + scope: appgw + name: guid(resourceGroup().id, appgwName, 'appgwcont') + properties: { + roleDefinitionId: contributor + principalType: 'ServicePrincipal' + principalId: agicPrincipleId + } +} + +var reader = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') +resource appGwAGICRGReader 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + scope: resourceGroup() + name: guid(resourceGroup().id, appgwName, 'rgread') + properties: { + roleDefinitionId: reader + principalType: 'ServicePrincipal' + principalId: agicPrincipleId + } +} + +output appgwId string = appgw.id +output ApplicationGatewayName string = appgw.name + +// ------------------------------------------------------------------ AppGW Diagnostics +var diagProperties = { + workspaceId: workspaceId + logs: [ + { + category: 'ApplicationGatewayAccessLog' + enabled: true + } + { + category: 'ApplicationGatewayPerformanceLog' + enabled: true + } + { + category: 'ApplicationGatewayFirewallLog' + enabled: true + } + ] +} +resource appgw_Diag 'Microsoft.Insights/diagnosticSettings@2017-05-01-preview' = if (!empty(workspaceId)) { + scope: appgw + name: 'appgwDiag' + properties: diagProperties +} diff --git a/templates/common/infra/bicep/core/host/aks/bicepconfig.json b/templates/common/infra/bicep/core/host/aks/bicepconfig.json new file mode 100644 index 00000000000..a4efe888291 --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/bicepconfig.json @@ -0,0 +1,55 @@ +{ + "analyzers": { + "core": { + "enabled": true, + "verbose": false, + "rules": { + "no-hardcoded-location" : { + "level": "error" + }, + "no-loc-expr-outside-params" : { + "level": "warning" + }, + "protect-commandtoexecute-secrets" : { + "level": "warning" + }, + "explicit-values-for-loc-params" : { + "level": "warning" + }, + "adminusername-should-not-be-literal": { + "level": "warning" + }, + "no-hardcoded-env-urls": { + "level": "warning" + }, + "no-unnecessary-dependson": { + "level": "warning" + }, + "no-unused-params": { + "level": "warning" + }, + "no-unused-vars": { + "level": "warning" + }, + "outputs-should-not-contain-secrets": { + "level": "warning" + }, + "prefer-interpolation": { + "level": "warning" + }, + "secure-parameter-default": { + "level": "warning" + }, + "simplify-interpolation": { + "level": "error" + }, + "use-stable-vm-image": { + "level": "warning" + }, + "secure-secrets-in-params" : { + "level": "error" + } + } + } + } + } \ No newline at end of file diff --git a/templates/common/infra/bicep/core/host/aks/calcAzFwIp.bicep b/templates/common/infra/bicep/core/host/aks/calcAzFwIp.bicep new file mode 100644 index 00000000000..762cdb66709 --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/calcAzFwIp.bicep @@ -0,0 +1,10 @@ +// As per https://github.com/Azure/bicep/issues/2189#issuecomment-815962675 this file is being used as a UDF +// Takes a subnet range and returns the AzFirewall private Ip address + +@description('A subnet address for the Azure Firewall') +param vnetFirewallSubnetAddressPrefix string + +var subnetOctets = split(vnetFirewallSubnetAddressPrefix,'.') +var hostIdOctet = '4' + +output FirewallPrivateIp string = '${subnetOctets[0]}.${subnetOctets[1]}.${subnetOctets[2]}.${hostIdOctet}' diff --git a/templates/common/infra/bicep/core/host/aks/dnsZone.bicep b/templates/common/infra/bicep/core/host/aks/dnsZone.bicep new file mode 100644 index 00000000000..2f678304cdb --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/dnsZone.bicep @@ -0,0 +1,47 @@ +param dnsZoneName string +param principalId string +param isPrivate bool +param vnetId string = '' + +resource dns 'Microsoft.Network/dnsZones@2018-05-01' existing = if (!isPrivate) { + name: dnsZoneName +} + +resource privateDns 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (isPrivate) { + name: dnsZoneName +} + +var DNSZoneContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314') +resource dnsContributor 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (!isPrivate) { + scope: dns + name: guid(dns.id, principalId, DNSZoneContributor) + properties: { + roleDefinitionId: DNSZoneContributor + principalType: 'ServicePrincipal' + principalId: principalId + } +} + +var PrivateDNSZoneContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f') +resource privateDnsContributor 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (isPrivate) { + scope: privateDns + name: guid(privateDns.id, principalId, PrivateDNSZoneContributor) + properties: { + roleDefinitionId: PrivateDNSZoneContributor + principalType: 'ServicePrincipal' + principalId: principalId + } +} + +resource dns_vnet_link 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (isPrivate && !empty(vnetId)) { + parent: privateDns + name: 'privatedns' + tags: {} + location: 'global' + properties: { + virtualNetwork: { + id: vnetId + } + registrationEnabled: false + } +} diff --git a/templates/common/infra/bicep/core/host/aks/dnsZoneRbac.bicep b/templates/common/infra/bicep/core/host/aks/dnsZoneRbac.bicep new file mode 100644 index 00000000000..ad4ae4354a5 --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/dnsZoneRbac.bicep @@ -0,0 +1,26 @@ +//This module facilitates RBAC assigned to specific DNS Zones. +//The DNS Zone Id is extracted and the scope is set correctly. + +@description('The full Azure resource ID of the DNS zone to use for the AKS cluster') +param dnsZoneId string + +@description('The id of a virtual network to be linked to a PRIVATE DNS Zone') +param vnetId string + +@description('The AAD identity to create the RBAC against') +param principalId string + +var dnsZoneRg = !empty(dnsZoneId) ? split(dnsZoneId, '/')[4] : '' +var dnsZoneName = !empty(dnsZoneId) ? split(dnsZoneId, '/')[8] : '' +var isDnsZonePrivate = !empty(dnsZoneId) ? split(dnsZoneId, '/')[7] == 'privateDnsZones' : false + +module dnsZone './dnsZone.bicep' = if (!empty(dnsZoneId)) { + name: 'dns-${dnsZoneName}' + scope: resourceGroup(dnsZoneRg) + params: { + dnsZoneName: dnsZoneName + isPrivate: isDnsZonePrivate + vnetId : vnetId + principalId: principalId + } +} diff --git a/templates/common/infra/bicep/core/host/aks/firewall.bicep b/templates/common/infra/bicep/core/host/aks/firewall.bicep new file mode 100644 index 00000000000..2fd934ce6f6 --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/firewall.bicep @@ -0,0 +1,324 @@ +param resourceName string +param location string = resourceGroup().location +param workspaceDiagsId string = '' +param fwSubnetId string +param fwManagementSubnetId string = '' +param vnetAksSubnetAddressPrefix string +param certManagerFW bool = false +param acrPrivatePool bool = false +param acrAgentPoolSubnetAddressPrefix string = '' +param availabilityZones array = [] +param fwSku string + +var firewallPublicIpName = 'pip-afw-${resourceName}' +var firewallManagementPublicIpName = 'pip-mgmt-afw-${resourceName}' + +var managementIpConfig = { + name: 'MgmtIpConf' + properties: { + publicIPAddress: { + id: !empty(fwManagementSubnetId) ? fwManagementIp_pip.id : null + } + subnet:{ + id: !empty(fwManagementSubnetId) ? fwManagementSubnetId : null + } + } +} + +resource fw_pip 'Microsoft.Network/publicIPAddresses@2021-03-01' = { + name: firewallPublicIpName + location: location + sku: { + name: 'Standard' + } + zones: !empty(availabilityZones) ? availabilityZones : [] + properties: { + publicIPAllocationMethod: 'Static' + publicIPAddressVersion: 'IPv4' + } +} + +resource fwManagementIp_pip 'Microsoft.Network/publicIPAddresses@2021-03-01' = if(fwSku=='Basic') { + name: firewallManagementPublicIpName + location: location + sku: { + name: 'Standard' + } + zones: !empty(availabilityZones) ? availabilityZones : [] + properties: { + publicIPAllocationMethod: 'Static' + publicIPAddressVersion: 'IPv4' + } +} + +resource fwDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceDiagsId)) { + scope: fw + name: 'fwDiags' + properties: { + workspaceId: workspaceDiagsId + logs: [ + { + category: 'AzureFirewallApplicationRule' + enabled: true + retentionPolicy: { + days: 10 + enabled: false + } + } + { + category: 'AzureFirewallNetworkRule' + enabled: true + retentionPolicy: { + days: 10 + enabled: false + } + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + retentionPolicy: { + enabled: false + days: 0 + } + } + ] + } +} + +@description('Whitelist dnsZone name (required by cert-manager validation process)') +param appDnsZoneName string = '' + +var fw_name = 'afw-${resourceName}' +resource fw 'Microsoft.Network/azureFirewalls@2022-01-01' = { + name: fw_name + location: location + zones: !empty(availabilityZones) ? availabilityZones : [] + properties: { + sku: { + tier: fwSku + } + ipConfigurations: [ + { + name: 'IpConf1' + properties: { + subnet: { + id: fwSubnetId + } + publicIPAddress: { + id: fw_pip.id + } + } + } + ] + managementIpConfiguration: !empty(fwManagementSubnetId) ? managementIpConfig : null + threatIntelMode: 'Alert' + firewallPolicy: { + id: fwPolicy.id + } + applicationRuleCollections: [] + networkRuleCollections: [] + } +} + +resource fwPolicy 'Microsoft.Network/firewallPolicies@2022-01-01' = { + name: 'afwp-${resourceName}' + location: location + properties: { + sku: { + tier: fwSku + } + threatIntelMode: 'Alert' + threatIntelWhitelist: { + fqdns: [] + ipAddresses: [] + } + } +} + +resource fwpRules 'Microsoft.Network/firewallPolicies/ruleCollectionGroups@2022-01-01' = { + parent: fwPolicy + name: 'AKSConstructionRuleGroup' + properties: { + priority: 200 + ruleCollections: [ + { + ruleCollectionType: 'FirewallPolicyFilterRuleCollection' + name: 'CoreAksNetEgress' + priority: 100 + action: { + type: 'Allow' + } + rules: concat([ + { + name: 'ControlPlaneTCP' + ruleType: 'NetworkRule' + ipProtocols: [ + 'TCP' + ] + sourceAddresses: [ + vnetAksSubnetAddressPrefix + ] + destinationAddresses: [ + 'AzureCloud.${location}' + ] + destinationPorts: [ + '9000' /* For tunneled secure communication between the nodes and the control plane. */ + '22' + ] + } + { + name: 'ControlPlaneUDP' + ruleType: 'NetworkRule' + ipProtocols: [ + 'UDP' + ] + sourceAddresses: [ + vnetAksSubnetAddressPrefix + ] + destinationAddresses: [ + 'AzureCloud.${location}' + ] + destinationPorts: [ + '1194' /* For tunneled secure communication between the nodes and the control plane. */ + ] + } + { + name: 'AzureMonitorForContainers' + ruleType: 'NetworkRule' + ipProtocols: [ + 'TCP' + ] + sourceAddresses: [ + vnetAksSubnetAddressPrefix + ] + destinationAddresses: [ + 'AzureMonitor' + ] + destinationPorts: [ + '443' + ] + } + ], acrPrivatePool ? [ + { + name: 'acr-agentpool' + ruleType: 'NetworkRule' + ipProtocols: [ + 'TCP' + ] + sourceAddresses: [ + acrAgentPoolSubnetAddressPrefix + ] + destinationAddresses: [ + 'AzureKeyVault' + 'Storage' + 'EventHub' + 'AzureActiveDirectory' + 'AzureMonitor' + ] + destinationPorts: [ + '443' + ] + } + ]:[]) + } + { + ruleCollectionType: 'FirewallPolicyFilterRuleCollection' + name: 'CoreAksHttpEgress' + priority: 400 + action: { + type: 'Allow' + } + rules: concat([ + { + name: 'aks' + ruleType: 'ApplicationRule' + protocols: [ + { + port: 443 + protocolType: 'Https' + } + { + port: 80 + protocolType: 'Http' + } + ] + targetFqdns: [] + fqdnTags: [ + 'AzureKubernetesService' + ] + sourceAddresses: [ + vnetAksSubnetAddressPrefix + ] + } + ], certManagerFW ? [ + { + name: 'certman-quay' + ruleType: 'ApplicationRule' + protocols: [ + { + port: 443 + protocolType: 'Https' + } + { + port: 80 + protocolType: 'Http' + } + ] + targetFqdns: [ + 'quay.io' + '*.quay.io' + ] + sourceAddresses: [ + vnetAksSubnetAddressPrefix + ] + } + { + name: 'certman-letsencrypt' + ruleType: 'ApplicationRule' + protocols: [ + { + port: 443 + protocolType: 'Https' + } + { + port: 80 + protocolType: 'Http' + } + ] + targetFqdns: [ + 'letsencrypt.org' + '*.letsencrypt.org' + ] + sourceAddresses: [ + vnetAksSubnetAddressPrefix + ] + } + ] : [], certManagerFW && !empty(appDnsZoneName) ? [ + { + name: 'certman-appDnsZoneName' + ruleType: 'ApplicationRule' + protocols: [ + { + port: 443 + protocolType: 'Https' + } + { + port: 80 + protocolType: 'Http' + } + ] + targetFqdns: [ + appDnsZoneName + '*.${appDnsZoneName}' + ] + sourceAddresses: [ + vnetAksSubnetAddressPrefix + ] + } + ] : []) + } + ] + } +} diff --git a/templates/common/infra/bicep/core/host/aks/keyvault.bicep b/templates/common/infra/bicep/core/host/aks/keyvault.bicep new file mode 100644 index 00000000000..3a6283f3d8e --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/keyvault.bicep @@ -0,0 +1,80 @@ +@minLength(2) +@description('The location to use for the deployment. defaults to Resource Groups location.') +param location string = resourceGroup().location + +@minLength(3) +@maxLength(20) +@description('Used to name all resources') +param resourceName string + +@description('Enable support for private links') +param privateLinks bool = false + +@description('If soft delete protection is enabled') +param keyVaultSoftDelete bool = true + +@description('If purge protection is enabled') +param keyVaultPurgeProtection bool = true + +@description('Add IP to KV firewall allow-list') +param keyVaultIPAllowlist array = [] + +param logAnalyticsWorkspaceId string = '' + +var akvRawName = 'kv-${replace(resourceName, '-', '')}${uniqueString(resourceGroup().id, resourceName)}' +var akvName = length(akvRawName) > 24 ? substring(akvRawName, 0, 24) : akvRawName + +var kvIPRules = [for kvIp in keyVaultIPAllowlist: { + value: kvIp +}] + +resource kv 'Microsoft.KeyVault/vaults@2021-11-01-preview' = { + name: akvName + location: location + properties: { + tenantId: subscription().tenantId + sku: { + family: 'A' + name: 'standard' + } + // publicNetworkAccess: whether the vault will accept traffic from public internet. If set to 'disabled' all traffic except private endpoint traffic and that that originates from trusted services will be blocked. + publicNetworkAccess: privateLinks && empty(keyVaultIPAllowlist) ? 'disabled' : 'enabled' + + networkAcls: privateLinks && !empty(keyVaultIPAllowlist) ? { + bypass: 'AzureServices' + defaultAction: 'Deny' + ipRules: kvIPRules + virtualNetworkRules: [] + } : {} + + enableRbacAuthorization: true + enabledForDeployment: false + enabledForDiskEncryption: false + enabledForTemplateDeployment: false + enableSoftDelete: keyVaultSoftDelete + enablePurgeProtection: keyVaultPurgeProtection ? true : json('null') + } +} + +resource kvDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(logAnalyticsWorkspaceId)) { + name: 'kvDiags' + scope: kv + properties: { + workspaceId: logAnalyticsWorkspaceId + logs: [ + { + category: 'AuditEvent' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } +} + +output keyVaultName string = kv.name +output keyVaultId string = kv.id diff --git a/templates/common/infra/bicep/core/host/aks/keyvaultkey.bicep b/templates/common/infra/bicep/core/host/aks/keyvaultkey.bicep new file mode 100644 index 00000000000..f00bf67017a --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/keyvaultkey.bicep @@ -0,0 +1,24 @@ +param keyVaultName string + +resource kv 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = { + name: keyVaultName +} + +resource kvKmsKey 'Microsoft.KeyVault/vaults/keys@2021-11-01-preview' = { + name: 'kmskey' + parent: kv + properties: { + kty: 'RSA' + keySize: 2048 + keyOps: [ + 'wrapKey' + 'unwrapKey' + 'decrypt' + 'encrypt' + 'verify' + 'sign' + ] + } +} + +output keyVaultKmsKeyUri string = kvKmsKey.properties.keyUriWithVersion diff --git a/templates/common/infra/bicep/core/host/aks/keyvaultrbac.bicep b/templates/common/infra/bicep/core/host/aks/keyvaultrbac.bicep new file mode 100644 index 00000000000..6ef65dd1e08 --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/keyvaultrbac.bicep @@ -0,0 +1,173 @@ +param keyVaultName string + +@description('An array of Service Principal IDs') +#disable-next-line secure-secrets-in-params //Disabling validation of this linter rule as param does not contain a secret. +param rbacSecretUserSps array = [] + +@description('An array of Service Principal IDs') +#disable-next-line secure-secrets-in-params //Disabling validation of this linter rule as param does not contain a secret. +param rbacSecretOfficerSps array = [] + +@description('An array of Service Principal IDs') +param rbacCertOfficerSps array = [] + +@description('An array of Service Principal IDs') +param rbacCryptoUserSps array = [] + +@description('An array of Service Principal IDs') +param rbacCryptoOfficerSps array = [] + +@description('An array of Service Principal IDs') +param rbacCryptoServiceEncryptSps array = [] + +@description('An array of Service Principal IDs') +param rbacKvContributorSps array = [] + +@description('An array of Service Principal IDs') +param rbacAdminSps array = [] + +@description('An array of User IDs') +param rbacCryptoOfficerUsers array = [] + +@description('An array of User IDs') +#disable-next-line secure-secrets-in-params //Disabling validation of this linter rule as param does not contain a secret. +param rbacSecretOfficerUsers array = [] + +@description('An array of User IDs') +param rbacCertOfficerUsers array = [] + +@description('An array of User IDs') +param rbacAdminUsers array = [] + +var keyVaultAdministratorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483') +var keyVaultContributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f25e0fa2-a7c8-4377-a976-54943a77a395') +var keyVaultSecretsUserRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') +var keyVaultSecretsOfficerRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7') +var keyVaultCertsOfficerRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a4417e6f-fecd-4de8-b567-7b0420556985') +var keyVaultCryptoUserRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '12338af0-0e69-4776-bea7-57ae8d297424') +var keyVaultCryptoOfficerRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '14b46e9e-c2b7-41b4-b07b-48a6ebf60603') +var keyVaultCryptoServiceEncrpytionRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions','e147488a-f6f5-4113-8e2d-b22465e65bf6') + +resource kv 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = { + name: keyVaultName +} + +resource rbacSecretUserSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacSecretUserSps : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultSecretsUserRole) + properties: { + roleDefinitionId: keyVaultSecretsUserRole + principalType: 'ServicePrincipal' + principalId: rbacSp + } +}] + +resource rbacSecretOfficerSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacSecretOfficerSps : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultSecretsOfficerRole) + properties: { + roleDefinitionId: keyVaultSecretsOfficerRole + principalType: 'ServicePrincipal' + principalId: rbacSp + } +}] + +resource rbacCertsOfficerSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCertOfficerSps : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultCertsOfficerRole) + properties: { + roleDefinitionId: keyVaultCertsOfficerRole + principalType: 'ServicePrincipal' + principalId: rbacSp + } +}] + +resource rbacCryptoUserSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCryptoUserSps : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultCryptoUserRole) + properties: { + roleDefinitionId: keyVaultCryptoUserRole + principalType: 'ServicePrincipal' + principalId: rbacSp + } +}] + +resource rbacCryptoServiceEncryptionSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCryptoServiceEncryptSps : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultCryptoServiceEncrpytionRole) + properties: { + roleDefinitionId: keyVaultCryptoServiceEncrpytionRole + principalType: 'ServicePrincipal' + principalId: rbacSp + } +}] + +resource rbacKvContributorSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacKvContributorSps : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultContributorRole) + properties: { + roleDefinitionId: keyVaultContributorRole + principalType: 'ServicePrincipal' + principalId: rbacSp + } +}] + +resource rbacCryptoOfficerSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCryptoOfficerSps : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultCryptoUserRole) + properties: { + roleDefinitionId: keyVaultCryptoOfficerRole + principalType: 'ServicePrincipal' + principalId: rbacSp + } +}] + +resource rbacAdminSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacAdminSps : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultAdministratorRole) + properties: { + roleDefinitionId: keyVaultAdministratorRole + principalType: 'ServicePrincipal' + principalId: rbacSp + } +}] + +resource rbacCryptoOfficerUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCryptoOfficerUsers : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultCryptoOfficerRole) + properties: { + roleDefinitionId: keyVaultCryptoOfficerRole + principalType: 'User' + principalId: rbacSp + } +}] + +resource rbacSecretOfficerUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacSecretOfficerUsers : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultSecretsOfficerRole) + properties: { + roleDefinitionId: keyVaultSecretsOfficerRole + principalType: 'User' + principalId: rbacSp + } +}] + +resource rbacCertsOfficerUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCertOfficerUsers : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultCertsOfficerRole) + properties: { + roleDefinitionId: keyVaultCertsOfficerRole + principalType: 'User' + principalId: rbacSp + } +}] + +resource rbacAdminUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacAdminUsers : if(!empty(rbacSp)) { + scope: kv + name: guid(kv.id, rbacSp, keyVaultAdministratorRole) + properties: { + roleDefinitionId: keyVaultAdministratorRole + principalType: 'User' + principalId: rbacSp + } +}] diff --git a/templates/common/infra/bicep/core/host/aks/main.bicep b/templates/common/infra/bicep/core/host/aks/main.bicep new file mode 100644 index 00000000000..47553d34e04 --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/main.bicep @@ -0,0 +1,1627 @@ +@minLength(2) +@description('The location to use for the deployment. defaults to Resource Groups location.') +param location string = resourceGroup().location + +@minLength(3) +@maxLength(20) +@description('Used to name all resources') +param resourceName string + +/* +Resource sections +1. Networking +2. DNS +3. Key Vault +4. Container Registry +5. Firewall +6. Application Gateway +7. AKS +8. Monitoring / Log Analytics +9. Deployment for telemetry +*/ + + +/*.__ __. _______ .___________.____ __ ____ ______ .______ __ ___ __ .__ __. _______ +| \ | | | ____|| |\ \ / \ / / / __ \ | _ \ | |/ / | | | \ | | / _____| +| \| | | |__ `---| |----` \ \/ \/ / | | | | | |_) | | ' / | | | \| | | | __ +| . ` | | __| | | \ / | | | | | / | < | | | . ` | | | |_ | +| |\ | | |____ | | \ /\ / | `--' | | |\ \----.| . \ | | | |\ | | |__| | +|__| \__| |_______| |__| \__/ \__/ \______/ | _| `._____||__|\__\ |__| |__| \__| \______| */ +//Networking can either be one of: custom / byo / default + +@description('Are you providing your own vNet CIDR blocks') +param custom_vnet bool = false + +@description('Full resource id path of an existing subnet to use for AKS') +param byoAKSSubnetId string = '' + +@description('Full resource id path of an existing subnet to use for Application Gateway') +param byoAGWSubnetId string = '' + +//--- Custom, BYO networking and PrivateApiZones requires BYO AKS User Identity +var createAksUai = custom_vnet || !empty(byoAKSSubnetId) || !empty(dnsApiPrivateZoneId) || keyVaultKmsCreateAndPrereqs || !empty(keyVaultKmsByoKeyId) +resource aksUai 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' = if (createAksUai) { + name: 'id-aks-${resourceName}' + location: location +} + +//----------------------------------------------------- BYO Vnet +var existingAksVnetRG = !empty(byoAKSSubnetId) ? (length(split(byoAKSSubnetId, '/')) > 4 ? split(byoAKSSubnetId, '/')[4] : '') : '' + +module aksnetcontrib './aksnetcontrib.bicep' = if (!empty(byoAKSSubnetId) && createAksUai) { + name: 'addAksNetContributor' + scope: resourceGroup(existingAksVnetRG) + params: { + byoAKSSubnetId: byoAKSSubnetId + user_identity_principalId: createAksUai ? aksUai.properties.principalId : '' + rbacAssignmentScope: uaiNetworkScopeRbac + } +} + +//------------------------------------------------------ Create custom vnet +@minLength(9) +@maxLength(18) +@description('The address range for the custom vnet') +param vnetAddressPrefix string = '10.240.0.0/16' + +@minLength(9) +@maxLength(18) +@description('The address range for AKS in your custom vnet') +param vnetAksSubnetAddressPrefix string = '10.240.0.0/22' + +@minLength(9) +@maxLength(18) +@description('The address range for the App Gateway in your custom vnet') +param vnetAppGatewaySubnetAddressPrefix string = '10.240.5.0/24' + +@minLength(9) +@maxLength(18) +@description('The address range for the ACR in your custom vnet') +param acrAgentPoolSubnetAddressPrefix string = '10.240.4.64/26' + +@minLength(9) +@maxLength(18) +@description('The address range for Azure Bastion in your custom vnet') +param bastionSubnetAddressPrefix string = '10.240.4.128/26' + +@minLength(9) +@maxLength(18) +@description('The address range for private link in your custom vnet') +param privateLinkSubnetAddressPrefix string = '10.240.4.192/26' + +@minLength(9) +@maxLength(18) +@description('The address range for Azure Firewall in your custom vnet') +param vnetFirewallSubnetAddressPrefix string = '10.240.50.0/24' + +@minLength(9) +@maxLength(18) +@description('The address range for Azure Firewall Management in your custom vnet') +param vnetFirewallManagementSubnetAddressPrefix string = '10.240.51.0/26' + +@description('Enable support for private links (required custom_vnet)') +param privateLinks bool = false + +@description('Enable support for ACR private pool') +param acrPrivatePool bool = false + +@description('Deploy Azure Bastion to your vnet. (works with Custom Networking only, not BYO)') +param bastion bool = false + +@description('Deploy NSGs to your vnet subnets. (works with Custom Networking only, not BYO)') +param CreateNetworkSecurityGroups bool = false + +@description('Configure Flow Logs for Network Security Groups in the NetworkWatcherRG resource group. Requires Contributor RBAC on NetworkWatcherRG and Reader on Subscription.') +param CreateNetworkSecurityGroupFlowLogs bool = false + +module network './network.bicep' = if (custom_vnet) { + name: 'network' + params: { + resourceName: resourceName + location: location + networkPluginIsKubenet: networkPlugin=='kubenet' + vnetAddressPrefix: vnetAddressPrefix + aksPrincipleId: createAksUai ? aksUai.properties.principalId : '' + vnetAksSubnetAddressPrefix: vnetAksSubnetAddressPrefix + ingressApplicationGateway: ingressApplicationGateway + vnetAppGatewaySubnetAddressPrefix: vnetAppGatewaySubnetAddressPrefix + azureFirewalls: azureFirewalls + vnetFirewallSubnetAddressPrefix: vnetFirewallSubnetAddressPrefix + vnetFirewallManagementSubnetAddressPrefix: vnetFirewallManagementSubnetAddressPrefix + privateLinks: privateLinks + privateLinkSubnetAddressPrefix: privateLinkSubnetAddressPrefix + privateLinkAcrId: privateLinks && !empty(registries_sku) ? acr.id : '' + privateLinkAkvId: privateLinks && keyVaultCreate ? kv.outputs.keyVaultId : '' + acrPrivatePool: acrPrivatePool + acrAgentPoolSubnetAddressPrefix: acrAgentPoolSubnetAddressPrefix + bastion: bastion + bastionSubnetAddressPrefix: bastionSubnetAddressPrefix + availabilityZones: availabilityZones + workspaceName: createLaw ? aks_law.name : '' + workspaceResourceGroupName: createLaw ? resourceGroup().name : '' + networkSecurityGroups: CreateNetworkSecurityGroups + CreateNsgFlowLogs: CreateNetworkSecurityGroups && CreateNetworkSecurityGroupFlowLogs + ingressApplicationGatewayPublic: empty(privateIpApplicationGateway) + natGateway: createNatGateway + natGatewayIdleTimeoutMins: natGwIdleTimeout + natGatewayPublicIps: natGwIpCount + } +} +output CustomVnetId string = custom_vnet ? network.outputs.vnetId : '' +output CustomVnetPrivateLinkSubnetId string = custom_vnet ? network.outputs.privateLinkSubnetId : '' + +var aksSubnetId = custom_vnet ? network.outputs.aksSubnetId : byoAKSSubnetId +var appGwSubnetId = ingressApplicationGateway ? (custom_vnet ? network.outputs.appGwSubnetId : byoAGWSubnetId) : '' + + +/*______ .__ __. _______. ________ ______ .__ __. _______ _______. +| \ | \ | | / | | / / __ \ | \ | | | ____| / | +| .--. || \| | | (----` `---/ / | | | | | \| | | |__ | (----` +| | | || . ` | \ \ / / | | | | | . ` | | __| \ \ +| '--' || |\ | .----) | / /----.| `--' | | |\ | | |____ .----) | +|_______/ |__| \__| |_______/ /________| \______/ |__| \__| |_______||_______/ */ + +@description('The full Azure resource ID of the DNS zone to use for the AKS cluster') +param dnsZoneId string = '' +var isDnsZonePrivate = !empty(dnsZoneId) ? split(dnsZoneId, '/')[7] == 'privateDnsZones' : false + +module dnsZone './dnsZoneRbac.bicep' = if (!empty(dnsZoneId)) { + name: 'addDnsContributor' + params: { + dnsZoneId: dnsZoneId + vnetId: isDnsZonePrivate ? (!empty(byoAKSSubnetId) ? split(byoAKSSubnetId, '/subnets')[0] : (custom_vnet ? network.outputs.vnetId : '')) : '' + principalId: any(aks.properties.identityProfile.kubeletidentity).objectId + } +} + + +/*__ __ _______ ____ ____ ____ ____ ___ __ __ __ .___________. +| |/ / | ____|\ \ / / \ \ / / / \ | | | | | | | | +| ' / | |__ \ \/ / \ \/ / / ^ \ | | | | | | `---| |----` +| < | __| \_ _/ \ / / /_\ \ | | | | | | | | +| . \ | |____ | | \ / / _____ \ | `--' | | `----. | | +|__|\__\ |_______| |__| \__/ /__/ \__\ \______/ |_______| |__| */ + +@description('Creates a KeyVault') +param keyVaultCreate bool = false + +@description('If soft delete protection is enabled') +param keyVaultSoftDelete bool = true + +@description('If purge protection is enabled') +param keyVaultPurgeProtection bool = true + +@description('Add IP to KV firewall allow-list') +param keyVaultIPAllowlist array = [] + +@description('Installs the AKS KV CSI provider') +param keyVaultAksCSI bool = false + +@description('Rotation poll interval for the AKS KV CSI provider') +param keyVaultAksCSIPollInterval string = '2m' + +@description('Creates a KeyVault for application secrets (eg. CSI)') +module kv 'keyvault.bicep' = if(keyVaultCreate) { + name: 'keyvaultApps' + params: { + resourceName: resourceName + keyVaultPurgeProtection: keyVaultPurgeProtection + keyVaultSoftDelete: keyVaultSoftDelete + keyVaultIPAllowlist: keyVaultIPAllowlist + location: location + privateLinks: privateLinks + } +} + +@description('The principal ID of the user or service principal that requires access to the Key Vault. Set automatedDeployment to toggle between user and service prinicpal') +param keyVaultOfficerRolePrincipalId string = '' +var keyVaultOfficerRolePrincipalIds = [ + keyVaultOfficerRolePrincipalId +] + +@description('Parsing an array with union ensures that duplicates are removed, which is great when dealing with highly conditional elements') +var rbacSecretUserSps = union([deployAppGw && appgwKVIntegration ? appGwIdentity.properties.principalId : ''],[keyVaultAksCSI ? aks.properties.addonProfiles.azureKeyvaultSecretsProvider.identity.objectId : '']) + +@description('A seperate module is used for RBAC to avoid delaying the KeyVault creation and causing a circular reference.') +module kvRbac 'keyvaultrbac.bicep' = if (keyVaultCreate) { + name: 'KeyVaultAppsRbac' + params: { + keyVaultName: keyVaultCreate ? kv.outputs.keyVaultName : '' + + //service principals + rbacSecretUserSps: rbacSecretUserSps + rbacSecretOfficerSps: !empty(keyVaultOfficerRolePrincipalId) && automatedDeployment ? keyVaultOfficerRolePrincipalIds : [] + rbacCertOfficerSps: !empty(keyVaultOfficerRolePrincipalId) && automatedDeployment ? keyVaultOfficerRolePrincipalIds : [] + + //users + rbacSecretOfficerUsers: !empty(keyVaultOfficerRolePrincipalId) && !automatedDeployment ? keyVaultOfficerRolePrincipalIds : [] + rbacCertOfficerUsers: !empty(keyVaultOfficerRolePrincipalId) && !automatedDeployment ? keyVaultOfficerRolePrincipalIds : [] + } +} + +output keyVaultName string = keyVaultCreate ? kv.outputs.keyVaultName : '' +output keyVaultId string = keyVaultCreate ? kv.outputs.keyVaultId : '' + + +/* KeyVault for KMS Etcd*/ + +@description('Enable encryption at rest for Kubernetes etcd data') +param keyVaultKmsCreate bool = false + +@description('Bring an existing Key from an existing Key Vault') +param keyVaultKmsByoKeyId string = '' + +@description('The resource group for the existing KMS Key Vault') +param keyVaultKmsByoRG string = resourceGroup().name + +@description('The PrincipalId of the deploying user, which is necessary when creating a Kms Key') +param keyVaultKmsOfficerRolePrincipalId string = '' + +@description('The extracted name of the existing Key Vault') +var keyVaultKmsByoName = !empty(keyVaultKmsByoKeyId) ? split(split(keyVaultKmsByoKeyId,'/')[2],'.')[0] : '' + +@description('The deployment delay to introduce when creating a new keyvault for kms key') +var kmsRbacWaitSeconds=30 + +@description('This indicates if the deploying user has provided their PrincipalId in order for the key to be created') +var keyVaultKmsCreateAndPrereqs = keyVaultKmsCreate && !empty(keyVaultKmsOfficerRolePrincipalId) && privateLinks == false + +resource kvKmsByo 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = if(!empty(keyVaultKmsByoName)) { + name: keyVaultKmsByoName + scope: resourceGroup(keyVaultKmsByoRG) +} + +@description('Creates a new Key vault for a new KMS Key') +module kvKms 'keyvault.bicep' = if(keyVaultKmsCreateAndPrereqs) { + name: 'keyvaultKms-${resourceName}' + params: { + resourceName: 'kms${resourceName}' + keyVaultPurgeProtection: keyVaultPurgeProtection + keyVaultSoftDelete: keyVaultSoftDelete + location: location + privateLinks: privateLinks + //aksUaiObjectId: aksUai.properties.principalId + } +} + +module kvKmsCreatedRbac 'keyvaultrbac.bicep' = if(keyVaultKmsCreateAndPrereqs) { + name: 'keyvaultKmsRbacs-${resourceName}' + params: { + keyVaultName: keyVaultKmsCreate ? kvKms.outputs.keyVaultName : '' + //We can't create a kms kv and key and do privatelink. Private Link is a BYO scenario + // rbacKvContributorSps : [ + // createAksUai && privateLinks ? aksUai.properties.principalId : '' + // ] + //This allows the Aks Cluster to access the key vault key + rbacCryptoUserSps: [ + createAksUai ? aksUai.properties.principalId : '' + ] + //This allows the Deploying user to create the key vault key + rbacCryptoOfficerUsers: [ + !empty(keyVaultKmsOfficerRolePrincipalId) && !automatedDeployment ? keyVaultKmsOfficerRolePrincipalId : '' + ] + //This allows the Deploying sp to create the key vault key + rbacCryptoOfficerSps: [ + !empty(keyVaultKmsOfficerRolePrincipalId) && automatedDeployment ? keyVaultKmsOfficerRolePrincipalId : '' + ] + } +} + +module kvKmsByoRbac 'keyvaultrbac.bicep' = if(!empty(keyVaultKmsByoKeyId)) { + name: 'keyvaultKmsByoRbacs-${resourceName}' + scope: resourceGroup(keyVaultKmsByoRG) + params: { + keyVaultName: kvKmsByo.name + //Contribuor allows AKS to create the private link + rbacKvContributorSps : [ + createAksUai && privateLinks ? aksUai.properties.principalId : '' + ] + //This allows the Aks Cluster to access the key vault key + rbacCryptoUserSps: [ + createAksUai ? aksUai.properties.principalId : '' + ] + } +} + +@description('It can take time for the RBAC to propagate, this delays the deployment to avoid this problem') +module waitForKmsRbac 'br/public:deployment-scripts/wait:1.0.1' = if(keyVaultKmsCreateAndPrereqs && kmsRbacWaitSeconds>0) { + name: 'keyvaultKmsRbac-waits-${resourceName}' + params: { + waitSeconds: kmsRbacWaitSeconds + location: location + } + dependsOn: [ + kvKmsCreatedRbac + ] +} + +@description('Adding a key to the keyvault... We can only do this for public key vaults') +module kvKmsKey 'keyvaultkey.bicep' = if(keyVaultKmsCreateAndPrereqs) { + name: 'keyvaultKmsKeys-${resourceName}' + params: { + keyVaultName: keyVaultKmsCreateAndPrereqs ? kvKms.outputs.keyVaultName : '' + } + dependsOn: [waitForKmsRbac] +} + +var azureKeyVaultKms = { + securityProfile : { + azureKeyVaultKms : { + enabled: keyVaultKmsCreateAndPrereqs || !empty(keyVaultKmsByoKeyId) + keyId: keyVaultKmsCreateAndPrereqs ? kvKmsKey.outputs.keyVaultKmsKeyUri : !empty(keyVaultKmsByoKeyId) ? keyVaultKmsByoKeyId : '' + keyVaultNetworkAccess: privateLinks ? 'private' : 'public' + keyVaultResourceId: privateLinks && !empty(keyVaultKmsByoKeyId) ? kvKmsByo.id : '' + } + } +} + +@description('The name of the Kms Key Vault') +output keyVaultKmsName string = keyVaultKmsCreateAndPrereqs ? kvKms.outputs.keyVaultName : !empty(keyVaultKmsByoKeyId) ? keyVaultKmsByoName : '' + +@description('Indicates if the user has supplied all the correct parameter to use a AKSC Created KMS') +output kmsCreatePrerequisitesMet bool = keyVaultKmsCreateAndPrereqs + + +/* ___ ______ .______ + / \ / | | _ \ + / ^ \ | ,----' | |_) | + / /_\ \ | | | / + / _____ \ __| `----. __ | |\ \----. __ +/__/ \__\ (__)\______|(__)| _| `._____|(__)*/ + +@allowed([ + '' + 'Basic' + 'Standard' + 'Premium' +]) +@description('The SKU to use for the Container Registry') +param registries_sku string = '' + +@description('Enable the ACR Content Trust Policy, SKU must be set to Premium') +param enableACRTrustPolicy bool = false +var acrContentTrustEnabled = enableACRTrustPolicy && registries_sku == 'Premium' ? 'enabled' : 'disabled' + +//param enableACRZoneRedundancy bool = true +var acrZoneRedundancyEnabled = !empty(availabilityZones) && registries_sku == 'Premium' ? 'Enabled' : 'Disabled' + +@description('Enable removing of untagged manifests from ACR') +param acrUntaggedRetentionPolicyEnabled bool = false + +@description('The number of days to retain untagged manifests for') +param acrUntaggedRetentionPolicy int = 30 + +var acrName = 'cr${replace(resourceName, '-', '')}${uniqueString(resourceGroup().id, resourceName)}' + +resource acr 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = if (!empty(registries_sku)) { + name: acrName + location: location + sku: { + #disable-next-line BCP036 //Disabling validation of this parameter to cope with empty string to indicate no ACR required. + name: registries_sku + } + properties: { + policies: { + trustPolicy: enableACRTrustPolicy ? { + status: acrContentTrustEnabled + type: 'Notary' + } : {} + retentionPolicy: acrUntaggedRetentionPolicyEnabled ? { + status: 'enabled' + days: acrUntaggedRetentionPolicy + } : json('null') + } + publicNetworkAccess: privateLinks /* && empty(acrIPWhitelist)*/ ? 'Disabled' : 'Enabled' + zoneRedundancy: acrZoneRedundancyEnabled + /* + networkRuleSet: { + defaultAction: 'Deny' + + ipRules: empty(acrIPWhitelist) ? [] : [ + { + action: 'Allow' + value: acrIPWhitelist + } + ] + virtualNetworkRules: [] + } + */ + } +} +output containerRegistryName string = !empty(registries_sku) ? acr.name : '' +output containerRegistryId string = !empty(registries_sku) ? acr.id : '' + + +resource acrDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (createLaw && !empty(registries_sku)) { + name: 'acrDiags' + scope: acr + properties: { + workspaceId:aks_law.id + logs: [ + { + category: 'ContainerRegistryRepositoryEvents' + enabled: true + } + { + category: 'ContainerRegistryLoginEvents' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + timeGrain: 'PT1M' + } + ] + } +} + +//resource acrPool 'Microsoft.ContainerRegistry/registries/agentPools@2019-06-01-preview' = if (custom_vnet && (!empty(registries_sku)) && privateLinks && acrPrivatePool) { +module acrPool 'acragentpool.bicep' = if (custom_vnet && (!empty(registries_sku)) && privateLinks && acrPrivatePool) { + name: 'acrprivatepool' + params: { + acrName: acr.name + acrPoolSubnetId: custom_vnet ? network.outputs.acrPoolSubnetId : '' + location: location + } +} + +var AcrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') +var KubeletObjectId = any(aks.properties.identityProfile.kubeletidentity).objectId + +resource aks_acr_pull 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(registries_sku)) { + scope: acr // Use when specifying a scope that is different than the deployment scope + name: guid(aks.id, 'Acr' , AcrPullRole) + properties: { + roleDefinitionId: AcrPullRole + principalType: 'ServicePrincipal' + principalId: KubeletObjectId + } +} + +var AcrPushRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec') + +@description('The principal ID of the service principal to assign the push role to the ACR') +param acrPushRolePrincipalId string = '' + +resource aks_acr_push 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(registries_sku) && !empty(acrPushRolePrincipalId)) { + scope: acr // Use when specifying a scope that is different than the deployment scope + name: guid(aks.id, 'Acr' , AcrPushRole) + properties: { + roleDefinitionId: AcrPushRole + principalType: automatedDeployment ? 'ServicePrincipal' : 'User' + principalId: acrPushRolePrincipalId + } +} + + +param imageNames array = [] + +module acrImport 'br/public:deployment-scripts/import-acr:2.0.1' = if (!empty(registries_sku) && !empty(imageNames)) { + name: 'testAcrImportMulti' + params: { + acrName: acr.name + location: location + images: imageNames + } +} + + + + +/*______ __ .______ _______ ____ __ ____ ___ __ __ +| ____|| | | _ \ | ____|\ \ / \ / / / \ | | | | +| |__ | | | |_) | | |__ \ \/ \/ / / ^ \ | | | | +| __| | | | / | __| \ / / /_\ \ | | | | +| | | | | |\ \----.| |____ \ /\ / / _____ \ | `----.| `----. +|__| |__| | _| `._____||_______| \__/ \__/ /__/ \__\ |_______||_______|*/ + +@description('Create an Azure Firewall, requires custom_vnet') +param azureFirewalls bool = false + +@description('Add application rules to the firewall for certificate management.') +param certManagerFW bool = false + +// @allowed([ +// 'AllowAllIn' +// 'AllowAcrSubnetIn' +// '' +// ]) +// @description('Allow Http traffic (80/443) into AKS from specific sources') +// param inboundHttpFW string = '' + +@allowed([ + 'Basic' + 'Premium' + 'Standard' +]) +param azureFirewallSku string = 'Standard' + +module firewall './firewall.bicep' = if (azureFirewalls && custom_vnet) { + name: 'firewall' + params: { + resourceName: resourceName + location: location + workspaceDiagsId: createLaw ? aks_law.id : '' + fwSubnetId: azureFirewalls && custom_vnet ? network.outputs.fwSubnetId : '' + fwSku: azureFirewallSku + fwManagementSubnetId: azureFirewalls && custom_vnet && azureFirewallSku=='Basic' ? network.outputs.fwMgmtSubnetId : '' + vnetAksSubnetAddressPrefix: vnetAksSubnetAddressPrefix + certManagerFW: certManagerFW + appDnsZoneName: !empty(dnsZoneId) ? split(dnsZoneId, '/')[8] : '' + acrPrivatePool: acrPrivatePool + acrAgentPoolSubnetAddressPrefix: acrAgentPoolSubnetAddressPrefix + // inboundHttpFW: inboundHttpFW + availabilityZones: availabilityZones + } +} + +/* ___ .______ .______ _______ ____ __ ____ + / \ | _ \ | _ \ / _____|\ \ / \ / / + / ^ \ | |_) | | |_) | | | __ \ \/ \/ / + / /_\ \ | ___/ | ___/ | | |_ | \ / + / _____ \ | | | | __ | |__| | \ /\ / __ +/__/ \__\ | _| | _| (__) \______| \__/ \__/ (__)*/ + +@description('Create an Application Gateway') +param ingressApplicationGateway bool = false + +@description('The number of applciation gateway instances') +param appGWcount int = 2 + +@description('The maximum number of application gateway instances') +param appGWmaxCount int = 0 + +@maxLength(15) +@description('A known private ip in the Application Gateway subnet range to be allocated for internal traffic') +param privateIpApplicationGateway string = '' + +@description('Enable key vault integration for application gateway') +param appgwKVIntegration bool = false + +@allowed([ + 'Standard_v2' + 'WAF_v2' +]) +@description('The SKU for AppGw') +param appGWsku string = 'WAF_v2' + +@description('Enable the WAF Firewall, valid for WAF_v2 SKUs') +param appGWenableFirewall bool = true + +var deployAppGw = ingressApplicationGateway && (custom_vnet || !empty(byoAGWSubnetId)) +var appGWenableWafFirewall = appGWsku=='Standard_v2' ? false : appGWenableFirewall + +// If integrating App Gateway with KeyVault, create a Identity App Gateway will use to access keyvault +// 'identity' is always created (adding: "|| deployAppGw") until this is fixed: +// https://github.com/Azure/bicep/issues/387#issuecomment-885671296 +resource appGwIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = if (deployAppGw) { + name: 'id-appgw-${resourceName}' + location: location +} + +var appgwName = 'agw-${resourceName}' +var appgwResourceId = deployAppGw ? resourceId('Microsoft.Network/applicationGateways', '${appgwName}') : '' + +resource appgwpip 'Microsoft.Network/publicIPAddresses@2021-02-01' = if (deployAppGw) { + name: 'pip-agw-${resourceName}' + location: location + sku: { + name: 'Standard' + } + zones: !empty(availabilityZones) ? availabilityZones : [] + properties: { + publicIPAllocationMethod: 'Static' + } +} + +var frontendPublicIpConfig = { + properties: { + publicIPAddress: { + id: appgwpip.id + } + } + name: 'appGatewayFrontendIP' +} + +var frontendPrivateIpConfig = { + properties: { + privateIPAllocationMethod: 'Static' + privateIPAddress: privateIpApplicationGateway + subnet: { + id: appGwSubnetId + } + } + name: 'appGatewayPrivateIP' +} + +@allowed([ + 'Prevention' + 'Detection' +]) +param appGwFirewallMode string = 'Prevention' + +var appGwFirewallConfigOwasp = { + enabled: appGWenableWafFirewall + firewallMode: appGwFirewallMode + ruleSetType: 'OWASP' + ruleSetVersion: '3.2' + requestBodyCheck: true + maxRequestBodySizeInKb: 128 + disabledRuleGroups: [] +} + +var appGWskuObj = union({ + name: appGWsku + tier: appGWsku +}, appGWmaxCount == 0 ? { + capacity: appGWcount +} : {}) + +// ugh, need to create a variable with the app gateway properies, because of the conditional key 'autoscaleConfiguration' +var appgwProperties = union({ + sku: appGWskuObj + sslPolicy: { + policyType: 'Predefined' + policyName: 'AppGwSslPolicy20170401S' + } + webApplicationFirewallConfiguration: appGWenableWafFirewall ? appGwFirewallConfigOwasp : json('null') + gatewayIPConfigurations: [ + { + name: 'besubnet' + properties: { + subnet: { + id: appGwSubnetId + } + } + } + ] + frontendIPConfigurations: empty(privateIpApplicationGateway) ? array(frontendPublicIpConfig) : concat(array(frontendPublicIpConfig), array(frontendPrivateIpConfig)) + frontendPorts: [ + { + name: 'appGatewayFrontendPort' + properties: { + port: 80 + } + } + ] + backendAddressPools: [ + { + name: 'defaultaddresspool' + } + ] + backendHttpSettingsCollection: [ + { + name: 'defaulthttpsetting' + properties: { + port: 80 + protocol: 'Http' + cookieBasedAffinity: 'Disabled' + requestTimeout: 30 + pickHostNameFromBackendAddress: true + } + } + ] + httpListeners: [ + { + name: 'hlisten' + properties: { + frontendIPConfiguration: { + id: empty(privateIpApplicationGateway) ? '${appgwResourceId}/frontendIPConfigurations/appGatewayFrontendIP' : '${appgwResourceId}/frontendIPConfigurations/appGatewayPrivateIP' + } + frontendPort: { + id: '${appgwResourceId}/frontendPorts/appGatewayFrontendPort' + } + protocol: 'Http' + } + } + ] + requestRoutingRules: [ + { + name: 'appGwRoutingRuleName' + properties: { + ruleType: 'Basic' + httpListener: { + id: '${appgwResourceId}/httpListeners/hlisten' + } + backendAddressPool: { + id: '${appgwResourceId}/backendAddressPools/defaultaddresspool' + } + backendHttpSettings: { + id: '${appgwResourceId}/backendHttpSettingsCollection/defaulthttpsetting' + } + } + } + ] +}, appGWmaxCount > 0 ? { + autoscaleConfiguration: { + minCapacity: appGWcount + maxCapacity: appGWmaxCount + } +} : {}) + +// 'identity' is always set until this is fixed: https://github.com/Azure/bicep/issues/387#issuecomment-885671296 +resource appgw 'Microsoft.Network/applicationGateways@2021-02-01' = if (deployAppGw) { + name: appgwName + location: location + zones: !empty(availabilityZones) ? availabilityZones : [] + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${appGwIdentity.id}': {} + } + } + properties: appgwProperties +} + +var contributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') +// https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal +// AGIC's identity requires "Contributor" permission over Application Gateway. +resource appGwAGICContrib 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (ingressApplicationGateway && deployAppGw) { + scope: appgw + name: guid(aks.id, 'Agic', contributor) + properties: { + roleDefinitionId: contributor + principalType: 'ServicePrincipal' + principalId: aks.properties.addonProfiles.ingressApplicationGateway.identity.objectId + } +} + +// AGIC's identity requires "Reader" permission over Application Gateway's resource group. +var reader = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') +resource appGwAGICRGReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (ingressApplicationGateway && deployAppGw) { + scope: resourceGroup() + name: guid(aks.id, 'Agic', reader) + properties: { + roleDefinitionId: reader + principalType: 'ServicePrincipal' + principalId: aks.properties.addonProfiles.ingressApplicationGateway.identity.objectId + } +} + +// AGIC's identity requires "Managed Identity Operator" permission over the user assigned identity of Application Gateway. +var managedIdentityOperator = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f1a07417-d97a-45cb-824c-7a7467783830') +resource appGwAGICMIOp 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (ingressApplicationGateway && deployAppGw) { + scope: appGwIdentity + name: guid(aks.id, 'Agic', managedIdentityOperator) + properties: { + roleDefinitionId: managedIdentityOperator + principalType: 'ServicePrincipal' + principalId: aks.properties.addonProfiles.ingressApplicationGateway.identity.objectId + } +} + +// AppGW Diagnostics +resource appgw_Diag 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (createLaw && deployAppGw) { + scope: appgw + name: 'appgwDiag' + properties: { + workspaceId: aks_law.id + logs: [ + { + category: 'ApplicationGatewayAccessLog' + enabled: true + } + { + category: 'ApplicationGatewayPerformanceLog' + enabled: true + } + { + category: 'ApplicationGatewayFirewallLog' + enabled: true + } + ] + } +} + +// ================================================= + +// Prevent error: AGIC Identity needs atleast has 'Contributor' access to Application Gateway and 'Reader' access to Application Gateway's Resource Group + +output ApplicationGatewayName string = deployAppGw ? appgw.name : '' + +/*_ ___ __ __ .______ _______ .______ .__ __. _______ .___________. _______ _______. +| |/ / | | | | | _ \ | ____|| _ \ | \ | | | ____|| || ____| / | +| ' / | | | | | |_) | | |__ | |_) | | \| | | |__ `---| |----`| |__ | (----` +| < | | | | | _ < | __| | / | . ` | | __| | | | __| \ \ +| . \ | `--' | | |_) | | |____ | |\ \----.| |\ | | |____ | | | |____ .----) | +|__|\__\ \______/ |______/ |_______|| _| `._____||__| \__| |_______| |__| |_______||_______/ */ + +@description('DNS prefix. Defaults to {resourceName}-dns') +param dnsPrefix string = '${resourceName}-dns' + +@description('Kubernetes Version') +param kubernetesVersion string = '1.23.12' + +@description('Enable Azure AD integration on AKS') +param enable_aad bool = false + +@description('The ID of the Azure AD tenant') +param aad_tenant_id string = '' + +@description('Create, and use a new Log Analytics workspace for AKS logs') +param omsagent bool = false + +@description('Enables the ContainerLogsV2 table to be of type Basic') +param containerLogsV2BasicLogs bool = false + +@description('Enable RBAC using AAD') +param enableAzureRBAC bool = false + +@description('Enables Kubernetes Event-driven Autoscaling (KEDA)') +param kedaAddon bool = false + +@description('Enables Open Service Mesh') +param openServiceMeshAddon bool = false + +@description('Enables the Blob CSI driver') +param blobCSIDriver bool = false + +@description('Enables the File CSI driver') +param fileCSIDriver bool = true + +@description('Enables the Disk CSI driver') +param diskCSIDriver bool = true + +@allowed([ + 'none' + 'patch' + 'stable' + 'rapid' + 'node-image' +]) +@description('AKS upgrade channel') +param upgradeChannel string = 'none' + +@allowed([ + 'Ephemeral' + 'Managed' +]) +@description('OS disk type') +param osDiskType string = 'Ephemeral' + +@description('VM SKU') +param agentVMSize string = 'Standard_DS3_v2' + +@description('Disk size in GB') +param osDiskSizeGB int = 0 + +@description('The number of agents for the user node pool') +param agentCount int = 3 + +@description('The maximum number of nodes for the user node pool') +param agentCountMax int = 0 +var autoScale = agentCountMax > agentCount + +@minLength(3) +@maxLength(12) +@description('Name for user node pool') +param nodePoolName string = 'npuser01' + +@description('Allocate pod ips dynamically') +param cniDynamicIpAllocation bool = false + +@minValue(10) +@maxValue(250) +@description('The maximum number of pods per node.') +param maxPods int = 30 + +@allowed([ + 'azure' + 'kubenet' +]) +@description('The network plugin type') +param networkPlugin string = 'azure' + +@allowed([ + '' + 'Overlay' +]) +@description('The network plugin type') +param networkPluginMode string = '' + +@allowed([ + '' + 'cilium' +]) +@description('Use Cilium dataplane (requires azure networkPlugin)') +param ebpfDataplane string = '' + +@allowed([ + '' + 'azure' + 'calico' +]) +@description('The network policy to use.') +param networkPolicy string = '' + +@allowed([ + '' + 'audit' + 'deny' +]) +@description('Enable the Azure Policy addon') +param azurepolicy string = '' + +@allowed([ + 'Baseline' + 'Restricted' +]) +param azurePolicyInitiative string = 'Baseline' + +@description('The IP addresses that are allowed to access the API server') +param authorizedIPRanges array = [] + +@description('Enable private cluster') +param enablePrivateCluster bool = false + +@allowed([ + 'system' + 'none' + 'privateDnsZone' +]) +@description('Private cluster dns advertisment method, leverages the dnsApiPrivateZoneId parameter') +param privateClusterDnsMethod string = 'system' + +@description('The full Azure resource ID of the privatelink DNS zone to use for the AKS cluster API Server') +param dnsApiPrivateZoneId string = '' + +@description('The zones to use for a node pool') +param availabilityZones array = [] + +@description('Disable local K8S accounts for AAD enabled clusters') +param AksDisableLocalAccounts bool = false + +@description('Use the paid sku for SLA rather than SLO') +param AksPaidSkuForSLA bool = false + +@minLength(9) +@maxLength(18) +@description('The address range to use for pods') +param podCidr string = '10.240.100.0/22' + +@minLength(9) +@maxLength(18) +@description('The address range to use for services') +param serviceCidr string = '172.10.0.0/16' + +@minLength(7) +@maxLength(15) +@description('The IP address to reserve for DNS') +param dnsServiceIP string = '172.10.0.10' + +@minLength(9) +@maxLength(18) +@description('The address range to use for the docker bridge') +param dockerBridgeCidr string = '172.17.0.1/16' + +@description('Enable Microsoft Defender for Containers (preview)') +param defenderForContainers bool = false + +@description('Only use the system node pool') +param JustUseSystemPool bool = false + +@allowed([ + 'CostOptimised' + 'Standard' + 'HighSpec' + 'Custom' +]) +@description('The System Pool Preset sizing') +param SystemPoolType string = 'CostOptimised' + +@description('A custom system pool spec') +param SystemPoolCustomPreset object = {} + +param AutoscaleProfile object = { + 'balance-similar-node-groups': 'true' + expander: 'random' + 'max-empty-bulk-delete': '10' + 'max-graceful-termination-sec': '600' + 'max-node-provision-time': '15m' + 'max-total-unready-percentage': '45' + 'new-pod-scale-up-delay': '0s' + 'ok-total-unready-count': '3' + 'scale-down-delay-after-add': '10m' + 'scale-down-delay-after-delete': '20s' + 'scale-down-delay-after-failure': '3m' + 'scale-down-unneeded-time': '10m' + 'scale-down-unready-time': '20m' + 'scale-down-utilization-threshold': '0.5' + 'scan-interval': '10s' + 'skip-nodes-with-local-storage': 'true' + 'skip-nodes-with-system-pods': 'true' +} + +@allowed([ + 'loadBalancer' + 'managedNATGateway' + 'userAssignedNATGateway' +]) +@description('Outbound traffic type for the egress traffic of your cluster') +param aksOutboundTrafficType string = 'loadBalancer' + +@description('Create a new Nat Gateway, applies to custom networking only') +param createNatGateway bool = false + +@minValue(1) +@maxValue(16) +@description('The effective outbound IP resources of the cluster NAT gateway') +param natGwIpCount int = 2 + +@minValue(4) +@maxValue(120) +@description('Outbound flow idle timeout in minutes for NatGw') +param natGwIdleTimeout int = 30 + +@description('Configures the cluster as an OIDC issuer for use with Workload Identity') +param oidcIssuer bool = false + +@description('Installs Azure Workload Identity into the cluster') +param workloadIdentity bool = false + +@description('Assign a public IP per node for user node pools') +param enableNodePublicIP bool = false + +param warIngressNginx bool = false + +@description('System Pool presets are derived from the recommended system pool specs') +var systemPoolPresets = { + CostOptimised : { + vmSize: 'Standard_B4ms' + count: 1 + minCount: 1 + maxCount: 3 + enableAutoScaling: true + availabilityZones: [] + } + Standard : { + vmSize: 'Standard_DS2_v2' + count: 3 + minCount: 3 + maxCount: 5 + enableAutoScaling: true + availabilityZones: [ + '1' + '2' + '3' + ] + } + HighSpec : { + vmSize: 'Standard_D4s_v3' + count: 3 + minCount: 3 + maxCount: 5 + enableAutoScaling: true + availabilityZones: [ + '1' + '2' + '3' + ] + } +} + +var systemPoolBase = { + name: JustUseSystemPool ? nodePoolName : 'npsystem' + vmSize: agentVMSize + count: agentCount + mode: 'System' + osType: 'Linux' + maxPods: 30 + type: 'VirtualMachineScaleSets' + vnetSubnetID: !empty(aksSubnetId) ? aksSubnetId : json('null') + upgradeSettings: { + maxSurge: '33%' + } + nodeTaints: [ + JustUseSystemPool ? '' : 'CriticalAddonsOnly=true:NoSchedule' + ] +} + +var agentPoolProfiles = JustUseSystemPool ? array(systemPoolBase) : concat(array(union(systemPoolBase, SystemPoolType=='Custom' && SystemPoolCustomPreset != {} ? SystemPoolCustomPreset : systemPoolPresets[SystemPoolType]))) + + +output userNodePoolName string = nodePoolName +output systemNodePoolName string = JustUseSystemPool ? nodePoolName : 'npsystem' + +var akssku = AksPaidSkuForSLA ? 'Paid' : 'Free' + +var aks_addons = union({ + azurepolicy: { + config: { + version: !empty(azurepolicy) ? 'v2' : json('null') + } + enabled: !empty(azurepolicy) + } + azureKeyvaultSecretsProvider: { + config: { + enableSecretRotation: 'true' + rotationPollInterval: keyVaultAksCSIPollInterval + } + enabled: keyVaultAksCSI + } + openServiceMesh: { + enabled: openServiceMeshAddon + config: {} + } +}, createLaw && omsagent ? { + omsagent: { + enabled: createLaw && omsagent + config: { + logAnalyticsWorkspaceResourceID: createLaw && omsagent ? aks_law.id : json('null') + } + }} : {}) + +var aks_addons1 = ingressApplicationGateway ? union(aks_addons, deployAppGw ? { + ingressApplicationGateway: { + config: { + applicationGatewayId: appgw.id + } + enabled: true + } +} : { + // AKS RP will deploy the AppGateway for us (not creating custom or BYO VNET) + ingressApplicationGateway: { + enabled: true + config: { + applicationGatewayName: appgwName + subnetCIDR: '10.225.0.0/16' + } + } +}) : aks_addons + +var aks_identity = { + type: 'UserAssigned' + userAssignedIdentities: { + '${aksUai.id}': {} + } +} + +@description('Sets the private dns zone id if provided') +var aksPrivateDnsZone = privateClusterDnsMethod=='privateDnsZone' ? (!empty(dnsApiPrivateZoneId) ? dnsApiPrivateZoneId : 'system') : privateClusterDnsMethod +output aksPrivateDnsZone string = aksPrivateDnsZone + +@description('Needing to seperately declare and union this because of https://github.com/Azure/AKS-Construction/issues/344') +var managedNATGatewayProfile = { + natGatewayProfile : { + managedOutboundIPProfile: { + count: natGwIpCount + } + idleTimeoutInMinutes: natGwIdleTimeout + } +} + +@description('Needing to seperately declare and union this because of https://github.com/Azure/AKS/issues/2774') +var azureDefenderSecurityProfile = { + securityProfile : { + defender: { + logAnalyticsWorkspaceResourceId: createLaw ? aks_law.id : null + securityMonitoring: { + enabled: defenderForContainers + } + } + } +} + +var aksProperties = union({ + kubernetesVersion: kubernetesVersion + enableRBAC: true + dnsPrefix: dnsPrefix + aadProfile: enable_aad ? { + managed: true + enableAzureRBAC: enableAzureRBAC + tenantID: aad_tenant_id + } : null + apiServerAccessProfile: !empty(authorizedIPRanges) ? { + authorizedIPRanges: authorizedIPRanges + } : { + enablePrivateCluster: enablePrivateCluster + privateDNSZone: enablePrivateCluster ? aksPrivateDnsZone : '' + enablePrivateClusterPublicFQDN: enablePrivateCluster && privateClusterDnsMethod=='none' + } + agentPoolProfiles: agentPoolProfiles + workloadAutoScalerProfile: { + keda: { + enabled: kedaAddon + } + } + networkProfile: { + loadBalancerSku: 'standard' + networkPlugin: networkPlugin + #disable-next-line BCP036 //Disabling validation of this parameter to cope with empty string to indicate no Network Policy required. + networkPolicy: networkPolicy + networkPluginMode: networkPlugin=='azure' ? networkPluginMode : '' + podCidr: networkPlugin=='kubenet' || cniDynamicIpAllocation ? podCidr : json('null') + serviceCidr: serviceCidr + dnsServiceIP: dnsServiceIP + dockerBridgeCidr: dockerBridgeCidr + outboundType: aksOutboundTrafficType + ebpfDataplane: networkPlugin=='azure' ? ebpfDataplane : '' + } + disableLocalAccounts: AksDisableLocalAccounts && enable_aad + autoUpgradeProfile: {upgradeChannel: upgradeChannel} + addonProfiles: !empty(aks_addons1) ? aks_addons1 : aks_addons + autoScalerProfile: autoScale ? AutoscaleProfile : {} + oidcIssuerProfile: { + enabled: oidcIssuer + } + securityProfile: { + workloadIdentity: { + enabled: workloadIdentity + } + } + ingressProfile: { + webAppRouting: { + enabled: warIngressNginx + } + } + storageProfile: { + blobCSIDriver: { + enabled: blobCSIDriver + } + diskCSIDriver: { + enabled: diskCSIDriver + } + fileCSIDriver: { + enabled: fileCSIDriver + } + } +}, +aksOutboundTrafficType == 'managedNATGateway' ? managedNATGatewayProfile : {}, +defenderForContainers && createLaw ? azureDefenderSecurityProfile : {}, +keyVaultKmsCreateAndPrereqs || !empty(keyVaultKmsByoKeyId) ? azureKeyVaultKms : {} +) + +resource aks 'Microsoft.ContainerService/managedClusters@2022-10-02-preview' = { + name: 'aks-${resourceName}' + location: location + properties: aksProperties + identity: createAksUai ? aks_identity : { + type: 'SystemAssigned' + } + sku: { + name: 'Basic' + tier: akssku + } + dependsOn: [ + privateDnsZoneRbac + waitForKmsRbac + ] +} +output aksClusterName string = aks.name +output aksOidcIssuerUrl string = oidcIssuer ? aks.properties.oidcIssuerProfile.issuerURL : '' + +@allowed(['Linux','Windows']) +@description('The User Node pool OS') +param osType string = 'Linux' + +@allowed(['Ubuntu','Windows2019','Windows2022']) +@description('The User Node pool OS SKU') +param osSKU string = 'Ubuntu' + +var poolName = osType == 'Linux' ? nodePoolName : take(nodePoolName, 6) + +module userNodePool 'aksagentpool.bicep' = if (!JustUseSystemPool){ + name: 'userNodePool' + params: { + AksName: aks.name + PoolName: poolName + subnetId: aksSubnetId + agentCount: agentCount + agentCountMax: agentCountMax + agentVMSize: agentVMSize + maxPods: maxPods + osDiskType: osDiskType + osType: osType + osSKU: osSKU + enableNodePublicIP: enableNodePublicIP + osDiskSizeGB: osDiskSizeGB + } +} + +@description('This output can be directly leveraged when creating a ManagedId Federated Identity') +output aksOidcFedIdentityProperties object = { + issuer: oidcIssuer ? aks.properties.oidcIssuerProfile.issuerURL : '' + audiences: ['api://AzureADTokenExchange'] + subject: 'system:serviceaccount:ns:svcaccount' +} + +@description('The name of the managed resource group AKS uses') +output aksNodeResourceGroup string = aks.properties.nodeResourceGroup + +@description('The Azure resource id for the AKS cluster') +output aksResourceId string = aks.id + +//output aksNodePools array = [for nodepool in agentPoolProfiles: name] + +@description('Not giving Rbac at the vnet level when using private dns results in ReconcilePrivateDNS. Therefore we need to upgrade the scope when private dns is being used, because it wants to set up the dns->vnet integration.') +var uaiNetworkScopeRbac = enablePrivateCluster && !empty(dnsApiPrivateZoneId) ? 'Vnet' : 'Subnet' +module privateDnsZoneRbac './dnsZoneRbac.bicep' = if (enablePrivateCluster && !empty(dnsApiPrivateZoneId) && createAksUai) { + name: 'addPrivateK8sApiDnsContributor' + params: { + vnetId: '' + dnsZoneId: dnsApiPrivateZoneId + principalId: createAksUai ? aksUai.properties.principalId : '' + } +} + +var policySetBaseline = '/providers/Microsoft.Authorization/policySetDefinitions/a8640138-9b0a-4a28-b8cb-1666c838647d' +var policySetRestrictive = '/providers/Microsoft.Authorization/policySetDefinitions/42b8ef37-b724-4e24-bbc8-7a7708edfe00' + +resource aks_policies 'Microsoft.Authorization/policyAssignments@2020-09-01' = if (!empty(azurepolicy)) { + name: '${resourceName}-${azurePolicyInitiative}' + location: location + properties: { + policyDefinitionId: azurePolicyInitiative == 'Baseline' ? policySetBaseline : policySetRestrictive + parameters: { + excludedNamespaces: { + value: [ + 'kube-system' + 'gatekeeper-system' + 'azure-arc' + 'cluster-baseline-setting' + ] + } + effect: { + value: azurepolicy + } + } + metadata: { + assignedBy: 'Aks Construction' + } + displayName: 'Kubernetes cluster pod security ${azurePolicyInitiative} standards for Linux-based workloads' + description: 'As per: https://github.com/Azure/azure-policy/blob/master/built-in-policies/policySetDefinitions/Kubernetes/' + } +} + +@description('If automated deployment, for the 3 automated user assignments, set Principal Type on each to "ServicePrincipal" rarter than "User"') +param automatedDeployment bool = false + +@description('The principal ID to assign the AKS admin role.') +param adminPrincipalId string = '' +// for AAD Integrated Cluster wusing 'enableAzureRBAC', add Cluster admin to the current user! +var buildInAKSRBACClusterAdmin = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b1ff04bb-8a4e-4dc4-8eb5-8693973ce19b') +resource aks_admin_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableAzureRBAC && !empty(adminPrincipalId)) { + scope: aks // Use when specifying a scope that is different than the deployment scope + name: guid(aks.id, 'aksadmin', buildInAKSRBACClusterAdmin) + properties: { + roleDefinitionId: buildInAKSRBACClusterAdmin + principalType: automatedDeployment ? 'ServicePrincipal' : 'User' + principalId: adminPrincipalId + } +} + +param fluxGitOpsAddon bool = false + +resource fluxAddon 'Microsoft.KubernetesConfiguration/extensions@2022-04-02-preview' = if(fluxGitOpsAddon) { + name: 'flux' + scope: aks + properties: { + extensionType: 'microsoft.flux' + autoUpgradeMinorVersion: true + releaseTrain: 'Stable' + scope: { + cluster: { + releaseNamespace: 'flux-system' + } + } + configurationProtectedSettings: {} + } + dependsOn: [daprExtension] //Chaining dependencies because of: https://github.com/Azure/AKS-Construction/issues/385 +} +output fluxReleaseNamespace string = fluxGitOpsAddon ? fluxAddon.properties.scope.cluster.releaseNamespace : '' + +@description('Add the Dapr extension') +param daprAddon bool = false +@description('Enable high availability (HA) mode for the Dapr control plane') +param daprAddonHA bool = false + +resource daprExtension 'Microsoft.KubernetesConfiguration/extensions@2022-04-02-preview' = if(daprAddon) { + name: 'dapr' + scope: aks + properties: { + extensionType: 'Microsoft.Dapr' + autoUpgradeMinorVersion: true + releaseTrain: 'Stable' + configurationSettings: { + 'global.ha.enabled': '${daprAddonHA}' + } + scope: { + cluster: { + releaseNamespace: 'dapr-system' + } + } + configurationProtectedSettings: {} + } +} + +output daprReleaseNamespace string = daprAddon ? daprExtension.properties.scope.cluster.releaseNamespace : '' + +/*__ ___. ______ .__ __. __ .___________. ______ .______ __ .__ __. _______ +| \/ | / __ \ | \ | | | | | | / __ \ | _ \ | | | \ | | / _____| +| \ / | | | | | | \| | | | `---| |----`| | | | | |_) | | | | \| | | | __ +| |\/| | | | | | | . ` | | | | | | | | | | / | | | . ` | | | |_ | +| | | | | `--' | | |\ | | | | | | `--' | | |\ \----.| | | |\ | | |__| | +|__| |__| \______/ |__| \__| |__| |__| \______/ | _| `._____||__| |__| \__| \______| */ + + +@description('Diagnostic categories to log') +param AksDiagCategories array = [ + 'cluster-autoscaler' + 'kube-controller-manager' + 'kube-audit-admin' + 'guard' +] + +resource AksDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (createLaw && omsagent) { + name: 'aksDiags' + scope: aks + properties: { + workspaceId: aks_law.id + logs: [for aksDiagCategory in AksDiagCategories: { + category: aksDiagCategory + enabled: true + }] + metrics: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } +} + +@description('Enable Metric Alerts') +param createAksMetricAlerts bool = true + +@allowed([ + 'Short' + 'Long' +]) +@description('Which Metric polling frequency model to use') +param AksMetricAlertMetricFrequencyModel string = 'Long' + +var AlertFrequencyLookup = { + Short: { + evalFrequency: 'PT1M' + windowSize: 'PT5M' + } + Long: { + evalFrequency: 'PT15M' + windowSize: 'PT1H' + } +} +var AlertFrequency = AlertFrequencyLookup[AksMetricAlertMetricFrequencyModel] + +module aksmetricalerts './aksmetricalerts.bicep' = if (createLaw) { + name: 'aksmetricalerts' + scope: resourceGroup() + params: { + clusterName: aks.name + logAnalyticsWorkspaceName: aks_law.name + metricAlertsEnabled: createAksMetricAlerts + evalFrequency: AlertFrequency.evalFrequency + windowSize: AlertFrequency.windowSize + alertSeverity: 'Informational' + logAnalyticsWorkspaceLocation: location + } +} + +//---------------------------------------------------------------------------------- Container Insights + +@description('The Log Analytics retention period') +param retentionInDays int = 30 + +@description('The Log Analytics daily data cap (GB) (0=no limit)') +param logDataCap int = 0 + +var aks_law_name = 'log-${resourceName}' + +var createLaw = (omsagent || deployAppGw || azureFirewalls || CreateNetworkSecurityGroups || defenderForContainers) + +resource aks_law 'Microsoft.OperationalInsights/workspaces@2022-10-01' = if (createLaw) { + name: aks_law_name + location: location + properties : union({ + retentionInDays: retentionInDays + }, + logDataCap>0 ? { workspaceCapping: { + dailyQuotaGb: logDataCap + }} : {} + ) +} + + +resource containerLogsV2_Basiclogs 'Microsoft.OperationalInsights/workspaces/tables@2022-10-01' = if(containerLogsV2BasicLogs){ + name: '${aks_law_name}/ContainerLogV2' + properties: { + plan: 'Basic' + } + dependsOn: [ + aks + ] +} + +//This role assignment enables AKS->LA Fast Alerting experience +var MonitoringMetricsPublisherRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3913510d-42f4-4e42-8a64-420c390055eb') +resource FastAlertingRole_Aks_Law 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (omsagent) { + scope: aks + name: guid(aks.id, 'omsagent', MonitoringMetricsPublisherRole) + properties: { + roleDefinitionId: MonitoringMetricsPublisherRole + principalId: aks.properties.addonProfiles.omsagent.identity.objectId + principalType: 'ServicePrincipal' + } +} + +output LogAnalyticsName string = (createLaw) ? aks_law.name : '' +output LogAnalyticsGuid string = (createLaw) ? aks_law.properties.customerId : '' +output LogAnalyticsId string = (createLaw) ? aks_law.id : '' + +//---------------------------------------------------------------------------------- AKS events with Event Grid +// Supported events : https://docs.microsoft.com/en-gb/azure/event-grid/event-schema-aks?tabs=event-grid-event-schema#available-event-types + +@description('Create an Event Grid System Topic for AKS events') +param createEventGrid bool = false + +resource eventGrid 'Microsoft.EventGrid/systemTopics@2021-12-01' = if(createEventGrid) { + name: 'evgt-${aks.name}' + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + source: aks.id + topicType: 'Microsoft.ContainerService.ManagedClusters' + } +} + +output eventGridName string = createEventGrid ? eventGrid.name : '' + +resource eventGridDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (createLaw && createEventGrid) { + name: 'eventGridDiags' + scope: eventGrid + properties: { + workspaceId:aks_law.id + logs: [ + { + category: 'DeliveryFailures' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } +} + +@description('Enable usage and telemetry feedback to Microsoft.') +param enableTelemetry bool = true + +var telemetryId = '3c1e2fc6-1c4b-44f9-8694-25d00ae30a3a-${location}' + +/*.___________. _______ __ _______ .___ ___. _______ .___________..______ ____ ____ _______ _______ .______ __ ______ ____ ____ .___ ___. _______ .__ __. .___________. +| || ____|| | | ____|| \/ | | ____|| || _ \ \ \ / / | \ | ____|| _ \ | | / __ \ \ \ / / | \/ | | ____|| \ | | | | +`---| |----`| |__ | | | |__ | \ / | | |__ `---| |----`| |_) | \ \/ / | .--. || |__ | |_) | | | | | | | \ \/ / | \ / | | |__ | \| | `---| |----` + | | | __| | | | __| | |\/| | | __| | | | / \_ _/ | | | || __| | ___/ | | | | | | \_ _/ | |\/| | | __| | . ` | | | + | | | |____ | `----.| |____ | | | | | |____ | | | |\ \----. | | | '--' || |____ | | | `----.| `--' | | | | | | | | |____ | |\ | | | + |__| |_______||_______||_______||__| |__| |_______| |__| | _| `._____| |__| |_______/ |_______|| _| |_______| \______/ |__| |__| |__| |_______||__| \__| |__| */ + +// Telemetry Deployment +resource telemetrydeployment 'Microsoft.Resources/deployments@2021-04-01' = if (enableTelemetry) { + name: telemetryId + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' + contentVersion: '1.0.0.0' + resources: {} + } + } +} + +//ACSCII Art link : https://textkool.com/en/ascii-art-generator?hl=default&vl=default&font=Star%20Wars&text=changeme diff --git a/templates/common/infra/bicep/core/host/aks/network.bicep b/templates/common/infra/bicep/core/host/aks/network.bicep new file mode 100644 index 00000000000..0ec70091a08 --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/network.bicep @@ -0,0 +1,506 @@ +param resourceName string +param location string = resourceGroup().location + +param networkPluginIsKubenet bool = false +param aksPrincipleId string = '' + +param vnetAddressPrefix string +param vnetAksSubnetAddressPrefix string + +//Nsg +param workspaceName string = '' +param workspaceResourceGroupName string = '' +param networkSecurityGroups bool = true + +//Firewall +param azureFirewalls bool = false +param azureFirewallsSku string = 'Basic' +param azureFirewallsManagementSeperation bool = azureFirewalls && azureFirewallsSku=='Basic' +param vnetFirewallSubnetAddressPrefix string = '' +param vnetFirewallManagementSubnetAddressPrefix string = '' + +//Ingress +param ingressApplicationGateway bool = false +param ingressApplicationGatewayPublic bool = false +param vnetAppGatewaySubnetAddressPrefix string ='' + +//Private Link +param privateLinks bool = false +param privateLinkSubnetAddressPrefix string = '' +param privateLinkAcrId string = '' +param privateLinkAkvId string = '' + +//ACR +param acrPrivatePool bool = false +param acrAgentPoolSubnetAddressPrefix string = '' + +//NatGatewayEgress +param natGateway bool = false +param natGatewayPublicIps int = 2 +param natGatewayIdleTimeoutMins int = 30 + +//Bastion +param bastion bool =false +param bastionSubnetAddressPrefix string = '' + +@description('Used by the Bastion Public IP') +param availabilityZones array = [] + + +var bastion_subnet_name = 'AzureBastionSubnet' +var bastion_baseSubnet = { + name: bastion_subnet_name + properties: { + addressPrefix: bastionSubnetAddressPrefix + } +} +var bastion_subnet = bastion && networkSecurityGroups ? union(bastion_baseSubnet, nsgBastion.outputs.nsgSubnetObj) : bastion_baseSubnet + +var acrpool_subnet_name = 'acrpool-sn' +var acrpool_baseSubnet = { + name: acrpool_subnet_name + properties: { + addressPrefix: acrAgentPoolSubnetAddressPrefix + } +} +var acrpool_subnet = privateLinks && networkSecurityGroups ? union(acrpool_baseSubnet, nsgAcrPool.outputs.nsgSubnetObj) : acrpool_baseSubnet + +var private_link_subnet_name = 'privatelinks-sn' +var private_link_baseSubnet = { + name: private_link_subnet_name + properties: { + addressPrefix: privateLinkSubnetAddressPrefix + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + } +} +var private_link_subnet = privateLinks && networkSecurityGroups ? union(private_link_baseSubnet, nsgPrivateLinks.outputs.nsgSubnetObj) : private_link_baseSubnet + + +var appgw_subnet_name = 'appgw-sn' +var appgw_baseSubnet = { + name: appgw_subnet_name + properties: { + addressPrefix: vnetAppGatewaySubnetAddressPrefix + } +} +var appgw_subnet = ingressApplicationGateway && networkSecurityGroups ? union(appgw_baseSubnet, nsgAppGw.outputs.nsgSubnetObj) : appgw_baseSubnet + +var fw_subnet_name = 'AzureFirewallSubnet' // Required by FW +var fw_subnet = { + name: fw_subnet_name + properties: { + addressPrefix: vnetFirewallSubnetAddressPrefix + } +} + +/// ---- Firewall VNET config +module calcAzFwIp './calcAzFwIp.bicep' = if (azureFirewalls) { + name: 'calcAzFwIp' + params: { + vnetFirewallSubnetAddressPrefix: vnetFirewallSubnetAddressPrefix + } +} + +var fwmgmt_subnet_name = 'AzureFirewallManagementSubnet' // Required by FW +var fwmgmt_subnet = { + name: fwmgmt_subnet_name + properties: { + addressPrefix: vnetFirewallManagementSubnetAddressPrefix + } +} + +var routeFwTableName = 'rt-afw-${resourceName}' +resource vnet_udr 'Microsoft.Network/routeTables@2022-07-01' = if (azureFirewalls) { + name: routeFwTableName + location: location + properties: { + routes: [ + { + name: 'AKSNodesEgress' + properties: { + addressPrefix: '0.0.0.0/1' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: azureFirewalls ? calcAzFwIp.outputs.FirewallPrivateIp : null + } + } + ] + } +} + +var contributorRoleId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + +@description('Required for kubenet networking.') +resource vnet_udr_rbac 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (azureFirewalls && !empty(aksPrincipleId) && networkPluginIsKubenet) { + scope: vnet_udr + name: guid(vnet_udr.id, aksPrincipleId, contributorRoleId) + properties: { + principalId: aksPrincipleId + roleDefinitionId: contributorRoleId + principalType: 'ServicePrincipal' + } +} + +var aks_subnet_name = 'aks-sn' +var aks_baseSubnet = { + name: aks_subnet_name + properties: union({ + addressPrefix: vnetAksSubnetAddressPrefix + }, privateLinks ? { + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + } : {}, natGateway ? { + natGateway: { + id: natGw.id + } + } : {}, azureFirewalls ? { + routeTable: { + id: vnet_udr.id //resourceId('Microsoft.Network/routeTables', routeFwTableName) + } + }: {}) +} + +var aks_subnet = networkSecurityGroups ? union(aks_baseSubnet, nsgAks.outputs.nsgSubnetObj) : aks_baseSubnet + +var subnets = union( + array(aks_subnet), + azureFirewalls ? array(fw_subnet) : [], + privateLinks ? array(private_link_subnet) : [], + acrPrivatePool ? array(acrpool_subnet) : [], + bastion ? array(bastion_subnet) : [], + ingressApplicationGateway ? array(appgw_subnet) : [], + azureFirewallsManagementSeperation ? array(fwmgmt_subnet) : [] +) +output debugSubnets array = subnets + +var vnetName = 'vnet-${resourceName}' +resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = { + name: vnetName + location: location + properties: { + addressSpace: { + addressPrefixes: [ + vnetAddressPrefix + ] + } + subnets: subnets + } +} +output vnetId string = vnet.id +output vnetName string = vnet.name +output aksSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, aks_subnet_name) +output fwSubnetId string = azureFirewalls ? '${vnet.id}/subnets/${fw_subnet_name}' : '' +output fwMgmtSubnetId string = azureFirewalls ? '${vnet.id}/subnets/${fwmgmt_subnet_name}' : '' +output acrPoolSubnetId string = acrPrivatePool ? '${vnet.id}/subnets/${acrpool_subnet_name}' : '' +output appGwSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, appgw_subnet_name) +output privateLinkSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, private_link_subnet_name) + +module aks_vnet_con 'networksubnetrbac.bicep' = if (!empty(aksPrincipleId)) { + name: '${resourceName}-subnetRbac' + params: { + servicePrincipalId: aksPrincipleId + subnetName: aks_subnet_name + vnetName: vnet.name + } +} + +/* -------------------------------------------------------------------------- Private Link for ACR */ +var privateLinkAcrName = 'pl-acr-${resourceName}' +resource privateLinkAcr 'Microsoft.Network/privateEndpoints@2021-08-01' = if (!empty(privateLinkAcrId)) { + name: privateLinkAcrName + location: location + properties: { + customNetworkInterfaceName: 'nic-${privateLinkAcrName}' + privateLinkServiceConnections: [ + { + name: 'Acr-Connection' + properties: { + privateLinkServiceId: privateLinkAcrId + groupIds: [ + 'registry' + ] + } + } + ] + subnet: { + id: '${vnet.id}/subnets/${private_link_subnet_name}' + } + } +} + +resource privateDnsAcr 'Microsoft.Network/privateDnsZones@2020-06-01' = if (!empty(privateLinkAcrId)) { + name: 'privatelink.azurecr.io' + location: 'global' +} + +var privateDnsAcrLinkName = 'vnet-dnscr-${resourceName}' +resource privateDnsAcrLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (!empty(privateLinkAcrId)) { + parent: privateDnsAcr + name: privateDnsAcrLinkName + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: vnet.id + } + } +} + +resource privateDnsAcrZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2021-08-01' = if (!empty(privateLinkAcrId)) { + parent: privateLinkAcr + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: 'vnet-pl-acr' + properties: { + privateDnsZoneId: privateDnsAcr.id + } + } + ] + } +} + + +/* -------------------------------------------------------------------------- Private Link for KeyVault */ +var privateLinkAkvName = 'pl-akv-${resourceName}' +resource privateLinkAkv 'Microsoft.Network/privateEndpoints@2021-08-01' = if (!empty(privateLinkAkvId)) { + name: privateLinkAkvName + location: location + properties: { + customNetworkInterfaceName: 'nic-${privateLinkAkvName}' + privateLinkServiceConnections: [ + { + name: 'Akv-Connection' + properties: { + privateLinkServiceId: privateLinkAkvId + groupIds: [ + 'vault' + ] + } + } + ] + subnet: { + id: '${vnet.id}/subnets/${private_link_subnet_name}' + } + } +} + +resource privateDnsAkv 'Microsoft.Network/privateDnsZones@2020-06-01' = if (!empty(privateLinkAkvId)) { + name: 'privatelink.vaultcore.azure.net' + location: 'global' +} + +var privateDnsAkvLinkName = 'vnet-dnscr-${resourceName}' +resource privateDnsAkvLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (!empty(privateLinkAkvId)) { + parent: privateDnsAkv + name: privateDnsAkvLinkName + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: vnet.id + } + } +} + +resource privateDnsAkvZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2021-08-01' = if (!empty(privateLinkAkvId)) { + parent: privateLinkAkv + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: 'vnet-pl-akv' + properties: { + privateDnsZoneId: privateDnsAkv.id + } + } + ] + } +} + +param bastionHostName string = 'bas-${resourceName}' +var publicIpAddressName = 'pip-${bastionHostName}' + +@allowed([ + 'Standard' + 'Basic' +]) +param bastionSku string = 'Standard' + +resource bastionPip 'Microsoft.Network/publicIPAddresses@2021-03-01' = if(bastion) { + name: publicIpAddressName + location: location + sku: { + name: 'Standard' + } + zones: !empty(availabilityZones) ? availabilityZones : [] + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource bastionHost 'Microsoft.Network/bastionHosts@2021-05-01' = if(bastion) { + name: bastionHostName + location: location + sku: { + name: bastionSku + } + properties: { + enableTunneling: true + ipConfigurations: [ + { + name: 'IpConf' + properties: { + subnet: { + id: '${vnet.id}/subnets/${bastion_subnet_name}' + } + publicIPAddress: { + id: bastionPip.id + } + } + } + ] + } +} + +resource log 'Microsoft.OperationalInsights/workspaces@2021-06-01' existing = if(networkSecurityGroups && !empty(workspaceName)) { + name: workspaceName + scope: resourceGroup(workspaceResourceGroupName) +} + +param CreateNsgFlowLogs bool = false + +var flowLogStorageRawName = replace(toLower('stflow${resourceName}${uniqueString(resourceGroup().id, resourceName)}'),'-','') +var flowLogStorageName = length(flowLogStorageRawName) > 24 ? substring(flowLogStorageRawName, 0, 24) : flowLogStorageRawName +resource flowLogStor 'Microsoft.Storage/storageAccounts@2021-08-01' = if(CreateNsgFlowLogs && networkSecurityGroups) { + name: flowLogStorageName + kind: 'StorageV2' + sku: { + name: 'Standard_LRS' + } + location: location + properties: { + minimumTlsVersion: 'TLS1_2' + } +} + +//NSG's +module nsgAks 'nsg.bicep' = if(networkSecurityGroups) { + name: 'nsgAks' + params: { + location: location + resourceName: '${aks_subnet_name}-${resourceName}' + workspaceId: !empty(workspaceName) ? log.properties.customerId : '' + workspaceRegion: !empty(workspaceName) ? log.location : '' + workspaceResourceId: !empty(workspaceName) ? log.id : '' + ruleInAllowInternetHttp: true + ruleInAllowInternetHttps: true + ruleInDenySsh: true + FlowLogStorageAccountId: CreateNsgFlowLogs ? flowLogStor.id : '' + } +} + +module nsgAcrPool 'nsg.bicep' = if(acrPrivatePool && networkSecurityGroups) { + name: 'nsgAcrPool' + params: { + location: location + resourceName: '${acrpool_subnet_name}-${resourceName}' + workspaceId: !empty(workspaceName) ? log.properties.customerId : '' + workspaceRegion: !empty(workspaceName) ? log.location : '' + workspaceResourceId: !empty(workspaceName) ? log.id : '' + FlowLogStorageAccountId: CreateNsgFlowLogs ? flowLogStor.id : '' + } + dependsOn: [ + nsgAks + ] +} + +module nsgAppGw 'nsg.bicep' = if(ingressApplicationGateway && networkSecurityGroups) { + name: 'nsgAppGw' + params: { + location: location + resourceName: '${appgw_subnet_name}-${resourceName}' + workspaceId: !empty(workspaceName) ? log.properties.customerId : '' + workspaceRegion: !empty(workspaceName) ? log.location : '' + workspaceResourceId: !empty(workspaceName) ? log.id : '' + ruleInAllowInternetHttp: ingressApplicationGatewayPublic + ruleInAllowInternetHttps: ingressApplicationGatewayPublic + ruleInAllowGwManagement: true + ruleInAllowAzureLoadBalancer: true + ruleInDenyInternet: true + ruleInGwManagementPort: '65200-65535' + FlowLogStorageAccountId: CreateNsgFlowLogs ? flowLogStor.id : '' + } + dependsOn: [ + nsgAcrPool + ] +} + +module nsgBastion 'nsg.bicep' = if(bastion && networkSecurityGroups) { + name: 'nsgBastion' + params: { + location: location + resourceName: '${bastion_subnet_name}-${resourceName}' + workspaceId: !empty(workspaceName) ? log.properties.customerId : '' + workspaceRegion: !empty(workspaceName) ? log.location : '' + workspaceResourceId: !empty(workspaceName) ? log.id : '' + ruleInAllowBastionHostComms: true + ruleInAllowInternetHttps: true + ruleInAllowGwManagement: true + ruleInAllowAzureLoadBalancer: true + ruleOutAllowBastionComms: true + ruleInGwManagementPort: '443' + FlowLogStorageAccountId: CreateNsgFlowLogs ? flowLogStor.id : '' + } + dependsOn: [ + nsgAppGw + ] +} + +module nsgPrivateLinks 'nsg.bicep' = if(privateLinks && networkSecurityGroups) { + name: 'nsgPrivateLinks' + params: { + location: location + resourceName: '${private_link_subnet_name}-${resourceName}' + workspaceId: !empty(workspaceName) ? log.properties.customerId : '' + workspaceRegion: !empty(workspaceName) ? log.location : '' + workspaceResourceId: !empty(workspaceName) ? log.id : '' + FlowLogStorageAccountId: CreateNsgFlowLogs ? flowLogStor.id : '' + } + dependsOn: [ + nsgBastion + ] +} + +resource natGwIp 'Microsoft.Network/publicIPAddresses@2021-08-01' = [for i in range(0, natGatewayPublicIps): if(natGateway) { + name: 'pip-${natGwName}-${i+1}' + location: location + sku: { + name: 'Standard' + } + zones: !empty(availabilityZones) ? availabilityZones : [] + properties: { + publicIPAllocationMethod: 'Static' + } +}] + +var natGwName = 'ng-${resourceName}' +resource natGw 'Microsoft.Network/natGateways@2021-08-01' = if(natGateway) { + name: natGwName + location: location + sku: { + name: 'Standard' + } + zones: !empty(availabilityZones) ? availabilityZones : [] + properties: { + publicIpAddresses: [for i in range(0, natGatewayPublicIps): { + id: natGwIp[i].id + }] + idleTimeoutInMinutes: natGatewayIdleTimeoutMins + } + dependsOn: [ + natGwIp + ] +} + diff --git a/templates/common/infra/bicep/core/host/aks/networksubnetrbac.bicep b/templates/common/infra/bicep/core/host/aks/networksubnetrbac.bicep new file mode 100644 index 00000000000..b19cfef527a --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/networksubnetrbac.bicep @@ -0,0 +1,19 @@ +param vnetName string +param subnetName string +param servicePrincipalId string + +var networkContributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7') + +resource subnet 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' existing = { + name: '${vnetName}/${subnetName}' +} + +resource aks_vnet_cont 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(subnet.id, servicePrincipalId, networkContributorRole) + scope: subnet + properties: { + roleDefinitionId: networkContributorRole + principalId: servicePrincipalId + principalType: 'ServicePrincipal' + } +} diff --git a/templates/common/infra/bicep/core/host/aks/networkwatcherflowlog.bicep b/templates/common/infra/bicep/core/host/aks/networkwatcherflowlog.bicep new file mode 100644 index 00000000000..8c77e80d9dd --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/networkwatcherflowlog.bicep @@ -0,0 +1,46 @@ +param name string +param location string = resourceGroup().location +param nsgId string +param storageId string +param trafficAnalytics bool +param trafficAnalyticsInterval int = 60 + +@description('The resource guid of the attached workspace.') +param workspaceId string = '' + +@description('Resource Id of the attached workspace.') +param workspaceResourceId string = '' +param workspaceRegion string = resourceGroup().location + +resource networkWatcher 'Microsoft.Network/networkWatchers@2022-01-01' = { + name: 'NetworkWatcher_${location}' + location: location + properties: {} +} + +resource nsgFlowLogs 'Microsoft.Network/networkWatchers/flowLogs@2021-05-01' = { + name: '${networkWatcher.name}/${name}' + location: location + properties: { + targetResourceId: nsgId + storageId: storageId + enabled: true + retentionPolicy: { + days: 2 + enabled: true + } + format: { + type: 'JSON' + version: 2 + } + flowAnalyticsConfiguration: { + networkWatcherFlowAnalyticsConfiguration: { + enabled: trafficAnalytics + workspaceId: workspaceId + trafficAnalyticsInterval: trafficAnalyticsInterval + workspaceRegion: workspaceRegion + workspaceResourceId: workspaceResourceId + } + } + } +} diff --git a/templates/common/infra/bicep/core/host/aks/nsg.bicep b/templates/common/infra/bicep/core/host/aks/nsg.bicep new file mode 100644 index 00000000000..e90e57f5d2d --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks/nsg.bicep @@ -0,0 +1,283 @@ +param resourceName string +param location string = resourceGroup().location +param workspaceId string = '' +param workspaceResourceId string = '' +param workspaceRegion string = resourceGroup().location + +var nsgName = 'nsg-${resourceName}' + +resource nsg 'Microsoft.Network/networkSecurityGroups@2021-05-01' = { + name: nsgName + location: location +} +output nsgId string = nsg.id + +param ruleInAllowGwManagement bool = false +param ruleInGwManagementPort string = '443,65200-65535' +resource ruleAppGwManagement 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInAllowGwManagement) { + parent: nsg + name: 'Allow_AppGatewayManagement' + properties: { + protocol: '*' + sourcePortRange: '*' + destinationPortRange: ruleInGwManagementPort + sourceAddressPrefix: 'GatewayManager' + destinationAddressPrefix: '*' + access: 'Allow' + priority: 110 + direction: 'Inbound' + } +} + +param ruleInAllowAzureLoadBalancer bool = false +resource ruleAzureLoadBalancer 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if (ruleInAllowAzureLoadBalancer) { + parent: nsg + name: 'Allow_AzureLoadBalancer' + properties: { + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '*' + sourceAddressPrefix: 'AzureLoadBalancer' + destinationAddressPrefix: '*' + access: 'Allow' + priority: 120 + direction: 'Inbound' + sourcePortRanges: [] + destinationPortRanges: [] + sourceAddressPrefixes: [] + destinationAddressPrefixes: [] + } +} + +param ruleInDenyInternet bool = false +resource ruleDenyInternet 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInDenyInternet) { + parent: nsg + name: 'Deny_AllInboundInternet' + properties: { + description: 'Azure infrastructure communication' + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '*' + sourceAddressPrefix: 'Internet' + destinationAddressPrefix: '*' + access: 'Deny' + priority: 4096 + direction: 'Inbound' + sourcePortRanges: [] + destinationPortRanges: [] + sourceAddressPrefixes: [] + destinationAddressPrefixes: [] + } +} + +param ruleInAllowInternetHttp bool = false +resource ruleInternetHttp 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInAllowInternetHttp) { + parent: nsg + name: 'Allow_Internet_Http' + properties: { + protocol: 'Tcp' + sourcePortRange: '*' + sourceAddressPrefix: 'Internet' + destinationAddressPrefix: '*' + access: 'Allow' + priority: 200 + direction: 'Inbound' + sourcePortRanges: [] + destinationPortRanges: [ + '80' + ] + sourceAddressPrefixes: [] + destinationAddressPrefixes: [] + } +} + +param ruleInAllowInternetHttps bool = false +resource ruleInternetHttps 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInAllowInternetHttps) { + parent: nsg + name: 'Allow_Internet_Https' + properties: { + protocol: 'Tcp' + sourcePortRange: '*' + sourceAddressPrefix: 'Internet' + destinationAddressPrefix: '*' + access: 'Allow' + priority: 210 + direction: 'Inbound' + sourcePortRanges: [] + destinationPortRanges: [ + '443' + ] + sourceAddressPrefixes: [] + destinationAddressPrefixes: [] + } +} + +param ruleInAllowBastionHostComms bool = false +resource ruleBastionHost 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInAllowBastionHostComms) { + parent: nsg + name: 'Allow_Bastion_Host_Communication' + properties: { + protocol: '*' + sourcePortRange: '*' + sourceAddressPrefix: 'VirtualNetwork' + destinationAddressPrefix: 'VirtualNetwork' + access: 'Allow' + priority: 700 + direction: 'Inbound' + sourcePortRanges: [] + destinationPortRanges: [ + '8080' + '5701' + ] + sourceAddressPrefixes: [] + destinationAddressPrefixes: [] + } +} + +param ruleOutAllowBastionComms bool = false +resource ruleBastionEgressSshRdp 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleOutAllowBastionComms) { + parent: nsg + name: 'Allow_SshRdp_Outbound' + properties: { + protocol: '*' + sourcePortRange: '*' + sourceAddressPrefix: '*' + destinationAddressPrefix: 'VirtualNetwork' + access: 'Allow' + priority: 200 + direction: 'Outbound' + sourcePortRanges: [] + destinationPortRanges: [ + '22' + '3389' + ] + sourceAddressPrefixes: [] + destinationAddressPrefixes: [] + } +} + +resource ruleBastionEgressAzure 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleOutAllowBastionComms) { + parent: nsg + name: 'Allow_Azure_Cloud_Outbound' + properties: { + protocol: 'Tcp' + sourcePortRange: '*' + sourceAddressPrefix: '*' + destinationAddressPrefix: 'AzureCloud' + access: 'Allow' + priority: 210 + direction: 'Outbound' + sourcePortRanges: [] + destinationPortRanges: [ + '443' + ] + sourceAddressPrefixes: [] + destinationAddressPrefixes: [] + } +} + +resource ruleBastionEgressBastionComms 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleOutAllowBastionComms) { + parent: nsg + name: 'Allow_Bastion_Communication' + properties: { + protocol: '*' + sourcePortRange: '*' + sourceAddressPrefix: 'VirtualNetwork' + destinationAddressPrefix: 'VirtualNetwork' + access: 'Allow' + priority: 220 + direction: 'Outbound' + sourcePortRanges: [] + destinationPortRanges: [ + '8080' + '5701' + ] + sourceAddressPrefixes: [] + destinationAddressPrefixes: [] + } +} + +resource ruleBastionEgressSessionInfo 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleOutAllowBastionComms) { + parent: nsg + name: 'Allow_Get_Session_Info' + properties: { + protocol: '*' + sourcePortRange: '*' + sourceAddressPrefix: '*' + destinationAddressPrefix: 'Internet' + access: 'Allow' + priority: 230 + direction: 'Outbound' + sourcePortRanges: [] + destinationPortRanges: [ + '80' + ] + sourceAddressPrefixes: [] + destinationAddressPrefixes: [] + } +} + +param ruleInDenySsh bool = false +resource ruleSshIngressDeny 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInDenySsh) { + parent: nsg + name: 'DenySshInbound' + properties: { + protocol: '*' + sourcePortRange: '*' + sourceAddressPrefix: '*' + destinationAddressPrefix: '*' + access: 'Deny' + priority: 100 + direction: 'Inbound' + sourcePortRanges: [] + destinationPortRanges: [ + '22' + ] + sourceAddressPrefixes: [] + destinationAddressPrefixes: [] + } +} + +param NsgDiagnosticCategories array = [ + 'NetworkSecurityGroupEvent' + 'NetworkSecurityGroupRuleCounter' +] + +resource nsgDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceResourceId)) { + name: 'diags-${nsgName}' + scope: nsg + properties: { + workspaceId: workspaceResourceId + logs: [for diagCategory in NsgDiagnosticCategories: { + category: diagCategory + enabled: true + }] + } +} + +//If multiple NSG's are trying to add flow logs at the same, this will result in CONFLICT: AnotherOperationInProgress +//Therefore advised to create FlowLogs outside of NSG creation, to better coordinate the creation - or sequence the NSG creation with DependsOn +param FlowLogStorageAccountId string = '' +param FlowLogTrafficAnalytics bool = !empty(FlowLogStorageAccountId) +module nsgFlow 'networkwatcherflowlog.bicep' = if(!empty(FlowLogStorageAccountId)) { + name: 'flow-${nsgName}' + scope: resourceGroup('NetworkWatcherRG') + params: { + location:location + name: 'flowNsg-${nsgName}' + nsgId: nsg.id + storageId: FlowLogStorageAccountId + trafficAnalytics: FlowLogTrafficAnalytics + workspaceId: workspaceId + workspaceResourceId: workspaceResourceId + workspaceRegion: workspaceRegion + } +} + +output nsgSubnetObj object = { + properties: { + networkSecurityGroup: { + id: nsg.id + } + } +} diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml new file mode 100644 index 00000000000..16167b806a1 --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: todo-nodejs-mongo-aks +metadata: + template: todo-nodejs-mongo-aks@0.0.1-beta +services: + web: + project: ../../web/react-fluent + dist: build + language: js + host: aks + api: + project: ../../api/js + language: js + host: aks diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep new file mode 100644 index 00000000000..b4a1f752af2 --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep @@ -0,0 +1,102 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +param location string + +// Optional parameters to override the default azd resource naming conventions. Update the main.parameters.json file to provide values. e.g.,: +// "resourceGroupName": { +// "value": "myGroupName" +// } +param applicationInsightsDashboardName string = '' +param applicationInsightsName string = '' +param cosmosAccountName string = '' +param cosmosDatabaseName string = '' +param keyVaultName string = '' +param logAnalyticsName string = '' +param resourceGroupName string = '' + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +// Organize resources in a resource group +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' + location: location + tags: tags +} + +module cluster '../../../../../../common/infra/bicep/core/host/aks/main.bicep' = { + name: 'aks' + scope: rg + params: { + location: location + resourceName: resourceToken + upgradeChannel: 'stable' + warIngressNginx: true + adminPrincipalId: principalId + acrPushRolePrincipalId: principalId + } +} + +// The application database +module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { + name: 'cosmos' + scope: rg + params: { + accountName: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}' + databaseName: cosmosDatabaseName + location: location + tags: tags + keyVaultName: keyVault.outputs.name + } +} + +// Store secrets in a keyvault +module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { + name: 'keyvault' + scope: rg + params: { + name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' + location: location + tags: tags + principalId: principalId + } +} + +// Monitor application with Azure Monitor +module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { + name: 'monitoring' + scope: rg + params: { + location: location + tags: tags + logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Data outputs +output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName + +// App outputs +output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId +output REACT_APP_API_BASE_URL string = '' +output REACT_APP_APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString +output REACT_APP_WEB_BASE_URL string = '' +output SERVICE_API_ENDPOINTS array = [] diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.parameters.json b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.parameters.json new file mode 100644 index 00000000000..67ad8524c44 --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + } + } +} \ No newline at end of file diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml new file mode 100644 index 00000000000..558c13f2504 --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml @@ -0,0 +1,202 @@ +templateApi: 1.0.0 +metadata: + type: repo + name: todo-nodejs-mongo-aks + description: ToDo Application with a Node.js API and Azure Cosmos DB API for MongoDB hosted in AKS + +repo: + includeProjectAssets: false + + remotes: + - name: wbreza-main + url: git@github.com:wbreza/todo-nodejs-mongo-aks.git + - name: wbreza-staging + url: git@github.com:wbreza/todo-nodejs-mongo-aks.git + branch: staging + + rewrite: + rules: + - from: ../../../../../../common/infra/bicep/core + to: ./core + patterns: + - "**/*.bicep" + + - from: ../../../../../common/infra/bicep/app + to: ./app + patterns: + - "**/*.bicep" + + - from: ../../../../../common/infra/bicep/core + to: ../core + patterns: + - "**/*.bicep" + + - from: ../../../../../common/infra/shared/gateway/apim + to: ./ + patterns: + - apim-api.bicep + + # app service modules + - from: ../../../../../../common/infra/bicep + to: ../ + patterns: + - "**/*.bicep" + ignore: + - "**/main.bicep" + + # main.bicep + - from: ../../../../../../common/infra/bicep + to: ./ + patterns: + - "**/main.bicep" + + - from: ../../api/js + to: ./src/api + patterns: + - "**/azure.@(yml|yaml)" + + - from: ../../web/react-fluent + to: ./src/web + patterns: + - "**/azure.@(yml|yaml)" + + - from: web-appservice.bicep + to: web.bicep + patterns: + - "**/main.bicep" + + - from: api-appservice-node.bicep + to: api.bicep + patterns: + - "**/main.bicep" + + - from: cosmos-mongo-db.bicep + to: db.bicep + patterns: + - "**/main.bicep" + + - from: "PLACEHOLDERIACTOOLS" + to: "" + patterns: + - "README.md" + + - from: "PLACEHOLDER_TITLE" + to: "ToDo Application with a Node.js API and Azure Cosmos DB API for MongoDB on Azure App Service" + patterns: + - "README.md" + + - from: "PLACEHOLDER_DESCRIPTION" + to: "using Bicep as the IaC provider" + patterns: + - "README.md" + + - from: ../../../../api/common/openapi.yaml + to: ../../src/api/openapi.yaml + patterns: + - "apim-api.bicep" + + assets: + # Common assets + + # Infra + - from: ./infra/ + to: ./infra + + - from: ../../../../common/infra/bicep/app/web-appservice.bicep + to: ./infra/app/web.bicep + + - from: ../../../../common/infra/bicep/app/api-appservice-node.bicep + to: ./infra/app/api.bicep + + - from: ../../../../common/infra/bicep/app/apim-api.bicep + to: ./infra/app/apim-api.bicep + + - from: ../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml + to: ./infra/app/apim-api-policy.xml + + - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep + to: ./infra/app/db.bicep + + - from: ./../../ + to: ./ + ignore: + - ".repo/**/*" + - "repo.y[a]ml" + - "azure.y[a]ml" + + # openapi.yaml to root + - from: ../../../../api/common + to: ./ + patterns: + - openapi.yaml + + # openapi.yaml to api root + - from: ../../../../api/common + to: ./src/api + patterns: + - openapi.yaml + + # Templates common + - from: ../../../../../common + to: ./ + ignore: + - .github/**/* + - .devcontainer/**/* + - "infra/**/*" + - .azdo/pipelines/*/azure-dev.yml + + # AzDo workflows for bicep + - from: ../../../../../common/.azdo/pipelines/bicep/azure-dev.yml + to: ./.azdo/pipelines/azure-dev.yml + + # Github workflows for bicep + - from: ../../../../../common/.github/workflows/bicep + to: ./.github/workflows + + # azd core modules + - from: ../../../../../common/infra/bicep + to: ./infra + + # .devcontainer common (devcontainer.json) + - from: ../../../../../common/.devcontainer/devcontainer.json/nodejs/devcontainer.json + to: ./.devcontainer/devcontainer.json + + # .devcontainer common (Dockerfile) + - from: ../../../../../common/.devcontainer/Dockerfile/base + to: ./.devcontainer + + # Assets common + - from: ../../../../common/assets + to: ./assets + + # Tests common + - from: ../../../../common/tests + to: ./tests + + # Auth JS common + - from: ../../../../common/auth/js + to: ./src/api/src + + # Node JS API + - from: ../../../../api/js + to: ./src/api + ignore: + - "dist/**/*" + - "coverage/**/*" + - "node_modules/**/*" + - "**/*.log" + + # React Frontend + - from: ../../../../web/react-fluent + to: ./src/web + ignore: + - "build/**/*" + - "node_modules/**/*" + + # Infra + - from: ./infra/ + to: ./infra + + # Azure.yml + - from: ./azure.yaml + to: ./azure.yaml diff --git a/templates/todo/projects/nodejs-mongo-aks/.vscode/launch.json b/templates/todo/projects/nodejs-mongo-aks/.vscode/launch.json new file mode 100644 index 00000000000..709bf3d1c0f --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/.vscode/launch.json @@ -0,0 +1,47 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Web", + "request": "launch", + "type": "msedge", + "webRoot": "${workspaceFolder}/src/web/src", + "url": "http://localhost:3000", + "sourceMapPathOverrides": { + "webpack:///src/*": "${webRoot}/*" + }, + }, + + { + "name": "Debug API", + "request": "launch", + "runtimeArgs": [ + "run", + "start" + ], + "runtimeExecutable": "npm", + "skipFiles": [ + "/**" + ], + "type": "pwa-node", + "cwd": "${workspaceFolder}/src/api", + "envFile": "${input:dotEnvFilePath}", + "env": { + "NODE_ENV": "development" + }, + "preLaunchTask": "Restore API", + "outputCapture": "std" + }, + ], + + "inputs": [ + { + "id": "dotEnvFilePath", + "type": "command", + "command": "azure-dev.commands.getDotEnvFilePath" + } + ] +} diff --git a/templates/todo/projects/nodejs-mongo-aks/.vscode/tasks.json b/templates/todo/projects/nodejs-mongo-aks/.vscode/tasks.json new file mode 100644 index 00000000000..935126d74d7 --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/.vscode/tasks.json @@ -0,0 +1,92 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Web", + "type": "dotenv", + "targetTasks": [ + "Restore Web", + "Web npm start" + ], + "file": "${input:dotEnvFilePath}" + }, + { + "label": "Restore Web", + "type": "shell", + "command": "azd restore --service web", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": [] + }, + { + "label": "Web npm start", + "detail": "Helper task--use 'Start Web' task to ensure environment is set up correctly", + "type": "shell", + "command": "npm run start", + "options": { + "cwd": "${workspaceFolder}/src/web/", + "env": { + "REACT_APP_API_BASE_URL": "http://localhost:3100", + "BROWSER": "none" + } + }, + "presentation": { + "panel": "dedicated", + }, + "problemMatcher": [] + }, + + { + "label": "Start API", + "type": "dotenv", + "targetTasks": [ + "Restore API", + "API npm start" + ], + "file": "${input:dotEnvFilePath}" + }, + { + "label": "Restore API", + "type": "shell", + "command": "azd restore --service api", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": [] + }, + { + "label": "API npm start", + "detail": "Helper task--use 'Start API' task to ensure environment is set up correctly", + "type": "shell", + "command": "npm run start", + "options": { + "cwd": "${workspaceFolder}/src/api/", + "env": { + "NODE_ENV": "development" + } + }, + "presentation": { + "panel": "dedicated", + }, + "problemMatcher": [] + }, + + { + "label": "Start API and Web", + "dependsOn":[ + "Start API", + "Start Web" + ], + "problemMatcher": [] + } + ], + + "inputs": [ + { + "id": "dotEnvFilePath", + "type": "command", + "command": "azure-dev.commands.getDotEnvFilePath" + } + ] +} diff --git a/templates/todo/projects/nodejs-mongo-aks/README.md b/templates/todo/projects/nodejs-mongo-aks/README.md new file mode 100644 index 00000000000..59112b9636b --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/README.md @@ -0,0 +1,210 @@ +# ToDo Application with a Node.js API and Azure Cosmos DB API for MongoDB on Azure App Service + +[![Open in Remote - Containers](https://img.shields.io/static/v1?label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/todo-nodejs-mongo) + +A complete ToDo application that includes everything you need to build, deploy, and monitor an Azure solution. This application uses the Azure Developer CLI (azd) to get you up and running on Azure quickly, React.js for the Web application, Node.js for the API, Azure Cosmos DB API for MongoDB for storage, and Azure Monitor for monitoring and logging. It includes application code, tools, and pipelines that serve as a foundation from which you can build upon and customize when creating your own solutions. + +Let's jump in and get the ToDo app up and running in Azure. When you are finished, you will have a fully functional web app deployed on Azure. In later steps, you'll see how to setup a pipeline and monitor the application. + +Screenshot of deployed ToDo app + +Screenshot of the deployed ToDo app + +### Prerequisites + +The following prerequisites are required to use this application. Please ensure that you have them all installed locally. + +- [Azure Developer CLI](https://aka.ms/azure-dev/install) + - Windows: + ```powershell + powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/install-azd.ps1' | Invoke-Expression" + ``` + - Linux/MacOS: + ``` + curl -fsSL https://aka.ms/install-azd.sh | bash + ``` +- [Azure CLI (2.38.0+)](https://docs.microsoft.com/cli/azure/install-azure-cli) +- [Node.js with npm (16.13.1+)](https://nodejs.org/) - for API backend and Web frontend +- [Git (2.36.1+)](https://git-scm.com/) +PLACEHOLDERIACTOOLS + +### Quickstart + +The fastest way for you to get this application up and running on Azure is to use the `azd up` command. This single command will create and configure all necessary Azure resources - including access policies and roles for your account and service-to-service communication with Managed Identities. + +1. Open a terminal, create a new empty folder, and change into it. +1. Run the following command to initialize the project, provision Azure resources, and deploy the application code. + +```bash +azd up --template todo-nodejs-mongo +``` + +You will be prompted for the following information: + +- `Environment Name`: This will be used as a prefix for the resource group that will be created to hold all Azure resources. This name should be unique within your Azure subscription. +- `Azure Location`: The Azure location where your resources will be deployed. +- `Azure Subscription`: The Azure Subscription where your resources will be deployed. + +> NOTE: This may take a while to complete as it executes three commands: `azd init` (initializes environment), `azd provision` (provisions Azure resources), and `azd deploy` (deploys application code). You will see a progress indicator as it provisions and deploys your application. + +When `azd up` is complete it will output the following URLs: + +- Azure Portal link to view resources +- ToDo Web application frontend +- ToDo API application + +!["azd up output"](assets/urls.png) + +Click the web application URL to launch the ToDo app. Create a new collection and add some items. This will create monitoring activity in the application that you will be able to see later when you run `azd monitor`. + +> NOTE: +> +> - The `azd up` command will create Azure resources that will incur costs to your Azure subscription. You can clean up those resources manually via the Azure portal or with the `azd down` command. +> - You can call `azd up` as many times as you like to both provision and deploy your solution, but you only need to provide the `--template` parameter the first time you call it to get the code locally. Subsequent `azd up` calls do not require the template parameter. If you do provide the parameter, all your local source code will be overwritten if you agree to overwrite when prompted. +> - You can always create a new environment with `azd env new`. + +### Application Architecture + +This application utilizes the following Azure resources: + +- [**Azure App Services**](https://docs.microsoft.com/azure/app-service/) to host the Web frontend and API backend +- [**Azure Cosmos DB API for MongoDB**](https://docs.microsoft.com/azure/cosmos-db/mongodb/mongodb-introduction) for storage +- [**Azure Monitor**](https://docs.microsoft.com/azure/azure-monitor/) for monitoring and logging +- [**Azure Key Vault**](https://docs.microsoft.com/azure/key-vault/) for securing secrets + +Here's a high level architecture diagram that illustrates these components. Notice that these are all contained within a single [resource group](https://docs.microsoft.com/azure/azure-resource-manager/management/manage-resource-groups-portal), that will be created for you when you create the resources. + +Application architecture diagram + +> This template provisions resources to an Azure subscription that you will select upon provisioning them. Please refer to the [Pricing calculator for Microsoft Azure](https://azure.microsoft.com/pricing/calculator/) and, if needed, update the included Azure resource definitions found in `infra/main.bicep` to suit your needs. + +### Application Code + +The repo is structured to follow the [Azure Developer CLI](https://aka.ms/azure-dev/overview) conventions including: + +- **Source Code**: All application source code is located in the `src` folder. +- **Infrastructure as Code**: All application "infrastructure as code" files are located in the `infra` folder. +- **Azure Developer Configuration**: An `azure.yaml` file located in the root that ties the application source code to the Azure services defined in your "infrastructure as code" files. +- **GitHub Actions**: A sample GitHub action file is located in the `.github/workflows` folder. +- **VS Code Configuration**: All VS Code configuration to run and debug the application is located in the `.vscode` folder. + +### Azure Subscription + +This template will create infrastructure and deploy code to Azure. If you don't have an Azure Subscription, you can sign up for a [free account here](https://azure.microsoft.com/free/). + +### Azure Developer CLI - VS Code Extension + +The Azure Developer experience includes an Azure Developer CLI VS Code Extension that mirrors all of the Azure Developer CLI commands into the `azure.yaml` context menu and command palette options. If you are a VS Code user, then we highly recommend installing this extension for the best experience. + +Here's how to install it: + +#### VS Code + +1. Click on the "Extensions" tab in VS Code +1. Search for "Azure Developer CLI" - authored by Microsoft +1. Click "Install" + +#### Marketplace + +1. Go to the [Azure Developer CLI - VS Code Extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azure-dev) page +1. Click "Install" + +Once the extension is installed, you can press `F1`, and type "Azure Developer CLI" to see all of your available options. You can also right click on your project's `azure.yaml` file for a list of commands. + +### Next Steps + +At this point, you have a complete application deployed on Azure. But there is much more that the Azure Developer CLI can do. These next steps will introduce you to additional commands that will make creating applications on Azure much easier. Using the Azure Developer CLI, you can setup your pipelines, monitor your application, test and debug locally. + +#### Set up a pipeline using `azd pipeline` + +This template includes a GitHub Actions pipeline configuration file that will deploy your application whenever code is pushed to the main branch. You can find that pipeline file here: `.github/workflows`. + +Setting up this pipeline requires you to give GitHub permission to deploy to Azure on your behalf, which is done via a Service Principal stored in a GitHub secret named `AZURE_CREDENTIALS`. The `azd pipeline config` command will automatically create a service principal for you. The command also helps to create a private GitHub repository and pushes code to the newly created repo. + +Before you call the `azd pipeline config` command, you'll need to install the following: + +- [GitHub CLI (2.3+)](https://github.com/cli/cli) + +Run the following command to set up a GitHub Action: + +```bash +azd pipeline config +``` + +> Support for Azure DevOps Pipelines is coming soon to `azd pipeline config`. In the meantime, you can follow the instructions found here: [.azdo/pipelines/README.md](./.azdo/pipelines/README.md) to set it up manually. + +#### Monitor the application using `azd monitor` + +To help with monitoring applications, the Azure Dev CLI provides a `monitor` command to help you get to the various Application Insights dashboards. + +- Run the following command to open the "Overview" dashboard: + + ```bash + azd monitor --overview + ``` + +- Live Metrics Dashboard + + Run the following command to open the "Live Metrics" dashboard: + + ```bash + azd monitor --live + ``` + +- Logs Dashboard + + Run the following command to open the "Logs" dashboard: + + ```bash + azd monitor --logs + ``` + +#### Run and Debug Locally + +The easiest way to run and debug is to leverage the Azure Developer CLI Visual Studio Code Extension. Refer to this [walk-through](https://aka.ms/azure-dev/vscode) for more details. + +#### Clean up resources + +When you are done, you can delete all the Azure resources created with this template by running the following command: + +```bash +azd down +``` + +### Additional azd commands + +The Azure Developer CLI includes many other commands to help with your Azure development experience. You can view these commands at the terminal by running `azd help`. You can also view the full list of commands on our [Azure Developer CLI command](https://aka.ms/azure-dev/ref) page. + +## Troubleshooting/Known issues + +Sometimes, things go awry. If you happen to run into issues, then please review our ["Known Issues"](https://aka.ms/azure-dev/knownissues) page for help. If you continue to have issues, then please file an issue in our main [Azure Dev](https://aka.ms/azure-dev/issues) repository. + +## Security + +### Roles + +This template creates a [managed identity](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) for your app inside your Azure Active Directory tenant, and it is used to authenticate your app with Azure and other services that support Azure AD authentication like Key Vault via access policies. You will see principalId referenced in the infrastructure as code files, that refers to the id of the currently logged in Azure CLI user, which will be granted access policies and permissions to run the application locally. To view your managed identity in the Azure Portal, follow these [steps](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-view-managed-identity-service-principal-portal). + +### Key Vault + +This template uses [Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/overview) to securely store your Cosmos DB connection string for the provisioned Cosmos DB account. Key Vault is a cloud service for securely storing and accessing secrets (API keys, passwords, certificates, cryptographic keys) and makes it simple to give other Azure services access to them. As you continue developing your solution, you may add as many secrets to your Key Vault as you require. + +## Uninstall + +To uninstall the Azure Developer CLI: + +Windows: + +``` +powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/uninstall-azd.ps1' | Invoke-Expression" +``` + +Linux/MacOS: + +``` +curl -fsSL https://aka.ms/uninstall-azd.sh | bash +``` + +## Reporting Issues and Feedback + +If you have any feature requests, issues, or areas for improvement, please [file an issue](https://aka.ms/azure-dev/issues). To keep up-to-date, ask questions, or share suggestions, join our [GitHub Discussions](https://aka.ms/azure-dev/discussions). You may also contact us via AzDevTeam@microsoft.com. diff --git a/templates/todo/projects/nodejs-mongo-aks/assets/resources.png b/templates/todo/projects/nodejs-mongo-aks/assets/resources.png new file mode 100644 index 0000000000000000000000000000000000000000..07f06cb01a671f5a3db821b615a8e65e9c52626c GIT binary patch literal 123324 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 z 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 literal 0 HcmV?d00001 diff --git a/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml new file mode 100644 index 00000000000..ae5c5996154 --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: todo-api +spec: + replicas: 1 + selector: + matchLabels: + app: todo-api + template: + metadata: + labels: + app: todo-api + spec: + containers: + - name: todo-api + image: crppnnadqdq7owqmxjqvwcid425o.azurecr.io/todo-nodejs-mongo-aks/api:latest + ports: + - containerPort: 3100 + env: + - name: AZURE_CLIENT_ID + value: 8111defe-2f69-4893-b025-ffeaea359c4a + - name: AZURE_KEY_VAULT_ENDPOINT + valueFrom: + secretKeyRef: + name: azd + key: AZURE_KEY_VAULT_ENDPOINT + optional: false + - name: APPLICATIONINSIGHTS_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: azd + key: APPLICATIONINSIGHTS_CONNECTION_STRING + optional: false diff --git a/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/ingress.yaml b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/ingress.yaml new file mode 100644 index 00000000000..c2baf8239ac --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/ingress.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: todo-ingress-api + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + ingressClassName: webapprouting.kubernetes.azure.com + rules: + - http: + paths: + - path: /api(/|$)(.*) + pathType: Prefix + backend: + service: + name: todo-api + port: + number: 80 diff --git a/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/service.yaml b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/service.yaml new file mode 100644 index 00000000000..a1622390d2b --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: todo-api +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 3100 + selector: + app: todo-api diff --git a/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml b/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml new file mode 100644 index 00000000000..512d0a80443 --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml @@ -0,0 +1,28 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: todo-web +spec: + replicas: 1 + selector: + matchLabels: + app: todo-web + template: + metadata: + labels: + app: todo-web + spec: + containers: + - name: todo-web + image: crppnnadqdq7owqmxjqvwcid425o.azurecr.io/todo-nodejs-mongo-aks/web:latest + ports: + - containerPort: 3000 + env: + - name: REACT_APP_API_BASE_URL + value: /api + - name: REACT_APP_APPLICATIONINSIGHTS_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: azd + key: APPLICATIONINSIGHTS_CONNECTION_STRING + optional: false diff --git a/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/ingress.yaml b/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/ingress.yaml new file mode 100644 index 00000000000..5676d5a9e01 --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/ingress.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: todo-ingress-web +spec: + ingressClassName: webapprouting.kubernetes.azure.com + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: todo-web + port: + number: 80 diff --git a/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/service.yaml b/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/service.yaml new file mode 100644 index 00000000000..ff91f4ba4b9 --- /dev/null +++ b/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: todo-web +spec: + type: ClusterIP + ports: + - port: 80 + selector: + app: todo-web From 657a538c23e39736f2f02c187016b6b42d0e8763 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 10 Feb 2023 17:43:05 -0800 Subject: [PATCH 02/25] Replace env vars in k8s manifests --- cli/azd/pkg/infra/azure_resource_types.go | 3 + cli/azd/pkg/project/service_target_aks.go | 10 +-- cli/azd/pkg/tools/kubectl/kube_config.go | 5 +- cli/azd/pkg/tools/kubectl/kubectl.go | 74 +++++++++++++++---- .../infra/bicep/core/host/aks/main.bicep | 4 + .../.repo/bicep/infra/main.bicep | 4 + 6 files changed, 80 insertions(+), 20 deletions(-) diff --git a/cli/azd/pkg/infra/azure_resource_types.go b/cli/azd/pkg/infra/azure_resource_types.go index e74fb446342..a5d9b2d4cba 100644 --- a/cli/azd/pkg/infra/azure_resource_types.go +++ b/cli/azd/pkg/infra/azure_resource_types.go @@ -28,6 +28,7 @@ const ( AzureResourceTypeContainerRegistry AzureResourceType = "Microsoft.ContainerRegistry/registries" AzureResourceTypeManagedCluster AzureResourceType = "Microsoft.ContainerService/managedClusters" AzureResourceTypeServicePlan AzureResourceType = "Microsoft.Web/serverfarms" + AzureResourceTypeAgentPool AzureResourceType = "Microsoft.ContainerService/managedClusters/agentPools" AzureResourceTypeSqlServer AzureResourceType = "Microsoft.Sql/servers" AzureResourceTypeVirtualNetwork AzureResourceType = "Microsoft.Network/virtualNetworks" AzureResourceTypeWebSite AzureResourceType = "Microsoft.Web/sites" @@ -84,6 +85,8 @@ func GetResourceTypeDisplayName(resourceType AzureResourceType) string { return "Container Registry" case AzureResourceTypeManagedCluster: return "AKS Managed Cluster" + case AzureResourceTypeAgentPool: + return "AKS Agent Pool" } return "" diff --git a/cli/azd/pkg/project/service_target_aks.go b/cli/azd/pkg/project/service_target_aks.go index a906b7e1907..f574103be39 100644 --- a/cli/azd/pkg/project/service_target_aks.go +++ b/cli/azd/pkg/project/service_target_aks.go @@ -79,19 +79,18 @@ func (t *aksTarget) Deploy(ctx context.Context, azdCtx *azdcontext.AzdContext, p return ServiceDeploymentResult{}, fmt.Errorf("failed creating kube namespace: %w", err) } - _, err = t.kubectl.ApplyPipe(ctx, *namespaceResult, nil) + _, err = t.kubectl.ApplyPipe(ctx, *&namespaceResult.Stdout, nil) if err != nil { return ServiceDeploymentResult{}, fmt.Errorf("failed applying kube namespace: %w", err) } progress <- "Creating k8s secrets" - secrets := t.env.Environ() - secretResult, err := t.kubectl.CreateSecretGenericFromLiterals(ctx, "azd", secrets, &kubeFlags) + secretResult, err := t.kubectl.CreateSecretGenericFromLiterals(ctx, "azd", t.env.Environ(), &kubeFlags) if err != nil { return ServiceDeploymentResult{}, fmt.Errorf("failed setting kube secrets: %w", err) } - _, err = t.kubectl.ApplyPipe(ctx, *secretResult, nil) + _, err = t.kubectl.ApplyPipe(ctx, *&secretResult.Stdout, nil) if err != nil { return ServiceDeploymentResult{}, fmt.Errorf("failed applying kube secrets: %w", err) } @@ -135,7 +134,8 @@ func (t *aksTarget) Deploy(ctx context.Context, azdCtx *azdcontext.AzdContext, p } progress <- "Applying k8s manifests" - _, err = t.kubectl.ApplyFiles(ctx, filepath.Join(t.config.RelativePath, "manifests"), &kubectl.KubeCliFlags{Namespace: namespace}) + t.kubectl.SetEnv(t.env.Values) + err = t.kubectl.ApplyFiles(ctx, filepath.Join(t.config.RelativePath, "manifests"), &kubectl.KubeCliFlags{Namespace: namespace}) if err != nil { return ServiceDeploymentResult{}, fmt.Errorf("failed applying kube manifests: %w", err) } diff --git a/cli/azd/pkg/tools/kubectl/kube_config.go b/cli/azd/pkg/tools/kubectl/kube_config.go index f9af9ffc513..a7f096c62f9 100644 --- a/cli/azd/pkg/tools/kubectl/kube_config.go +++ b/cli/azd/pkg/tools/kubectl/kube_config.go @@ -106,7 +106,10 @@ func (kcm *KubeConfigManager) MergeConfigs(ctx context.Context, newConfigName st fullConfigPaths = append(fullConfigPaths, filepath.Join(kcm.configPath, kubeConfigName)) } - kcm.cli.SetEnv(fmt.Sprintf("KUBECONFIG=%s", strings.Join(fullConfigPaths, string(os.PathListSeparator)))) + envValues := map[string]string{ + "KUBECONFIG": strings.Join(fullConfigPaths, string(os.PathListSeparator)), + } + kcm.cli.SetEnv(envValues) res, err := kcm.cli.ConfigView(ctx, true, true, nil) if err != nil { return fmt.Errorf("kubectl config view failed: %w", err) diff --git a/cli/azd/pkg/tools/kubectl/kubectl.go b/cli/azd/pkg/tools/kubectl/kubectl.go index 60c73ceb84d..49700adee03 100644 --- a/cli/azd/pkg/tools/kubectl/kubectl.go +++ b/cli/azd/pkg/tools/kubectl/kubectl.go @@ -4,30 +4,33 @@ import ( "context" "encoding/json" "fmt" + "os" + "path/filepath" "strings" "github.com/azure/azure-dev/cli/azd/pkg/exec" "github.com/azure/azure-dev/cli/azd/pkg/tools" + "github.com/drone/envsubst" ) type KubectlCli interface { tools.ExternalTool Cwd(cwd string) - SetEnv(env ...string) + SetEnv(env map[string]string) GetNodes(ctx context.Context, flags *KubeCliFlags) ([]Node, error) - ApplyFiles(ctx context.Context, path string, flags *KubeCliFlags) (*exec.RunResult, error) + ApplyFiles(ctx context.Context, path string, flags *KubeCliFlags) error + ApplyPipe(ctx context.Context, input string, flags *KubeCliFlags) (*exec.RunResult, error) ApplyKustomize(ctx context.Context, path string, flags *KubeCliFlags) (*exec.RunResult, error) ConfigView(ctx context.Context, merge bool, flatten bool, flags *KubeCliFlags) (*exec.RunResult, error) ConfigUseContext(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) CreateNamespace(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) CreateSecretGenericFromLiterals(ctx context.Context, name string, secrets []string, flags *KubeCliFlags) (*exec.RunResult, error) - ApplyPipe(ctx context.Context, result exec.RunResult, flags *KubeCliFlags) (*exec.RunResult, error) } type kubectlCli struct { tools.ExternalTool commandRunner exec.CommandRunner - env []string + env map[string]string cwd string } @@ -43,8 +46,8 @@ func (cli *kubectlCli) Name() string { return "kubectl" } -func (cli *kubectlCli) SetEnv(env ...string) { - cli.env = env +func (cli *kubectlCli) SetEnv(envValues map[string]string) { + cli.env = envValues } func (cli *kubectlCli) Cwd(cwd string) { @@ -76,7 +79,7 @@ func (cli *kubectlCli) ConfigView(ctx context.Context, merge bool, flatten bool, runArgs := exec.NewRunArgs("kubectl", args...). WithCwd(kubeConfigDir). - WithEnv(cli.env) + WithEnv(environ(cli.env)) res, err := cli.executeCommandWithArgs(ctx, runArgs, flags) if err != nil { @@ -112,10 +115,11 @@ func (cli *kubectlCli) GetNodes(ctx context.Context, flags *KubeCliFlags) ([]Nod return nodes, nil } -func (cli *kubectlCli) ApplyPipe(ctx context.Context, result exec.RunResult, flags *KubeCliFlags) (*exec.RunResult, error) { +func (cli *kubectlCli) ApplyPipe(ctx context.Context, input string, flags *KubeCliFlags) (*exec.RunResult, error) { runArgs := exec. NewRunArgs("kubectl", "apply", "-f", "-"). - WithStdIn(strings.NewReader(result.Stdout)) + WithEnv(environ(cli.env)). + WithStdIn(strings.NewReader(input)) res, err := cli.executeCommandWithArgs(ctx, runArgs, flags) if err != nil { @@ -125,14 +129,47 @@ func (cli *kubectlCli) ApplyPipe(ctx context.Context, result exec.RunResult, fla return &res, nil } -func (cli *kubectlCli) ApplyFiles(ctx context.Context, path string, flags *KubeCliFlags) (*exec.RunResult, error) { - - res, err := cli.executeCommand(ctx, flags, "apply", "-f", path) +func (cli *kubectlCli) ApplyFiles(ctx context.Context, path string, flags *KubeCliFlags) error { + entries, err := os.ReadDir(path) if err != nil { - return nil, fmt.Errorf("kubectl apply -f: %w", err) + return fmt.Errorf("failed reading files in path, '%s', %w", path, err) } - return &res, nil + for _, entry := range entries { + if entry.IsDir() { + continue + } + + ext := filepath.Ext(entry.Name()) + if !(ext == ".yaml" || ext == ".yml") { + continue + } + + filePath := filepath.Join(path, entry.Name()) + fileBytes, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed reading manifest file '%s', %w", filePath, err) + } + + yaml := string(fileBytes) + replaced, err := envsubst.Eval(yaml, func(name string) string { + if val, has := cli.env[name]; has { + return val + } + return os.Getenv(name) + }) + + if err != nil { + return fmt.Errorf("failed replacing env vars, %w", err) + } + + _, err = cli.ApplyPipe(ctx, replaced, flags) + if err != nil { + return fmt.Errorf("failed applying manifest, %w", err) + } + } + + return nil } func (cli *kubectlCli) ApplyKustomize(ctx context.Context, path string, flags *KubeCliFlags) (*exec.RunResult, error) { @@ -209,3 +246,12 @@ func NewKubectl(commandRunner exec.CommandRunner) KubectlCli { commandRunner: commandRunner, } } + +func environ(values map[string]string) []string { + env := []string{} + for key, value := range values { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + + return env +} diff --git a/templates/common/infra/bicep/core/host/aks/main.bicep b/templates/common/infra/bicep/core/host/aks/main.bicep index 47553d34e04..dae52c719b5 100644 --- a/templates/common/infra/bicep/core/host/aks/main.bicep +++ b/templates/common/infra/bicep/core/host/aks/main.bicep @@ -391,6 +391,9 @@ param acrUntaggedRetentionPolicyEnabled bool = false @description('The number of days to retain untagged manifests for') param acrUntaggedRetentionPolicy int = 30 +@description('Enable admin user for ACR push/pull') +param acrAdminUserEnabled bool = true + var acrName = 'cr${replace(resourceName, '-', '')}${uniqueString(resourceGroup().id, resourceName)}' resource acr 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = if (!empty(registries_sku)) { @@ -413,6 +416,7 @@ resource acr 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = if (! } publicNetworkAccess: privateLinks /* && empty(acrIPWhitelist)*/ ? 'Disabled' : 'Enabled' zoneRedundancy: acrZoneRedundancyEnabled + adminUserEnabled: acrAdminUserEnabled /* networkRuleSet: { defaultAction: 'Deny' diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep index b4a1f752af2..76e123b57eb 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep @@ -45,6 +45,7 @@ module cluster '../../../../../../common/infra/bicep/core/host/aks/main.bicep' = warIngressNginx: true adminPrincipalId: principalId acrPushRolePrincipalId: principalId + registries_sku: 'Standard' } } @@ -96,6 +97,9 @@ output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId +output AZURE_AKS_CLUSTER_NAME string = cluster.outputs.aksClusterName +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = '${cluster.outputs.containerRegistryName}.azurecr.io' +output AZURE_CONTAINER_REGISTRY_NAME string = cluster.outputs.containerRegistryName output REACT_APP_API_BASE_URL string = '' output REACT_APP_APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString output REACT_APP_WEB_BASE_URL string = '' From 8b06c1b3212e6f2f5b6c6611e1f79467367aa5c5 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 15 Feb 2023 11:02:37 -0800 Subject: [PATCH 03/25] Bicep updates for AKS --- .../nodejs-mongo-aks/.repo/bicep/repo.yaml | 17 ----------------- .../src/api/manifests/deployment.yaml | 4 ++-- .../src/web/manifests/deployment.yaml | 2 +- 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml index 558c13f2504..09ec837ab8d 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml @@ -31,11 +31,6 @@ repo: patterns: - "**/*.bicep" - - from: ../../../../../common/infra/shared/gateway/apim - to: ./ - patterns: - - apim-api.bicep - # app service modules - from: ../../../../../../common/infra/bicep to: ../ @@ -102,18 +97,6 @@ repo: - from: ./infra/ to: ./infra - - from: ../../../../common/infra/bicep/app/web-appservice.bicep - to: ./infra/app/web.bicep - - - from: ../../../../common/infra/bicep/app/api-appservice-node.bicep - to: ./infra/app/api.bicep - - - from: ../../../../common/infra/bicep/app/apim-api.bicep - to: ./infra/app/apim-api.bicep - - - from: ../../../../../common/infra/shared/gateway/apim/apim-api-policy.xml - to: ./infra/app/apim-api-policy.xml - - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep to: ./infra/app/db.bicep diff --git a/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml index ae5c5996154..7a19c28a9ce 100644 --- a/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml +++ b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml @@ -14,12 +14,12 @@ spec: spec: containers: - name: todo-api - image: crppnnadqdq7owqmxjqvwcid425o.azurecr.io/todo-nodejs-mongo-aks/api:latest + image: ${AZURE_CONTAINER_REGISTRY_ENDPOINT}/todo-nodejs-mongo-aks/api:latest ports: - containerPort: 3100 env: - name: AZURE_CLIENT_ID - value: 8111defe-2f69-4893-b025-ffeaea359c4a + value: ${AZURE_PRINCIPAL_ID} - name: AZURE_KEY_VAULT_ENDPOINT valueFrom: secretKeyRef: diff --git a/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml b/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml index 512d0a80443..9e14d550fce 100644 --- a/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml +++ b/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml @@ -14,7 +14,7 @@ spec: spec: containers: - name: todo-web - image: crppnnadqdq7owqmxjqvwcid425o.azurecr.io/todo-nodejs-mongo-aks/web:latest + image: ${AZURE_CONTAINER_REGISTRY_ENDPOINT}/todo-nodejs-mongo-aks/web:latest ports: - containerPort: 3000 env: From ffef76569291b63cfe75709c25ebf71f6395ea87 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 17 Feb 2023 11:44:42 -0800 Subject: [PATCH 04/25] Add kubectl identity to keyvault access --- .../infra/bicep/core/host/aks/main.bicep | 490 +++++++++--------- .../.repo/bicep/infra/main.bicep | 11 + .../src/api/manifests/deployment.yaml | 2 +- 3 files changed, 254 insertions(+), 249 deletions(-) diff --git a/templates/common/infra/bicep/core/host/aks/main.bicep b/templates/common/infra/bicep/core/host/aks/main.bicep index dae52c719b5..a90de304ea3 100644 --- a/templates/common/infra/bicep/core/host/aks/main.bicep +++ b/templates/common/infra/bicep/core/host/aks/main.bicep @@ -20,7 +20,6 @@ Resource sections 9. Deployment for telemetry */ - /*.__ __. _______ .___________.____ __ ____ ______ .______ __ ___ __ .__ __. _______ | \ | | | ____|| |\ \ / \ / / / __ \ | _ \ | |/ / | | | \ | | / _____| | \| | | |__ `---| |----` \ \/ \/ / | | | | | |_) | | ' / | | | \| | | | __ @@ -119,7 +118,7 @@ module network './network.bicep' = if (custom_vnet) { params: { resourceName: resourceName location: location - networkPluginIsKubenet: networkPlugin=='kubenet' + networkPluginIsKubenet: networkPlugin == 'kubenet' vnetAddressPrefix: vnetAddressPrefix aksPrincipleId: createAksUai ? aksUai.properties.principalId : '' vnetAksSubnetAddressPrefix: vnetAksSubnetAddressPrefix @@ -138,7 +137,7 @@ module network './network.bicep' = if (custom_vnet) { bastionSubnetAddressPrefix: bastionSubnetAddressPrefix availabilityZones: availabilityZones workspaceName: createLaw ? aks_law.name : '' - workspaceResourceGroupName: createLaw ? resourceGroup().name : '' + workspaceResourceGroupName: createLaw ? resourceGroup().name : '' networkSecurityGroups: CreateNetworkSecurityGroups CreateNsgFlowLogs: CreateNetworkSecurityGroups && CreateNetworkSecurityGroupFlowLogs ingressApplicationGatewayPublic: empty(privateIpApplicationGateway) @@ -153,7 +152,6 @@ output CustomVnetPrivateLinkSubnetId string = custom_vnet ? network.outputs.priv var aksSubnetId = custom_vnet ? network.outputs.aksSubnetId : byoAKSSubnetId var appGwSubnetId = ingressApplicationGateway ? (custom_vnet ? network.outputs.appGwSubnetId : byoAGWSubnetId) : '' - /*______ .__ __. _______. ________ ______ .__ __. _______ _______. | \ | \ | | / | | / / __ \ | \ | | | ____| / | | .--. || \| | | (----` `---/ / | | | | | \| | | |__ | (----` @@ -174,7 +172,6 @@ module dnsZone './dnsZoneRbac.bicep' = if (!empty(dnsZoneId)) { } } - /*__ __ _______ ____ ____ ____ ____ ___ __ __ __ .___________. | |/ / | ____|\ \ / / \ \ / / / \ | | | | | | | | | ' / | |__ \ \/ / \ \/ / / ^ \ | | | | | | `---| |----` @@ -201,7 +198,7 @@ param keyVaultAksCSI bool = false param keyVaultAksCSIPollInterval string = '2m' @description('Creates a KeyVault for application secrets (eg. CSI)') -module kv 'keyvault.bicep' = if(keyVaultCreate) { +module kv 'keyvault.bicep' = if (keyVaultCreate) { name: 'keyvaultApps' params: { resourceName: resourceName @@ -220,7 +217,7 @@ var keyVaultOfficerRolePrincipalIds = [ ] @description('Parsing an array with union ensures that duplicates are removed, which is great when dealing with highly conditional elements') -var rbacSecretUserSps = union([deployAppGw && appgwKVIntegration ? appGwIdentity.properties.principalId : ''],[keyVaultAksCSI ? aks.properties.addonProfiles.azureKeyvaultSecretsProvider.identity.objectId : '']) +var rbacSecretUserSps = union([ deployAppGw && appgwKVIntegration ? appGwIdentity.properties.principalId : '' ], [ keyVaultAksCSI ? aks.properties.addonProfiles.azureKeyvaultSecretsProvider.identity.objectId : '' ]) @description('A seperate module is used for RBAC to avoid delaying the KeyVault creation and causing a circular reference.') module kvRbac 'keyvaultrbac.bicep' = if (keyVaultCreate) { @@ -242,7 +239,6 @@ module kvRbac 'keyvaultrbac.bicep' = if (keyVaultCreate) { output keyVaultName string = keyVaultCreate ? kv.outputs.keyVaultName : '' output keyVaultId string = keyVaultCreate ? kv.outputs.keyVaultId : '' - /* KeyVault for KMS Etcd*/ @description('Enable encryption at rest for Kubernetes etcd data') @@ -258,21 +254,21 @@ param keyVaultKmsByoRG string = resourceGroup().name param keyVaultKmsOfficerRolePrincipalId string = '' @description('The extracted name of the existing Key Vault') -var keyVaultKmsByoName = !empty(keyVaultKmsByoKeyId) ? split(split(keyVaultKmsByoKeyId,'/')[2],'.')[0] : '' +var keyVaultKmsByoName = !empty(keyVaultKmsByoKeyId) ? split(split(keyVaultKmsByoKeyId, '/')[2], '.')[0] : '' @description('The deployment delay to introduce when creating a new keyvault for kms key') -var kmsRbacWaitSeconds=30 +var kmsRbacWaitSeconds = 30 @description('This indicates if the deploying user has provided their PrincipalId in order for the key to be created') var keyVaultKmsCreateAndPrereqs = keyVaultKmsCreate && !empty(keyVaultKmsOfficerRolePrincipalId) && privateLinks == false -resource kvKmsByo 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = if(!empty(keyVaultKmsByoName)) { +resource kvKmsByo 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = if (!empty(keyVaultKmsByoName)) { name: keyVaultKmsByoName scope: resourceGroup(keyVaultKmsByoRG) } @description('Creates a new Key vault for a new KMS Key') -module kvKms 'keyvault.bicep' = if(keyVaultKmsCreateAndPrereqs) { +module kvKms 'keyvault.bicep' = if (keyVaultKmsCreateAndPrereqs) { name: 'keyvaultKms-${resourceName}' params: { resourceName: 'kms${resourceName}' @@ -284,7 +280,7 @@ module kvKms 'keyvault.bicep' = if(keyVaultKmsCreateAndPrereqs) { } } -module kvKmsCreatedRbac 'keyvaultrbac.bicep' = if(keyVaultKmsCreateAndPrereqs) { +module kvKmsCreatedRbac 'keyvaultrbac.bicep' = if (keyVaultKmsCreateAndPrereqs) { name: 'keyvaultKmsRbacs-${resourceName}' params: { keyVaultName: keyVaultKmsCreate ? kvKms.outputs.keyVaultName : '' @@ -307,13 +303,13 @@ module kvKmsCreatedRbac 'keyvaultrbac.bicep' = if(keyVaultKmsCreateAndPrereqs) { } } -module kvKmsByoRbac 'keyvaultrbac.bicep' = if(!empty(keyVaultKmsByoKeyId)) { +module kvKmsByoRbac 'keyvaultrbac.bicep' = if (!empty(keyVaultKmsByoKeyId)) { name: 'keyvaultKmsByoRbacs-${resourceName}' scope: resourceGroup(keyVaultKmsByoRG) params: { keyVaultName: kvKmsByo.name //Contribuor allows AKS to create the private link - rbacKvContributorSps : [ + rbacKvContributorSps: [ createAksUai && privateLinks ? aksUai.properties.principalId : '' ] //This allows the Aks Cluster to access the key vault key @@ -324,7 +320,7 @@ module kvKmsByoRbac 'keyvaultrbac.bicep' = if(!empty(keyVaultKmsByoKeyId)) { } @description('It can take time for the RBAC to propagate, this delays the deployment to avoid this problem') -module waitForKmsRbac 'br/public:deployment-scripts/wait:1.0.1' = if(keyVaultKmsCreateAndPrereqs && kmsRbacWaitSeconds>0) { +module waitForKmsRbac 'br/public:deployment-scripts/wait:1.0.1' = if (keyVaultKmsCreateAndPrereqs && kmsRbacWaitSeconds > 0) { name: 'keyvaultKmsRbac-waits-${resourceName}' params: { waitSeconds: kmsRbacWaitSeconds @@ -336,21 +332,21 @@ module waitForKmsRbac 'br/public:deployment-scripts/wait:1.0.1' = if(keyVaultKms } @description('Adding a key to the keyvault... We can only do this for public key vaults') -module kvKmsKey 'keyvaultkey.bicep' = if(keyVaultKmsCreateAndPrereqs) { +module kvKmsKey 'keyvaultkey.bicep' = if (keyVaultKmsCreateAndPrereqs) { name: 'keyvaultKmsKeys-${resourceName}' params: { keyVaultName: keyVaultKmsCreateAndPrereqs ? kvKms.outputs.keyVaultName : '' } - dependsOn: [waitForKmsRbac] + dependsOn: [ waitForKmsRbac ] } var azureKeyVaultKms = { - securityProfile : { - azureKeyVaultKms : { + securityProfile: { + azureKeyVaultKms: { enabled: keyVaultKmsCreateAndPrereqs || !empty(keyVaultKmsByoKeyId) keyId: keyVaultKmsCreateAndPrereqs ? kvKmsKey.outputs.keyVaultKmsKeyUri : !empty(keyVaultKmsByoKeyId) ? keyVaultKmsByoKeyId : '' keyVaultNetworkAccess: privateLinks ? 'private' : 'public' - keyVaultResourceId: privateLinks && !empty(keyVaultKmsByoKeyId) ? kvKmsByo.id : '' + keyVaultResourceId: privateLinks && !empty(keyVaultKmsByoKeyId) ? kvKmsByo.id : '' } } } @@ -359,8 +355,7 @@ var azureKeyVaultKms = { output keyVaultKmsName string = keyVaultKmsCreateAndPrereqs ? kvKms.outputs.keyVaultName : !empty(keyVaultKmsByoKeyId) ? keyVaultKmsByoName : '' @description('Indicates if the user has supplied all the correct parameter to use a AKSC Created KMS') -output kmsCreatePrerequisitesMet bool = keyVaultKmsCreateAndPrereqs - +output kmsCreatePrerequisitesMet bool = keyVaultKmsCreateAndPrereqs /* ___ ______ .______ / \ / | | _ \ @@ -435,12 +430,11 @@ resource acr 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = if (! output containerRegistryName string = !empty(registries_sku) ? acr.name : '' output containerRegistryId string = !empty(registries_sku) ? acr.id : '' - resource acrDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (createLaw && !empty(registries_sku)) { name: 'acrDiags' scope: acr properties: { - workspaceId:aks_law.id + workspaceId: aks_law.id logs: [ { category: 'ContainerRegistryRepositoryEvents' @@ -476,7 +470,7 @@ var KubeletObjectId = any(aks.properties.identityProfile.kubeletidentity).object resource aks_acr_pull 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(registries_sku)) { scope: acr // Use when specifying a scope that is different than the deployment scope - name: guid(aks.id, 'Acr' , AcrPullRole) + name: guid(aks.id, 'Acr', AcrPullRole) properties: { roleDefinitionId: AcrPullRole principalType: 'ServicePrincipal' @@ -491,7 +485,7 @@ param acrPushRolePrincipalId string = '' resource aks_acr_push 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(registries_sku) && !empty(acrPushRolePrincipalId)) { scope: acr // Use when specifying a scope that is different than the deployment scope - name: guid(aks.id, 'Acr' , AcrPushRole) + name: guid(aks.id, 'Acr', AcrPushRole) properties: { roleDefinitionId: AcrPushRole principalType: automatedDeployment ? 'ServicePrincipal' : 'User' @@ -499,7 +493,6 @@ resource aks_acr_push 'Microsoft.Authorization/roleAssignments@2022-04-01' = if } } - param imageNames array = [] module acrImport 'br/public:deployment-scripts/import-acr:2.0.1' = if (!empty(registries_sku) && !empty(imageNames)) { @@ -511,9 +504,6 @@ module acrImport 'br/public:deployment-scripts/import-acr:2.0.1' = if (!empty(re } } - - - /*______ __ .______ _______ ____ __ ____ ___ __ __ | ____|| | | _ \ | ____|\ \ / \ / / / \ | | | | | |__ | | | |_) | | |__ \ \/ \/ / / ^ \ | | | | @@ -550,7 +540,7 @@ module firewall './firewall.bicep' = if (azureFirewalls && custom_vnet) { workspaceDiagsId: createLaw ? aks_law.id : '' fwSubnetId: azureFirewalls && custom_vnet ? network.outputs.fwSubnetId : '' fwSku: azureFirewallSku - fwManagementSubnetId: azureFirewalls && custom_vnet && azureFirewallSku=='Basic' ? network.outputs.fwMgmtSubnetId : '' + fwManagementSubnetId: azureFirewalls && custom_vnet && azureFirewallSku == 'Basic' ? network.outputs.fwMgmtSubnetId : '' vnetAksSubnetAddressPrefix: vnetAksSubnetAddressPrefix certManagerFW: certManagerFW appDnsZoneName: !empty(dnsZoneId) ? split(dnsZoneId, '/')[8] : '' @@ -595,7 +585,7 @@ param appGWsku string = 'WAF_v2' param appGWenableFirewall bool = true var deployAppGw = ingressApplicationGateway && (custom_vnet || !empty(byoAGWSubnetId)) -var appGWenableWafFirewall = appGWsku=='Standard_v2' ? false : appGWenableFirewall +var appGWenableWafFirewall = appGWsku == 'Standard_v2' ? false : appGWenableFirewall // If integrating App Gateway with KeyVault, create a Identity App Gateway will use to access keyvault // 'identity' is always created (adding: "|| deployAppGw") until this is fixed: @@ -657,93 +647,93 @@ var appGwFirewallConfigOwasp = { } var appGWskuObj = union({ - name: appGWsku - tier: appGWsku -}, appGWmaxCount == 0 ? { - capacity: appGWcount -} : {}) + name: appGWsku + tier: appGWsku + }, appGWmaxCount == 0 ? { + capacity: appGWcount + } : {}) // ugh, need to create a variable with the app gateway properies, because of the conditional key 'autoscaleConfiguration' var appgwProperties = union({ - sku: appGWskuObj - sslPolicy: { - policyType: 'Predefined' - policyName: 'AppGwSslPolicy20170401S' - } - webApplicationFirewallConfiguration: appGWenableWafFirewall ? appGwFirewallConfigOwasp : json('null') - gatewayIPConfigurations: [ - { - name: 'besubnet' - properties: { - subnet: { - id: appGwSubnetId + sku: appGWskuObj + sslPolicy: { + policyType: 'Predefined' + policyName: 'AppGwSslPolicy20170401S' + } + webApplicationFirewallConfiguration: appGWenableWafFirewall ? appGwFirewallConfigOwasp : json('null') + gatewayIPConfigurations: [ + { + name: 'besubnet' + properties: { + subnet: { + id: appGwSubnetId + } } } - } - ] - frontendIPConfigurations: empty(privateIpApplicationGateway) ? array(frontendPublicIpConfig) : concat(array(frontendPublicIpConfig), array(frontendPrivateIpConfig)) - frontendPorts: [ - { - name: 'appGatewayFrontendPort' - properties: { - port: 80 + ] + frontendIPConfigurations: empty(privateIpApplicationGateway) ? array(frontendPublicIpConfig) : concat(array(frontendPublicIpConfig), array(frontendPrivateIpConfig)) + frontendPorts: [ + { + name: 'appGatewayFrontendPort' + properties: { + port: 80 + } } - } - ] - backendAddressPools: [ - { - name: 'defaultaddresspool' - } - ] - backendHttpSettingsCollection: [ - { - name: 'defaulthttpsetting' - properties: { - port: 80 - protocol: 'Http' - cookieBasedAffinity: 'Disabled' - requestTimeout: 30 - pickHostNameFromBackendAddress: true + ] + backendAddressPools: [ + { + name: 'defaultaddresspool' } - } - ] - httpListeners: [ - { - name: 'hlisten' - properties: { - frontendIPConfiguration: { - id: empty(privateIpApplicationGateway) ? '${appgwResourceId}/frontendIPConfigurations/appGatewayFrontendIP' : '${appgwResourceId}/frontendIPConfigurations/appGatewayPrivateIP' - } - frontendPort: { - id: '${appgwResourceId}/frontendPorts/appGatewayFrontendPort' + ] + backendHttpSettingsCollection: [ + { + name: 'defaulthttpsetting' + properties: { + port: 80 + protocol: 'Http' + cookieBasedAffinity: 'Disabled' + requestTimeout: 30 + pickHostNameFromBackendAddress: true } - protocol: 'Http' } - } - ] - requestRoutingRules: [ - { - name: 'appGwRoutingRuleName' - properties: { - ruleType: 'Basic' - httpListener: { - id: '${appgwResourceId}/httpListeners/hlisten' - } - backendAddressPool: { - id: '${appgwResourceId}/backendAddressPools/defaultaddresspool' + ] + httpListeners: [ + { + name: 'hlisten' + properties: { + frontendIPConfiguration: { + id: empty(privateIpApplicationGateway) ? '${appgwResourceId}/frontendIPConfigurations/appGatewayFrontendIP' : '${appgwResourceId}/frontendIPConfigurations/appGatewayPrivateIP' + } + frontendPort: { + id: '${appgwResourceId}/frontendPorts/appGatewayFrontendPort' + } + protocol: 'Http' } - backendHttpSettings: { - id: '${appgwResourceId}/backendHttpSettingsCollection/defaulthttpsetting' + } + ] + requestRoutingRules: [ + { + name: 'appGwRoutingRuleName' + properties: { + ruleType: 'Basic' + httpListener: { + id: '${appgwResourceId}/httpListeners/hlisten' + } + backendAddressPool: { + id: '${appgwResourceId}/backendAddressPools/defaultaddresspool' + } + backendHttpSettings: { + id: '${appgwResourceId}/backendHttpSettingsCollection/defaulthttpsetting' + } } } + ] + }, appGWmaxCount > 0 ? { + autoscaleConfiguration: { + minCapacity: appGWcount + maxCapacity: appGWmaxCount } - ] -}, appGWmaxCount > 0 ? { - autoscaleConfiguration: { - minCapacity: appGWcount - maxCapacity: appGWmaxCount - } -} : {}) + } : {}) // 'identity' is always set until this is fixed: https://github.com/Azure/bicep/issues/387#issuecomment-885671296 resource appgw 'Microsoft.Network/applicationGateways@2021-02-01' = if (deployAppGw) { @@ -786,7 +776,7 @@ resource appGwAGICRGReader 'Microsoft.Authorization/roleAssignments@2022-04-01' // AGIC's identity requires "Managed Identity Operator" permission over the user assigned identity of Application Gateway. var managedIdentityOperator = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f1a07417-d97a-45cb-824c-7a7467783830') -resource appGwAGICMIOp 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (ingressApplicationGateway && deployAppGw) { +resource appGwAGICMIOp 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (ingressApplicationGateway && deployAppGw) { scope: appGwIdentity name: guid(aks.id, 'Agic', managedIdentityOperator) properties: { @@ -1072,7 +1062,7 @@ param warIngressNginx bool = false @description('System Pool presets are derived from the recommended system pool specs') var systemPoolPresets = { - CostOptimised : { + CostOptimised: { vmSize: 'Standard_B4ms' count: 1 minCount: 1 @@ -1080,7 +1070,7 @@ var systemPoolPresets = { enableAutoScaling: true availabilityZones: [] } - Standard : { + Standard: { vmSize: 'Standard_DS2_v2' count: 3 minCount: 3 @@ -1092,7 +1082,7 @@ var systemPoolPresets = { '3' ] } - HighSpec : { + HighSpec: { vmSize: 'Standard_D4s_v3' count: 3 minCount: 3 @@ -1107,7 +1097,7 @@ var systemPoolPresets = { } var systemPoolBase = { - name: JustUseSystemPool ? nodePoolName : 'npsystem' + name: JustUseSystemPool ? nodePoolName : 'npsystem' vmSize: agentVMSize count: agentCount mode: 'System' @@ -1123,8 +1113,7 @@ var systemPoolBase = { ] } -var agentPoolProfiles = JustUseSystemPool ? array(systemPoolBase) : concat(array(union(systemPoolBase, SystemPoolType=='Custom' && SystemPoolCustomPreset != {} ? SystemPoolCustomPreset : systemPoolPresets[SystemPoolType]))) - +var agentPoolProfiles = JustUseSystemPool ? array(systemPoolBase) : concat(array(union(systemPoolBase, SystemPoolType == 'Custom' && SystemPoolCustomPreset != {} ? SystemPoolCustomPreset : systemPoolPresets[SystemPoolType]))) output userNodePoolName string = nodePoolName output systemNodePoolName string = JustUseSystemPool ? nodePoolName : 'npsystem' @@ -1132,48 +1121,48 @@ output systemNodePoolName string = JustUseSystemPool ? nodePoolName : 'npsystem' var akssku = AksPaidSkuForSLA ? 'Paid' : 'Free' var aks_addons = union({ - azurepolicy: { - config: { - version: !empty(azurepolicy) ? 'v2' : json('null') + azurepolicy: { + config: { + version: !empty(azurepolicy) ? 'v2' : json('null') + } + enabled: !empty(azurepolicy) } - enabled: !empty(azurepolicy) - } - azureKeyvaultSecretsProvider: { - config: { - enableSecretRotation: 'true' - rotationPollInterval: keyVaultAksCSIPollInterval + azureKeyvaultSecretsProvider: { + config: { + enableSecretRotation: 'true' + rotationPollInterval: keyVaultAksCSIPollInterval + } + enabled: keyVaultAksCSI } - enabled: keyVaultAksCSI - } - openServiceMesh: { - enabled: openServiceMeshAddon - config: {} - } -}, createLaw && omsagent ? { - omsagent: { - enabled: createLaw && omsagent - config: { - logAnalyticsWorkspaceResourceID: createLaw && omsagent ? aks_law.id : json('null') + openServiceMesh: { + enabled: openServiceMeshAddon + config: {} } - }} : {}) + }, createLaw && omsagent ? { + omsagent: { + enabled: createLaw && omsagent + config: { + logAnalyticsWorkspaceResourceID: createLaw && omsagent ? aks_law.id : json('null') + } + } } : {}) var aks_addons1 = ingressApplicationGateway ? union(aks_addons, deployAppGw ? { - ingressApplicationGateway: { - config: { - applicationGatewayId: appgw.id + ingressApplicationGateway: { + config: { + applicationGatewayId: appgw.id + } + enabled: true } - enabled: true - } -} : { - // AKS RP will deploy the AppGateway for us (not creating custom or BYO VNET) - ingressApplicationGateway: { - enabled: true - config: { - applicationGatewayName: appgwName - subnetCIDR: '10.225.0.0/16' + } : { + // AKS RP will deploy the AppGateway for us (not creating custom or BYO VNET) + ingressApplicationGateway: { + enabled: true + config: { + applicationGatewayName: appgwName + subnetCIDR: '10.225.0.0/16' + } } - } -}) : aks_addons + }) : aks_addons var aks_identity = { type: 'UserAssigned' @@ -1183,12 +1172,12 @@ var aks_identity = { } @description('Sets the private dns zone id if provided') -var aksPrivateDnsZone = privateClusterDnsMethod=='privateDnsZone' ? (!empty(dnsApiPrivateZoneId) ? dnsApiPrivateZoneId : 'system') : privateClusterDnsMethod +var aksPrivateDnsZone = privateClusterDnsMethod == 'privateDnsZone' ? (!empty(dnsApiPrivateZoneId) ? dnsApiPrivateZoneId : 'system') : privateClusterDnsMethod output aksPrivateDnsZone string = aksPrivateDnsZone @description('Needing to seperately declare and union this because of https://github.com/Azure/AKS-Construction/issues/344') -var managedNATGatewayProfile = { - natGatewayProfile : { +var managedNATGatewayProfile = { + natGatewayProfile: { managedOutboundIPProfile: { count: natGwIpCount } @@ -1198,7 +1187,7 @@ var managedNATGatewayProfile = { @description('Needing to seperately declare and union this because of https://github.com/Azure/AKS/issues/2774') var azureDefenderSecurityProfile = { - securityProfile : { + securityProfile: { defender: { logAnalyticsWorkspaceResourceId: createLaw ? aks_law.id : null securityMonitoring: { @@ -1209,72 +1198,72 @@ var azureDefenderSecurityProfile = { } var aksProperties = union({ - kubernetesVersion: kubernetesVersion - enableRBAC: true - dnsPrefix: dnsPrefix - aadProfile: enable_aad ? { - managed: true - enableAzureRBAC: enableAzureRBAC - tenantID: aad_tenant_id - } : null - apiServerAccessProfile: !empty(authorizedIPRanges) ? { - authorizedIPRanges: authorizedIPRanges - } : { - enablePrivateCluster: enablePrivateCluster - privateDNSZone: enablePrivateCluster ? aksPrivateDnsZone : '' - enablePrivateClusterPublicFQDN: enablePrivateCluster && privateClusterDnsMethod=='none' - } - agentPoolProfiles: agentPoolProfiles - workloadAutoScalerProfile: { - keda: { + kubernetesVersion: kubernetesVersion + enableRBAC: true + dnsPrefix: dnsPrefix + aadProfile: enable_aad ? { + managed: true + enableAzureRBAC: enableAzureRBAC + tenantID: aad_tenant_id + } : null + apiServerAccessProfile: !empty(authorizedIPRanges) ? { + authorizedIPRanges: authorizedIPRanges + } : { + enablePrivateCluster: enablePrivateCluster + privateDNSZone: enablePrivateCluster ? aksPrivateDnsZone : '' + enablePrivateClusterPublicFQDN: enablePrivateCluster && privateClusterDnsMethod == 'none' + } + agentPoolProfiles: agentPoolProfiles + workloadAutoScalerProfile: { + keda: { enabled: kedaAddon + } } - } - networkProfile: { - loadBalancerSku: 'standard' - networkPlugin: networkPlugin - #disable-next-line BCP036 //Disabling validation of this parameter to cope with empty string to indicate no Network Policy required. - networkPolicy: networkPolicy - networkPluginMode: networkPlugin=='azure' ? networkPluginMode : '' - podCidr: networkPlugin=='kubenet' || cniDynamicIpAllocation ? podCidr : json('null') - serviceCidr: serviceCidr - dnsServiceIP: dnsServiceIP - dockerBridgeCidr: dockerBridgeCidr - outboundType: aksOutboundTrafficType - ebpfDataplane: networkPlugin=='azure' ? ebpfDataplane : '' - } - disableLocalAccounts: AksDisableLocalAccounts && enable_aad - autoUpgradeProfile: {upgradeChannel: upgradeChannel} - addonProfiles: !empty(aks_addons1) ? aks_addons1 : aks_addons - autoScalerProfile: autoScale ? AutoscaleProfile : {} - oidcIssuerProfile: { - enabled: oidcIssuer - } - securityProfile: { - workloadIdentity: { - enabled: workloadIdentity + networkProfile: { + loadBalancerSku: 'standard' + networkPlugin: networkPlugin + #disable-next-line BCP036 //Disabling validation of this parameter to cope with empty string to indicate no Network Policy required. + networkPolicy: networkPolicy + networkPluginMode: networkPlugin == 'azure' ? networkPluginMode : '' + podCidr: networkPlugin == 'kubenet' || cniDynamicIpAllocation ? podCidr : json('null') + serviceCidr: serviceCidr + dnsServiceIP: dnsServiceIP + dockerBridgeCidr: dockerBridgeCidr + outboundType: aksOutboundTrafficType + ebpfDataplane: networkPlugin == 'azure' ? ebpfDataplane : '' } - } - ingressProfile: { - webAppRouting: { - enabled: warIngressNginx + disableLocalAccounts: AksDisableLocalAccounts && enable_aad + autoUpgradeProfile: { upgradeChannel: upgradeChannel } + addonProfiles: !empty(aks_addons1) ? aks_addons1 : aks_addons + autoScalerProfile: autoScale ? AutoscaleProfile : {} + oidcIssuerProfile: { + enabled: oidcIssuer } - } - storageProfile: { - blobCSIDriver: { - enabled: blobCSIDriver + securityProfile: { + workloadIdentity: { + enabled: workloadIdentity + } } - diskCSIDriver: { - enabled: diskCSIDriver + ingressProfile: { + webAppRouting: { + enabled: warIngressNginx + } } - fileCSIDriver: { - enabled: fileCSIDriver + storageProfile: { + blobCSIDriver: { + enabled: blobCSIDriver + } + diskCSIDriver: { + enabled: diskCSIDriver + } + fileCSIDriver: { + enabled: fileCSIDriver + } } - } -}, -aksOutboundTrafficType == 'managedNATGateway' ? managedNATGatewayProfile : {}, -defenderForContainers && createLaw ? azureDefenderSecurityProfile : {}, -keyVaultKmsCreateAndPrereqs || !empty(keyVaultKmsByoKeyId) ? azureKeyVaultKms : {} + }, + aksOutboundTrafficType == 'managedNATGateway' ? managedNATGatewayProfile : {}, + defenderForContainers && createLaw ? azureDefenderSecurityProfile : {}, + keyVaultKmsCreateAndPrereqs || !empty(keyVaultKmsByoKeyId) ? azureKeyVaultKms : {} ) resource aks 'Microsoft.ContainerService/managedClusters@2022-10-02-preview' = { @@ -1296,17 +1285,24 @@ resource aks 'Microsoft.ContainerService/managedClusters@2022-10-02-preview' = { output aksClusterName string = aks.name output aksOidcIssuerUrl string = oidcIssuer ? aks.properties.oidcIssuerProfile.issuerURL : '' -@allowed(['Linux','Windows']) +@description('The AKS cluster identity') +output aksClusterIdentity object = { + clientId: aks.properties.identityProfile.kubeletidentity.clientId + objectId: aks.properties.identityProfile.kubeletidentity.objectId + resourceId: aks.properties.identityProfile.kubeletidentity.resourceId +} + +@allowed([ 'Linux', 'Windows' ]) @description('The User Node pool OS') param osType string = 'Linux' -@allowed(['Ubuntu','Windows2019','Windows2022']) +@allowed([ 'Ubuntu', 'Windows2019', 'Windows2022' ]) @description('The User Node pool OS SKU') param osSKU string = 'Ubuntu' var poolName = osType == 'Linux' ? nodePoolName : take(nodePoolName, 6) -module userNodePool 'aksagentpool.bicep' = if (!JustUseSystemPool){ +module userNodePool 'aksagentpool.bicep' = if (!JustUseSystemPool) { name: 'userNodePool' params: { AksName: aks.name @@ -1327,7 +1323,7 @@ module userNodePool 'aksagentpool.bicep' = if (!JustUseSystemPool){ @description('This output can be directly leveraged when creating a ManagedId Federated Identity') output aksOidcFedIdentityProperties object = { issuer: oidcIssuer ? aks.properties.oidcIssuerProfile.issuerURL : '' - audiences: ['api://AzureADTokenExchange'] + audiences: [ 'api://AzureADTokenExchange' ] subject: 'system:serviceaccount:ns:svcaccount' } @@ -1361,10 +1357,10 @@ resource aks_policies 'Microsoft.Authorization/policyAssignments@2020-09-01' = i parameters: { excludedNamespaces: { value: [ - 'kube-system' - 'gatekeeper-system' - 'azure-arc' - 'cluster-baseline-setting' + 'kube-system' + 'gatekeeper-system' + 'azure-arc' + 'cluster-baseline-setting' ] } effect: { @@ -1398,7 +1394,7 @@ resource aks_admin_role_assignment 'Microsoft.Authorization/roleAssignments@2022 param fluxGitOpsAddon bool = false -resource fluxAddon 'Microsoft.KubernetesConfiguration/extensions@2022-04-02-preview' = if(fluxGitOpsAddon) { +resource fluxAddon 'Microsoft.KubernetesConfiguration/extensions@2022-04-02-preview' = if (fluxGitOpsAddon) { name: 'flux' scope: aks properties: { @@ -1412,7 +1408,7 @@ resource fluxAddon 'Microsoft.KubernetesConfiguration/extensions@2022-04-02-prev } configurationProtectedSettings: {} } - dependsOn: [daprExtension] //Chaining dependencies because of: https://github.com/Azure/AKS-Construction/issues/385 + dependsOn: [ daprExtension ] //Chaining dependencies because of: https://github.com/Azure/AKS-Construction/issues/385 } output fluxReleaseNamespace string = fluxGitOpsAddon ? fluxAddon.properties.scope.cluster.releaseNamespace : '' @@ -1421,23 +1417,23 @@ param daprAddon bool = false @description('Enable high availability (HA) mode for the Dapr control plane') param daprAddonHA bool = false -resource daprExtension 'Microsoft.KubernetesConfiguration/extensions@2022-04-02-preview' = if(daprAddon) { - name: 'dapr' - scope: aks - properties: { - extensionType: 'Microsoft.Dapr' - autoUpgradeMinorVersion: true - releaseTrain: 'Stable' - configurationSettings: { - 'global.ha.enabled': '${daprAddonHA}' - } - scope: { - cluster: { - releaseNamespace: 'dapr-system' - } - } - configurationProtectedSettings: {} +resource daprExtension 'Microsoft.KubernetesConfiguration/extensions@2022-04-02-preview' = if (daprAddon) { + name: 'dapr' + scope: aks + properties: { + extensionType: 'Microsoft.Dapr' + autoUpgradeMinorVersion: true + releaseTrain: 'Stable' + configurationSettings: { + 'global.ha.enabled': '${daprAddonHA}' + } + scope: { + cluster: { + releaseNamespace: 'dapr-system' + } } + configurationProtectedSettings: {} + } } output daprReleaseNamespace string = daprAddon ? daprExtension.properties.scope.cluster.releaseNamespace : '' @@ -1449,7 +1445,6 @@ output daprReleaseNamespace string = daprAddon ? daprExtension.properties.scope. | | | | | `--' | | |\ | | | | | | `--' | | |\ \----.| | | |\ | | |__| | |__| |__| \______/ |__| \__| |__| |__| \______/ | _| `._____||__| |__| \__| \______| */ - @description('Diagnostic categories to log') param AksDiagCategories array = [ 'cluster-autoscaler' @@ -1458,7 +1453,7 @@ param AksDiagCategories array = [ 'guard' ] -resource AksDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (createLaw && omsagent) { +resource AksDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (createLaw && omsagent) { name: 'aksDiags' scope: aks properties: { @@ -1527,17 +1522,16 @@ var createLaw = (omsagent || deployAppGw || azureFirewalls || CreateNetworkSecur resource aks_law 'Microsoft.OperationalInsights/workspaces@2022-10-01' = if (createLaw) { name: aks_law_name location: location - properties : union({ + properties: union({ retentionInDays: retentionInDays }, - logDataCap>0 ? { workspaceCapping: { - dailyQuotaGb: logDataCap - }} : {} + logDataCap > 0 ? { workspaceCapping: { + dailyQuotaGb: logDataCap + } } : {} ) } - -resource containerLogsV2_Basiclogs 'Microsoft.OperationalInsights/workspaces/tables@2022-10-01' = if(containerLogsV2BasicLogs){ +resource containerLogsV2_Basiclogs 'Microsoft.OperationalInsights/workspaces/tables@2022-10-01' = if (containerLogsV2BasicLogs) { name: '${aks_law_name}/ContainerLogV2' properties: { plan: 'Basic' @@ -1569,7 +1563,7 @@ output LogAnalyticsId string = (createLaw) ? aks_law.id : '' @description('Create an Event Grid System Topic for AKS events') param createEventGrid bool = false -resource eventGrid 'Microsoft.EventGrid/systemTopics@2021-12-01' = if(createEventGrid) { +resource eventGrid 'Microsoft.EventGrid/systemTopics@2021-12-01' = if (createEventGrid) { name: 'evgt-${aks.name}' location: location identity: { @@ -1587,7 +1581,7 @@ resource eventGridDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-previe name: 'eventGridDiags' scope: eventGrid properties: { - workspaceId:aks_law.id + workspaceId: aks_law.id logs: [ { category: 'DeliveryFailures' diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep index 76e123b57eb..c31d4832249 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep @@ -49,6 +49,16 @@ module cluster '../../../../../../common/infra/bicep/core/host/aks/main.bicep' = } } +// Give the AKS Cluster access to KeyVault +module clusterKeyVaultAccess '../../../../../../common/infra/bicep/core/security/keyvault-access.bicep' = { + name: 'cluster-keyvault-access' + scope: rg + params: { + keyVaultName: keyVault.outputs.name + principalId: cluster.outputs.aksClusterIdentity.objectId + } +} + // The application database module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { name: 'cosmos' @@ -98,6 +108,7 @@ output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId output AZURE_AKS_CLUSTER_NAME string = cluster.outputs.aksClusterName +output AZURE_AKS_IDENTITY_CLIENT_ID string = cluster.outputs.aksClusterIdentity.clientId output AZURE_CONTAINER_REGISTRY_ENDPOINT string = '${cluster.outputs.containerRegistryName}.azurecr.io' output AZURE_CONTAINER_REGISTRY_NAME string = cluster.outputs.containerRegistryName output REACT_APP_API_BASE_URL string = '' diff --git a/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml index 7a19c28a9ce..76332addab4 100644 --- a/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml +++ b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml @@ -19,7 +19,7 @@ spec: - containerPort: 3100 env: - name: AZURE_CLIENT_ID - value: ${AZURE_PRINCIPAL_ID} + value: ${AZURE_AKS_IDENTITY_CLIENT_ID} - name: AZURE_KEY_VAULT_ENDPOINT valueFrom: secretKeyRef: From 01795f53ec06d0b1b6bbad1f3ebf05ef7f051195 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 17 Feb 2023 11:50:56 -0800 Subject: [PATCH 05/25] Updates aks template readme --- .../todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml | 6 +++--- templates/todo/projects/nodejs-mongo-aks/README.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml index 09ec837ab8d..4ab49f0c44f 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml @@ -70,17 +70,17 @@ repo: patterns: - "**/main.bicep" - - from: "PLACEHOLDERIACTOOLS" + - from: "$PLACEHOLDERIACTOOLS" to: "" patterns: - "README.md" - - from: "PLACEHOLDER_TITLE" + - from: "$PLACEHOLDER_TITLE" to: "ToDo Application with a Node.js API and Azure Cosmos DB API for MongoDB on Azure App Service" patterns: - "README.md" - - from: "PLACEHOLDER_DESCRIPTION" + - from: "$PLACEHOLDER_DESCRIPTION" to: "using Bicep as the IaC provider" patterns: - "README.md" diff --git a/templates/todo/projects/nodejs-mongo-aks/README.md b/templates/todo/projects/nodejs-mongo-aks/README.md index 59112b9636b..2e52b81b3de 100644 --- a/templates/todo/projects/nodejs-mongo-aks/README.md +++ b/templates/todo/projects/nodejs-mongo-aks/README.md @@ -1,4 +1,4 @@ -# ToDo Application with a Node.js API and Azure Cosmos DB API for MongoDB on Azure App Service +# ToDo Application with a Node.js API and Azure Cosmos DB API for MongoDB on Azure Kubernetes Service (AKS) [![Open in Remote - Containers](https://img.shields.io/static/v1?label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/todo-nodejs-mongo) @@ -26,7 +26,7 @@ The following prerequisites are required to use this application. Please ensure - [Azure CLI (2.38.0+)](https://docs.microsoft.com/cli/azure/install-azure-cli) - [Node.js with npm (16.13.1+)](https://nodejs.org/) - for API backend and Web frontend - [Git (2.36.1+)](https://git-scm.com/) -PLACEHOLDERIACTOOLS +$PLACEHOLDERIACTOOLS ### Quickstart @@ -36,7 +36,7 @@ The fastest way for you to get this application up and running on Azure is to us 1. Run the following command to initialize the project, provision Azure resources, and deploy the application code. ```bash -azd up --template todo-nodejs-mongo +azd up --template todo-nodejs-mongo-aks ``` You will be prompted for the following information: @@ -67,7 +67,7 @@ Click the web application URL to launch the ToDo app. Create a new collection an This application utilizes the following Azure resources: -- [**Azure App Services**](https://docs.microsoft.com/azure/app-service/) to host the Web frontend and API backend +- [**Azure Kubernetes Service (AKS)**](https://docs.microsoft.com/azure/aks) to host the Web frontend and API backend - [**Azure Cosmos DB API for MongoDB**](https://docs.microsoft.com/azure/cosmos-db/mongodb/mongodb-introduction) for storage - [**Azure Monitor**](https://docs.microsoft.com/azure/azure-monitor/) for monitoring and logging - [**Azure Key Vault**](https://docs.microsoft.com/azure/key-vault/) for securing secrets From e21053dac6fba66bd85a0c7fe0936c78bd76cf3a Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 17 Feb 2023 12:35:29 -0800 Subject: [PATCH 06/25] Fixes linting issues --- .vscode/cspell.global.yaml | 5 ++ cli/azd/cmd/container.go | 4 +- cli/azd/pkg/account/manager_test.go | 6 +- cli/azd/pkg/azure/resource_ids.go | 6 +- cli/azd/pkg/environment/environment.go | 2 +- cli/azd/pkg/project/service_config.go | 10 ++- cli/azd/pkg/project/service_target_aks.go | 61 +++++++++++++------ .../project/service_target_containerapp.go | 2 +- cli/azd/pkg/tools/azcli/container_service.go | 18 +++++- cli/azd/pkg/tools/kubectl/kubectl.go | 29 +++++++-- cli/azd/pkg/tools/kubectl/models.go | 2 +- 11 files changed, 112 insertions(+), 33 deletions(-) diff --git a/.vscode/cspell.global.yaml b/.vscode/cspell.global.yaml index 4ede35babbb..52ae4526794 100644 --- a/.vscode/cspell.global.yaml +++ b/.vscode/cspell.global.yaml @@ -24,6 +24,7 @@ ignoreWords: - armauthorization - armappcontainers - armappservice + - armcontainerservice - armkeyvault - armresources - armruntime @@ -48,6 +49,7 @@ ignoreWords: - configlist - conjunction - containerregistry + - containerservice - databricks - dedb - devcontainer @@ -73,6 +75,9 @@ ignoreWords: - JOBOBJECT - kubernetes - kusto + - kubeconfig + - kubeconfigs + - kustomize - magefile - mainfic - menuid diff --git a/cli/azd/cmd/container.go b/cli/azd/cmd/container.go index 4954154b5e3..8711d7cbe8b 100644 --- a/cli/azd/cmd/container.go +++ b/cli/azd/cmd/container.go @@ -132,7 +132,9 @@ func registerCommonDependencies(container *ioc.NestedContainer) { credProvider auth.MultiTenantCredentialProvider) (azcore.TokenCredential, error) { if env == nil { //nolint:lll - panic("command asked for azcore.TokenCredential, but prerequisite dependency environment.Environment was not registered.") + panic( + "command asked for azcore.TokenCredential, but prerequisite dependency environment.Environment was not registered.", + ) } subscriptionId := env.GetSubscriptionId() diff --git a/cli/azd/pkg/account/manager_test.go b/cli/azd/pkg/account/manager_test.go index bdb7ec1117d..cc240e992e4 100644 --- a/cli/azd/pkg/account/manager_test.go +++ b/cli/azd/pkg/account/manager_test.go @@ -111,7 +111,11 @@ func Test_GetAccountDefaults(t *testing.T) { require.NoError(t, err) accountDefaults, err := manager.GetAccountDefaults(context.Background()) - require.Equal(t, &Account{DefaultSubscription: (*Subscription)(nil), DefaultLocation: (&defaultLocation)}, accountDefaults) + require.Equal( + t, + &Account{DefaultSubscription: (*Subscription)(nil), DefaultLocation: (&defaultLocation)}, + accountDefaults, + ) require.NoError(t, err) }) diff --git a/cli/azd/pkg/azure/resource_ids.go b/cli/azd/pkg/azure/resource_ids.go index a02b221373e..079ed67dc95 100644 --- a/cli/azd/pkg/azure/resource_ids.go +++ b/cli/azd/pkg/azure/resource_ids.go @@ -52,7 +52,11 @@ func WebsiteRID(subscriptionId, resourceGroupName, websiteName string) string { } func AksRID(subscriptionId, resourceGroupName, clusterName string) string { - returnValue := fmt.Sprintf("%s/providers/Microsoft.ContainerService/managedClusters/%s", ResourceGroupRID(subscriptionId, resourceGroupName), clusterName) + returnValue := fmt.Sprintf( + "%s/providers/Microsoft.ContainerService/managedClusters/%s", + ResourceGroupRID(subscriptionId, resourceGroupName), + clusterName, + ) return returnValue } diff --git a/cli/azd/pkg/environment/environment.go b/cli/azd/pkg/environment/environment.go index 16a013d2fe7..994d4d2222b 100644 --- a/cli/azd/pkg/environment/environment.go +++ b/cli/azd/pkg/environment/environment.go @@ -36,7 +36,7 @@ const TenantIdEnvVarName = "AZURE_TENANT_ID" // to. const ContainerRegistryEndpointEnvVarName = "AZURE_CONTAINER_REGISTRY_ENDPOINT" -// ContainerRegistryEndpointEnvVarName is the name of they key used to store the endpoint of the container registry to push to. +// AksClusterEnvVarName is the name of they key used to store the endpoint of the AKS cluster to push to. const AksClusterEnvVarName = "AZURE_AKS_CLUSTER_NAME" // ResourceGroupEnvVarName is the name of the azure resource group that should be used for deployments diff --git a/cli/azd/pkg/project/service_config.go b/cli/azd/pkg/project/service_config.go index 0051567639d..fa1ccc568af 100644 --- a/cli/azd/pkg/project/service_config.go +++ b/cli/azd/pkg/project/service_config.go @@ -130,7 +130,15 @@ func (sc *ServiceConfig) GetServiceTarget( if err != nil { return nil, err } - target = NewAksTarget(sc, env, resource, azCli, containerService, kubectl.NewKubectl(commandRunner), docker.NewDocker(commandRunner)) + target = NewAksTarget( + sc, + env, + resource, + azCli, + containerService, + kubectl.NewKubectl(commandRunner), + docker.NewDocker(commandRunner), + ) default: return nil, fmt.Errorf("unsupported host '%s' for service '%s'", sc.Host, sc.Name) } diff --git a/cli/azd/pkg/project/service_target_aks.go b/cli/azd/pkg/project/service_target_aks.go index f574103be39..d8bfa43a56d 100644 --- a/cli/azd/pkg/project/service_target_aks.go +++ b/cli/azd/pkg/project/service_target_aks.go @@ -30,12 +30,20 @@ func (t *aksTarget) RequiredExternalTools() []tools.ExternalTool { return []tools.ExternalTool{t.docker} } -func (t *aksTarget) Deploy(ctx context.Context, azdCtx *azdcontext.AzdContext, path string, progress chan<- string) (ServiceDeploymentResult, error) { +func (t *aksTarget) Deploy( + ctx context.Context, + azdCtx *azdcontext.AzdContext, + path string, + progress chan<- string, +) (ServiceDeploymentResult, error) { // Login to AKS cluster namespace := t.config.Project.Name clusterName, has := t.env.Values[environment.AksClusterEnvVarName] if !has { - return ServiceDeploymentResult{}, fmt.Errorf("could not determine AKS cluster, ensure %s is set as an output of your infrastructure", environment.AksClusterEnvVarName) + return ServiceDeploymentResult{}, fmt.Errorf( + "could not determine AKS cluster, ensure %s is set as an output of your infrastructure", + environment.AksClusterEnvVarName, + ) } log.Printf("getting AKS credentials %s\n", clusterName) @@ -74,12 +82,16 @@ func (t *aksTarget) Deploy(ctx context.Context, azdCtx *azdcontext.AzdContext, p } progress <- "Creating k8s namespace" - namespaceResult, err := t.kubectl.CreateNamespace(ctx, namespace, &kubectl.KubeCliFlags{DryRun: "client", Output: "yaml"}) + namespaceResult, err := t.kubectl.CreateNamespace( + ctx, + namespace, + &kubectl.KubeCliFlags{DryRun: "client", Output: "yaml"}, + ) if err != nil { return ServiceDeploymentResult{}, fmt.Errorf("failed creating kube namespace: %w", err) } - _, err = t.kubectl.ApplyPipe(ctx, *&namespaceResult.Stdout, nil) + _, err = t.kubectl.ApplyPipe(ctx, namespaceResult.Stdout, nil) if err != nil { return ServiceDeploymentResult{}, fmt.Errorf("failed applying kube namespace: %w", err) } @@ -90,7 +102,7 @@ func (t *aksTarget) Deploy(ctx context.Context, azdCtx *azdcontext.AzdContext, p return ServiceDeploymentResult{}, fmt.Errorf("failed setting kube secrets: %w", err) } - _, err = t.kubectl.ApplyPipe(ctx, *&secretResult.Stdout, nil) + _, err = t.kubectl.ApplyPipe(ctx, secretResult.Stdout, nil) if err != nil { return ServiceDeploymentResult{}, fmt.Errorf("failed applying kube secrets: %w", err) } @@ -98,7 +110,10 @@ func (t *aksTarget) Deploy(ctx context.Context, azdCtx *azdcontext.AzdContext, p // Login to container registry. loginServer, has := t.env.Values[environment.ContainerRegistryEndpointEnvVarName] if !has { - return ServiceDeploymentResult{}, fmt.Errorf("could not determine container registry endpoint, ensure %s is set as an output of your infrastructure", environment.ContainerRegistryEndpointEnvVarName) + return ServiceDeploymentResult{}, fmt.Errorf( + "could not determine container registry endpoint, ensure %s is set as an output of your infrastructure", + environment.ContainerRegistryEndpointEnvVarName, + ) } log.Printf("logging into registry %s\n", loginServer) @@ -135,7 +150,11 @@ func (t *aksTarget) Deploy(ctx context.Context, azdCtx *azdcontext.AzdContext, p progress <- "Applying k8s manifests" t.kubectl.SetEnv(t.env.Values) - err = t.kubectl.ApplyFiles(ctx, filepath.Join(t.config.RelativePath, "manifests"), &kubectl.KubeCliFlags{Namespace: namespace}) + err = t.kubectl.ApplyFiles( + ctx, + filepath.Join(t.config.RelativePath, "manifests"), + &kubectl.KubeCliFlags{Namespace: namespace}, + ) if err != nil { return ServiceDeploymentResult{}, fmt.Errorf("failed applying kube manifests: %w", err) } @@ -146,25 +165,31 @@ func (t *aksTarget) Deploy(ctx context.Context, azdCtx *azdcontext.AzdContext, p } return ServiceDeploymentResult{ - TargetResourceId: azure.ContainerAppRID(t.env.GetSubscriptionId(), t.scope.ResourceGroupName(), t.scope.ResourceName()), - Kind: ContainerAppTarget, - Details: nil, - Endpoints: endpoints, + TargetResourceId: azure.ContainerAppRID( + t.env.GetSubscriptionId(), + t.scope.ResourceGroupName(), + t.scope.ResourceName(), + ), + Kind: ContainerAppTarget, + Details: nil, + Endpoints: endpoints, }, nil } func (t *aksTarget) Endpoints(ctx context.Context) ([]string, error) { // TODO Update return []string{"https://aks.azure.com/sample"}, nil - // containerAppProperties, err := t.cli.GetContainerAppProperties(ctx, t.env.GetSubscriptionId(), t.scope.ResourceGroupName(), t.scope.ResourceName()) - // if err != nil { - // return nil, fmt.Errorf("fetching service properties: %w", err) - // } - - // return []string{fmt.Sprintf("https://%s/", containerAppProperties.Properties.Configuration.Ingress.Fqdn)}, nil } -func NewAksTarget(config *ServiceConfig, env *environment.Environment, scope *environment.TargetResource, azCli azcli.AzCli, containerService azcli.ContainerServiceClient, kubectlCli kubectl.KubectlCli, docker docker.Docker) ServiceTarget { +func NewAksTarget( + config *ServiceConfig, + env *environment.Environment, + scope *environment.TargetResource, + azCli azcli.AzCli, + containerService azcli.ContainerServiceClient, + kubectlCli kubectl.KubectlCli, + docker docker.Docker, +) ServiceTarget { return &aksTarget{ config: config, env: env, diff --git a/cli/azd/pkg/project/service_target_containerapp.go b/cli/azd/pkg/project/service_target_containerapp.go index d985dd8d75b..8be00b8dc98 100644 --- a/cli/azd/pkg/project/service_target_containerapp.go +++ b/cli/azd/pkg/project/service_target_containerapp.go @@ -31,7 +31,7 @@ type containerAppTarget struct { env *environment.Environment resource *environment.TargetResource cli azcli.AzCli - docker docker.Docker + docker docker.Docker console input.Console commandRunner exec.CommandRunner accountManager account.Manager diff --git a/cli/azd/pkg/tools/azcli/container_service.go b/cli/azd/pkg/tools/azcli/container_service.go index acfdefbf82d..1721064499a 100644 --- a/cli/azd/pkg/tools/azcli/container_service.go +++ b/cli/azd/pkg/tools/azcli/container_service.go @@ -9,7 +9,11 @@ import ( ) type ContainerServiceClient interface { - GetAdminCredentials(ctx context.Context, resourceGroupName string, resourceName string) (*armcontainerservice.CredentialResults, error) + GetAdminCredentials( + ctx context.Context, + resourceGroupName string, + resourceName string, + ) (*armcontainerservice.CredentialResults, error) } type containerServiceClient struct { @@ -17,7 +21,11 @@ type containerServiceClient struct { subscriptionId string } -func NewContainerServiceClient(subscriptionId string, credential azcore.TokenCredential, options *arm.ClientOptions) (ContainerServiceClient, error) { +func NewContainerServiceClient( + subscriptionId string, + credential azcore.TokenCredential, + options *arm.ClientOptions, +) (ContainerServiceClient, error) { azureClient, err := armcontainerservice.NewManagedClustersClient(subscriptionId, credential, options) if err != nil { return nil, err @@ -29,7 +37,11 @@ func NewContainerServiceClient(subscriptionId string, credential azcore.TokenCre }, nil } -func (cs *containerServiceClient) GetAdminCredentials(ctx context.Context, resourceGroupName string, resourceName string) (*armcontainerservice.CredentialResults, error) { +func (cs *containerServiceClient) GetAdminCredentials( + ctx context.Context, + resourceGroupName string, + resourceName string, +) (*armcontainerservice.CredentialResults, error) { creds, err := cs.client.ListClusterAdminCredentials(ctx, resourceGroupName, resourceName, nil) if err != nil { return nil, err diff --git a/cli/azd/pkg/tools/kubectl/kubectl.go b/cli/azd/pkg/tools/kubectl/kubectl.go index 49700adee03..6f09c9876fc 100644 --- a/cli/azd/pkg/tools/kubectl/kubectl.go +++ b/cli/azd/pkg/tools/kubectl/kubectl.go @@ -24,7 +24,12 @@ type KubectlCli interface { ConfigView(ctx context.Context, merge bool, flatten bool, flags *KubeCliFlags) (*exec.RunResult, error) ConfigUseContext(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) CreateNamespace(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) - CreateSecretGenericFromLiterals(ctx context.Context, name string, secrets []string, flags *KubeCliFlags) (*exec.RunResult, error) + CreateSecretGenericFromLiterals( + ctx context.Context, + name string, + secrets []string, + flags *KubeCliFlags, + ) (*exec.RunResult, error) } type kubectlCli struct { @@ -63,7 +68,12 @@ func (cli *kubectlCli) ConfigUseContext(ctx context.Context, name string, flags return &res, nil } -func (cli *kubectlCli) ConfigView(ctx context.Context, merge bool, flatten bool, flags *KubeCliFlags) (*exec.RunResult, error) { +func (cli *kubectlCli) ConfigView( + ctx context.Context, + merge bool, + flatten bool, + flags *KubeCliFlags, +) (*exec.RunResult, error) { kubeConfigDir, err := getKubeConfigDir() if err != nil { return nil, err @@ -100,7 +110,7 @@ func (cli *kubectlCli) GetNodes(ctx context.Context, flags *KubeCliFlags) ([]Nod var listResult ListResult if err := json.Unmarshal([]byte(res.Stdout), &listResult); err != nil { - return nil, fmt.Errorf("unmarshaling json: %w", err) + return nil, fmt.Errorf("unmarshalling json: %w", err) } nodes := []Node{} @@ -181,7 +191,12 @@ func (cli *kubectlCli) ApplyKustomize(ctx context.Context, path string, flags *K return &res, nil } -func (cli *kubectlCli) CreateSecretGenericFromLiterals(ctx context.Context, name string, secrets []string, flags *KubeCliFlags) (*exec.RunResult, error) { +func (cli *kubectlCli) CreateSecretGenericFromLiterals( + ctx context.Context, + name string, + secrets []string, + flags *KubeCliFlags, +) (*exec.RunResult, error) { args := []string{"create", "secret", "generic", name} for _, secret := range secrets { args = append(args, fmt.Sprintf("--from-literal=%s", secret)) @@ -220,7 +235,11 @@ func (cli *kubectlCli) executeCommand(ctx context.Context, flags *KubeCliFlags, return cli.executeCommandWithArgs(ctx, runArgs, flags) } -func (cli *kubectlCli) executeCommandWithArgs(ctx context.Context, args exec.RunArgs, flags *KubeCliFlags) (exec.RunResult, error) { +func (cli *kubectlCli) executeCommandWithArgs( + ctx context.Context, + args exec.RunArgs, + flags *KubeCliFlags, +) (exec.RunResult, error) { args = args.WithEnrichError(true) if cli.cwd != "" { args = args.WithCwd(cli.cwd) diff --git a/cli/azd/pkg/tools/kubectl/models.go b/cli/azd/pkg/tools/kubectl/models.go index 7074775f22b..09fb0b969e5 100644 --- a/cli/azd/pkg/tools/kubectl/models.go +++ b/cli/azd/pkg/tools/kubectl/models.go @@ -9,6 +9,6 @@ type Node struct { type ListResult struct { ApiVersion string `json:"apiVersion"` - Kind string `json: "kind"` + Kind string `json:"kind"` Items []map[string]any `json:"items"` } From 80d67fabaf7c2211064994d17e8af210bdb030ac Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 17 Feb 2023 13:05:46 -0800 Subject: [PATCH 07/25] Fixes failing unit tests --- cli/azd/pkg/tools/azcli/azcli.go | 14 --------- cli/azd/pkg/tools/kubectl/kube_config.go | 10 ++++++- cli/azd/pkg/tools/kubectl/kubectl.go | 38 ------------------------ 3 files changed, 9 insertions(+), 53 deletions(-) diff --git a/cli/azd/pkg/tools/azcli/azcli.go b/cli/azd/pkg/tools/azcli/azcli.go index 8548de6c067..0cc8ca4ebf0 100644 --- a/cli/azd/pkg/tools/azcli/azcli.go +++ b/cli/azd/pkg/tools/azcli/azcli.go @@ -348,20 +348,6 @@ func clientOptionsBuilder(httpClient httputil.HttpClient, userAgent string) *azs WithPerCallPolicy(azsdk.NewUserAgentPolicy(userAgent)) } -func (cli *azCli) GetAksCredentials(ctx context.Context, resourceGroupName string, clusterName string) error { - // _, err := cli.runAzCommand(ctx, - // "aks", "get-credentials", - // "--resource-group", resourceGroupName, - // "--name", clusterName, - // ) - - // if err != nil { - // return fmt.Errorf("getting AKS credentials: %w", err) - // } - - return nil -} - func (cli *azCli) ContainerService(ctx context.Context, subscriptionId string) (ContainerServiceClient, error) { options := cli.createDefaultClientOptionsBuilder(ctx).BuildArmClientOptions() return NewContainerServiceClient(subscriptionId, cli.credential, options) diff --git a/cli/azd/pkg/tools/kubectl/kube_config.go b/cli/azd/pkg/tools/kubectl/kube_config.go index a7f096c62f9..04de12dfe38 100644 --- a/cli/azd/pkg/tools/kubectl/kube_config.go +++ b/cli/azd/pkg/tools/kubectl/kube_config.go @@ -81,10 +81,18 @@ func (kcm *KubeConfigManager) SaveKubeConfig(ctx context.Context, configName str return fmt.Errorf("failed marshalling KubeConfig to yaml: %w", err) } + // Create .kube config folder if it doesn't already exist + _, err = os.Stat(kcm.configPath) + if err != nil { + if err := os.MkdirAll(kcm.configPath, osutil.PermissionDirectory); err != nil { + return fmt.Errorf("failed creating .kube config directory, %w", err) + } + } + outFilePath := filepath.Join(kcm.configPath, configName) err = os.WriteFile(outFilePath, kubeConfigRaw, osutil.PermissionFile) if err != nil { - return fmt.Errorf("failed write kube config file: %w", err) + return fmt.Errorf("failed writing kube config file: %w", err) } return nil diff --git a/cli/azd/pkg/tools/kubectl/kubectl.go b/cli/azd/pkg/tools/kubectl/kubectl.go index 6f09c9876fc..6e4dd1f62e0 100644 --- a/cli/azd/pkg/tools/kubectl/kubectl.go +++ b/cli/azd/pkg/tools/kubectl/kubectl.go @@ -2,7 +2,6 @@ package kubectl import ( "context" - "encoding/json" "fmt" "os" "path/filepath" @@ -17,10 +16,8 @@ type KubectlCli interface { tools.ExternalTool Cwd(cwd string) SetEnv(env map[string]string) - GetNodes(ctx context.Context, flags *KubeCliFlags) ([]Node, error) ApplyFiles(ctx context.Context, path string, flags *KubeCliFlags) error ApplyPipe(ctx context.Context, input string, flags *KubeCliFlags) (*exec.RunResult, error) - ApplyKustomize(ctx context.Context, path string, flags *KubeCliFlags) (*exec.RunResult, error) ConfigView(ctx context.Context, merge bool, flatten bool, flags *KubeCliFlags) (*exec.RunResult, error) ConfigUseContext(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) CreateNamespace(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) @@ -99,32 +96,6 @@ func (cli *kubectlCli) ConfigView( return &res, nil } -func (cli *kubectlCli) GetNodes(ctx context.Context, flags *KubeCliFlags) ([]Node, error) { - res, err := cli.executeCommand(ctx, flags, - "get", "nodes", - "-o", "json", - ) - if err != nil { - return nil, fmt.Errorf("kubectl get nodes: %w", err) - } - - var listResult ListResult - if err := json.Unmarshal([]byte(res.Stdout), &listResult); err != nil { - return nil, fmt.Errorf("unmarshalling json: %w", err) - } - - nodes := []Node{} - for _, item := range listResult.Items { - metadata := item["metadata"].(map[string]any) - - nodes = append(nodes, Node{ - Name: metadata["name"].(string), - }) - } - - return nodes, nil -} - func (cli *kubectlCli) ApplyPipe(ctx context.Context, input string, flags *KubeCliFlags) (*exec.RunResult, error) { runArgs := exec. NewRunArgs("kubectl", "apply", "-f", "-"). @@ -182,15 +153,6 @@ func (cli *kubectlCli) ApplyFiles(ctx context.Context, path string, flags *KubeC return nil } -func (cli *kubectlCli) ApplyKustomize(ctx context.Context, path string, flags *KubeCliFlags) (*exec.RunResult, error) { - res, err := cli.executeCommand(ctx, flags, "apply", "-k", path) - if err != nil { - return nil, fmt.Errorf("kubectl apply -k: %w", err) - } - - return &res, nil -} - func (cli *kubectlCli) CreateSecretGenericFromLiterals( ctx context.Context, name string, From 774af746d4ed464ea5e94407c1df936543906c11 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 17 Feb 2023 13:40:37 -0800 Subject: [PATCH 08/25] Install kubectl task --- eng/pipelines/release-cli.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/pipelines/release-cli.yml b/eng/pipelines/release-cli.yml index a8f531adb75..855ec4e5b0a 100644 --- a/eng/pipelines/release-cli.yml +++ b/eng/pipelines/release-cli.yml @@ -96,6 +96,7 @@ stages: Condition: and(succeeded(), ne(variables['Skip.LiveTest'], 'true')) - template: /eng/pipelines/templates/steps/install-terraform.yml + - template: /eng/pipelines/templates/steps/install-kubectl.yml # Pinning DockerInstaller to 0.209.0 because 0.214.0 has failures. # Remove this pin when later versions succeed. From e3d3d8500d7ddf4c60e68a00ac32f368e0ae0bbb Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 17 Feb 2023 13:42:31 -0800 Subject: [PATCH 09/25] Install kubectl task --- eng/pipelines/templates/steps/install-kubectl.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 eng/pipelines/templates/steps/install-kubectl.yml diff --git a/eng/pipelines/templates/steps/install-kubectl.yml b/eng/pipelines/templates/steps/install-kubectl.yml new file mode 100644 index 00000000000..72a08d7ca87 --- /dev/null +++ b/eng/pipelines/templates/steps/install-kubectl.yml @@ -0,0 +1,5 @@ +steps: + - task: KubectlInstaller@0 + displayName: Kubectl installer + inputs: + kubectlVersion: latest \ No newline at end of file From fb0848680e69f929a48846014e8d01e57d38e5e4 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 17 Feb 2023 13:43:33 -0800 Subject: [PATCH 10/25] Install kubectl task --- eng/pipelines/templates/steps/install-kubectl.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/pipelines/templates/steps/install-kubectl.yml b/eng/pipelines/templates/steps/install-kubectl.yml index 72a08d7ca87..18d9021ef1e 100644 --- a/eng/pipelines/templates/steps/install-kubectl.yml +++ b/eng/pipelines/templates/steps/install-kubectl.yml @@ -1,5 +1,5 @@ steps: - task: KubectlInstaller@0 - displayName: Kubectl installer - inputs: - kubectlVersion: latest \ No newline at end of file + displayName: Kubectl installer + inputs: + kubectlVersion: latest From 9c90dbab8daa850d4af6ce0146b3431988f4762f Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 22 Feb 2023 16:06:07 -0800 Subject: [PATCH 11/25] Updates AKS bicep modules --- .../infra/bicep/core/host/agent-pool.bicep | 21 + .../common/infra/bicep/core/host/aks.bicep | 213 +++ .../bicep/core/host/aks/acragentpool.bicep | 19 - .../bicep/core/host/aks/aksagentpool.bicep | 80 - .../bicep/core/host/aks/aksmetricalerts.bicep | 753 -------- .../bicep/core/host/aks/aksnetcontrib.bicep | 44 - .../infra/bicep/core/host/aks/appgw.bicep | 196 -- .../bicep/core/host/aks/bicepconfig.json | 55 - .../bicep/core/host/aks/calcAzFwIp.bicep | 10 - .../infra/bicep/core/host/aks/dnsZone.bicep | 47 - .../bicep/core/host/aks/dnsZoneRbac.bicep | 26 - .../infra/bicep/core/host/aks/firewall.bicep | 324 ---- .../infra/bicep/core/host/aks/keyvault.bicep | 80 - .../bicep/core/host/aks/keyvaultkey.bicep | 24 - .../bicep/core/host/aks/keyvaultrbac.bicep | 173 -- .../infra/bicep/core/host/aks/main.bicep | 1625 ----------------- .../infra/bicep/core/host/aks/network.bicep | 506 ----- .../core/host/aks/networksubnetrbac.bicep | 19 - .../core/host/aks/networkwatcherflowlog.bicep | 46 - .../infra/bicep/core/host/aks/nsg.bicep | 283 --- .../bicep/core/host/container-registry.bicep | 29 + .../bicep/core/host/managed-cluster.bicep | 134 ++ .../bicep/core/security/registry-access.bicep | 18 + 23 files changed, 415 insertions(+), 4310 deletions(-) create mode 100644 templates/common/infra/bicep/core/host/agent-pool.bicep create mode 100644 templates/common/infra/bicep/core/host/aks.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/acragentpool.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/aksagentpool.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/aksmetricalerts.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/aksnetcontrib.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/appgw.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/bicepconfig.json delete mode 100644 templates/common/infra/bicep/core/host/aks/calcAzFwIp.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/dnsZone.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/dnsZoneRbac.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/firewall.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/keyvault.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/keyvaultkey.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/keyvaultrbac.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/main.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/network.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/networksubnetrbac.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/networkwatcherflowlog.bicep delete mode 100644 templates/common/infra/bicep/core/host/aks/nsg.bicep create mode 100644 templates/common/infra/bicep/core/host/managed-cluster.bicep create mode 100644 templates/common/infra/bicep/core/security/registry-access.bicep diff --git a/templates/common/infra/bicep/core/host/agent-pool.bicep b/templates/common/infra/bicep/core/host/agent-pool.bicep new file mode 100644 index 00000000000..24796f0f9ca --- /dev/null +++ b/templates/common/infra/bicep/core/host/agent-pool.bicep @@ -0,0 +1,21 @@ +param clusterName string + +@description('The agent pool name') +param name string + +@description('The agent pool configuration') +param config object + +@description('Custom tags to apply to the AKS resources') +param tags object = {} + +resource aksCluster 'Microsoft.ContainerService/managedClusters@2022-11-02-preview' existing = { + name: clusterName +} + +resource nodePool 'Microsoft.ContainerService/managedClusters/agentPools@2022-11-02-preview' = { + parent: aksCluster + name: name + properties: config + tags: tags +} diff --git a/templates/common/infra/bicep/core/host/aks.bicep b/templates/common/infra/bicep/core/host/aks.bicep new file mode 100644 index 00000000000..9ee00afe5cc --- /dev/null +++ b/templates/common/infra/bicep/core/host/aks.bicep @@ -0,0 +1,213 @@ +@description('The name for the AKS managed cluster') +param name string + +@description('The name for the Azure container registry (ACR)') +param containerRegistryName string + +@description('The name of the connected log analytics workspace') +param logAnalyticsName string = '' + +@description('The name of the keyvault to grant access') +param keyVaultName string + +@description('The Azure region/location for the AKS resources') +param location string = resourceGroup().location + +@description('Custom tags to apply to the AKS resources') +param tags object = {} + +@description('AKS add-ons configuration') +param addOns object = { + azurePolicy: { + enabled: true + config: { + version: 'v2' + } + } + keyVault: { + enabled: true + config: { + enableSecretRotation: 'true' + rotationPollInterval: '2m' + } + } + openServiceMesh: { + enabled: false + config: {} + } + omsAgent: { + enabled: true + config: {} + } + applicationGateway: { + enabled: false + config: {} + } +} + +@allowed([ + 'CostOptimised' + 'Standard' + 'HighSpec' + 'Custom' +]) +@description('The System Pool Preset sizing') +param systemPoolType string = 'CostOptimised' + +@allowed([ + '' + 'CostOptimised' + 'Standard' + 'HighSpec' + 'Custom' +]) +@description('The System Pool Preset sizing') +param agentPoolType string = '' + +// Configure system / user agent pools +@description('Custom configuration of system node pool') +param systemPoolConfig object = {} +@description('Custom configuration of user node pool') +param agentPoolConfig object = {} + +// Configure AKS add-ons +var omsAgentConfig = (!empty(logAnalyticsName) && !empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? union( + addOns.omsAgent, + { + config: { + logAnalyticsWorkspaceResourceID: logAnalytics.id + } + } +) : {} + +var addOnsConfig = union( + (!empty(addOns.azurePolicy) && addOns.azurePolicy.enabled) ? { azurepolicy: addOns.azurePolicy } : {}, + (!empty(addOns.keyVault) && addOns.keyVault.enabled) ? { azureKeyvaultSecretsProvider: addOns.keyVault } : {}, + (!empty(addOns.openServiceMesh) && addOns.openServiceMesh.enabled) ? { openServiceMesh: addOns.openServiceMesh } : {}, + (!empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? { omsagent: omsAgentConfig } : {}, + (!empty(addOns.applicationGateway) && addOns.applicationGateway.enabled) ? { ingressApplicationGateway: addOns.applicationGateway } : {} +) + +// Link to existing log analytics workspace when available +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = if (!empty(logAnalyticsName)) { + name: logAnalyticsName +} + +var systemPoolSpec = !empty(systemPoolConfig) ? systemPoolConfig : nodePoolPresets[systemPoolType] + +// Create the primary AKS cluster resources and system node pool +module managedCluster 'managed-cluster.bicep' = { + name: 'managed-cluster' + params: { + name: name + location: location + tags: tags + systemPoolConfig: union( + { name: 'npsystem', mode: 'System' }, + nodePoolBase, + systemPoolSpec + ) + addOns: addOnsConfig + workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : '' + } +} + +var hasAgentPool = !empty(agentPoolConfig) || !empty(agentPoolType) +var agentPoolSpec = hasAgentPool && !empty(agentPoolConfig) ? agentPoolConfig : nodePoolPresets[agentPoolType] + +// Create additional user agent pool when specified +module agentPool 'agent-pool.bicep' = if (hasAgentPool) { + name: 'aks-node-pool' + params: { + clusterName: managedCluster.outputs.aksClusterName + name: 'npuserpool' + config: union({ name: 'npuser', mode: 'User' }, nodePoolBase, agentPoolSpec) + } +} + +// Creates container registry (ACR) +module containerRegistry 'container-registry.bicep' = { + name: 'container-registry' + params: { + name: containerRegistryName + location: location + tags: tags + workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : '' + } +} + +// Grant ACR Pull access from cluster managed identity to container registry +module containerRegistryAccess '../security/registry-access.bicep' = { + name: 'cluster-container-registry-access' + params: { + containerRegistryName: containerRegistry.outputs.name + principalId: managedCluster.outputs.aksClusterIdentity.objectId + } +} + +// Give the AKS Cluster access to KeyVault +module clusterKeyVaultAccess '../security/keyvault-access.bicep' = { + name: 'cluster-keyvault-access' + params: { + keyVaultName: keyVaultName + principalId: managedCluster.outputs.aksClusterIdentity.objectId + } +} + +// Helpers for node pool configuration +var nodePoolBase = { + osType: 'Linux' + maxPods: 30 + type: 'VirtualMachineScaleSets' + upgradeSettings: { + maxSurge: '33%' + } +} + +var nodePoolPresets = { + CostOptimised: { + vmSize: 'Standard_B4ms' + count: 1 + minCount: 1 + maxCount: 3 + enableAutoScaling: true + availabilityZones: [] + } + Standard: { + vmSize: 'Standard_DS2_v2' + count: 3 + minCount: 3 + maxCount: 5 + enableAutoScaling: true + availabilityZones: [ + '1' + '2' + '3' + ] + } + HighSpec: { + vmSize: 'Standard_D4s_v3' + count: 3 + minCount: 3 + maxCount: 5 + enableAutoScaling: true + availabilityZones: [ + '1' + '2' + '3' + ] + } +} + +// Module outputs +@description('The resource name of the AKS cluster') +output aksClusterName string = managedCluster.outputs.aksClusterName + +@description('The AKS cluster identity') +output aksClusterIdentity object = managedCluster.outputs.aksClusterIdentity + +@description('The resource name of the ACR') +output containerRegistryName string = containerRegistry.outputs.name + +@description('The login server for the container registry') +output containerRegistryLoginServer string = containerRegistry.outputs.loginServer diff --git a/templates/common/infra/bicep/core/host/aks/acragentpool.bicep b/templates/common/infra/bicep/core/host/aks/acragentpool.bicep deleted file mode 100644 index 4bba2c43484..00000000000 --- a/templates/common/infra/bicep/core/host/aks/acragentpool.bicep +++ /dev/null @@ -1,19 +0,0 @@ -param location string = resourceGroup().location -param acrName string -param acrPoolSubnetId string = '' - -resource acr 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' existing = { - name: acrName -} - -resource acrPool 'Microsoft.ContainerRegistry/registries/agentPools@2019-06-01-preview' = { - name: 'private-pool' - location: location - parent: acr - properties: { - count: 1 - os: 'Linux' - tier: 'S1' - virtualNetworkSubnetResourceId: acrPoolSubnetId - } -} diff --git a/templates/common/infra/bicep/core/host/aks/aksagentpool.bicep b/templates/common/infra/bicep/core/host/aks/aksagentpool.bicep deleted file mode 100644 index 100992aaf88..00000000000 --- a/templates/common/infra/bicep/core/host/aks/aksagentpool.bicep +++ /dev/null @@ -1,80 +0,0 @@ -param AksName string - -param PoolName string - -@description('The zones to use for a node pool') -param availabilityZones array = [] - -@description('OS disk type') -param osDiskType string - -@description('VM SKU') -param agentVMSize string - -@description('Disk size in GB') -param osDiskSizeGB int = 0 - -@description('The number of agents for the user node pool') -param agentCount int = 1 - -@description('The maximum number of nodes for the user node pool') -param agentCountMax int = 3 -var autoScale = agentCountMax > agentCount - -@description('The maximum number of pods per node.') -param maxPods int = 30 - -@description('Any taints that should be applied to the node pool') -param nodeTaints array = [] - -@description('Any labels that should be applied to the node pool') -param nodeLabels object = {} - -@description('The subnet the node pool will use') -param subnetId string - -@description('OS Type for the node pool') -@allowed(['Linux','Windows']) -param osType string - -@allowed(['Ubuntu','Windows2019','Windows2022']) -param osSKU string - -@description('Assign a public IP per node') -param enableNodePublicIP bool = false - -@description('Apply a default sku taint to Windows node pools') -param autoTaintWindows bool = false - -var taints = autoTaintWindows ? union(nodeTaints, ['sku=Windows:NoSchedule']) : nodeTaints - -resource aks 'Microsoft.ContainerService/managedClusters@2021-10-01' existing = { - name: AksName -} - -resource userNodepool 'Microsoft.ContainerService/managedClusters/agentPools@2021-10-01' = { - parent: aks - name: PoolName - properties: { - mode: 'User' - vmSize: agentVMSize - count: agentCount - minCount: autoScale ? agentCount : json('null') - maxCount: autoScale ? agentCountMax : json('null') - enableAutoScaling: autoScale - availabilityZones: !empty(availabilityZones) ? availabilityZones : null - osDiskType: osDiskType - osSKU: osSKU - osDiskSizeGB: osDiskSizeGB - osType: osType - maxPods: maxPods - type: 'VirtualMachineScaleSets' - vnetSubnetID: !empty(subnetId) ? subnetId : json('null') - upgradeSettings: { - maxSurge: '33%' - } - nodeTaints: taints - nodeLabels: nodeLabels - enableNodePublicIP: enableNodePublicIP - } -} diff --git a/templates/common/infra/bicep/core/host/aks/aksmetricalerts.bicep b/templates/common/infra/bicep/core/host/aks/aksmetricalerts.bicep deleted file mode 100644 index d1ed20d9a01..00000000000 --- a/templates/common/infra/bicep/core/host/aks/aksmetricalerts.bicep +++ /dev/null @@ -1,753 +0,0 @@ -@description('The name of the AKS Cluster to configure the alerts on') -param clusterName string - -@description('The name of the Log Analytics workspace to log metric data to') -param logAnalyticsWorkspaceName string - -@description('The location of the Log Analytics workspace') -param logAnalyticsWorkspaceLocation string = resourceGroup().location - -@description('Select the frequency on how often the alert rule should be run. Selecting frequency smaller than granularity of datapoints grouping will result in sliding window evaluation') -@allowed([ - 'PT1M' - 'PT15M' -]) -param evalFrequency string = 'PT1M' - -@description('Create the metric alerts as either enabled or disabled') -param metricAlertsEnabled bool = true - -@description('Defines the interval over which datapoints are grouped using the aggregation type function') -@allowed([ - 'PT5M' - 'PT1H' -]) -param windowSize string = 'PT5M' - -@allowed([ - 'Critical' - 'Error' - 'Warning' - 'Informational' - 'Verbose' -]) -param alertSeverity string = 'Informational' - -var alertServerityLookup = { - Critical: 0 - Error: 1 - Warning: 2 - Informational: 3 - Verbose: 4 -} -var alertSeverityNumber = alertServerityLookup[alertSeverity] - -var AksResourceId = resourceId('Microsoft.ContainerService/managedClusters', clusterName) - -resource Node_CPU_utilization_high_for_clusterName_CI_1 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Node CPU utilization high for ${clusterName} CI-1' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'host' - operator: 'Include' - values: [ - '*' - ] - } - ] - metricName: 'cpuUsagePercentage' - metricNamespace: 'Insights.Container/nodes' - name: 'Metric1' - operator: 'GreaterThan' - threshold: 80 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'Node CPU utilization across the cluster.' - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'microsoft.containerservice/managedclusters' - windowSize: windowSize - } -} - -resource Node_working_set_memory_utilization_high_for_clusterName_CI_2 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Node working set memory utilization high for ${clusterName} CI-2' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'host' - operator: 'Include' - values: [ - '*' - ] - } - ] - metricName: 'memoryWorkingSetPercentage' - metricNamespace: 'Insights.Container/nodes' - name: 'Metric1' - operator: 'GreaterThan' - threshold: 80 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'Node working set memory utilization across the cluster.' - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'microsoft.containerservice/managedclusters' - windowSize: windowSize - } -} - -resource Jobs_completed_more_than_6_hours_ago_for_clusterName_CI_11 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Jobs completed more than 6 hours ago for ${clusterName} CI-11' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'controllerName' - operator: 'Include' - values: [ - '*' - ] - } - { - name: 'kubernetes namespace' - operator: 'Include' - values: [ - '*' - ] - } - ] - metricName: 'completedJobsCount' - metricNamespace: 'Insights.Container/pods' - name: 'Metric1' - operator: 'GreaterThan' - threshold: 0 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'This alert monitors completed jobs (more than 6 hours ago).' - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'microsoft.containerservice/managedclusters' - windowSize: windowSize - } -} - -resource Container_CPU_usage_high_for_clusterName_CI_9 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Container CPU usage high for ${clusterName} CI-9' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'controllerName' - operator: 'Include' - values: [ - '*' - ] - } - { - name: 'kubernetes namespace' - operator: 'Include' - values: [ - '*' - ] - } - ] - metricName: 'cpuExceededPercentage' - metricNamespace: 'Insights.Container/containers' - name: 'Metric1' - operator: 'GreaterThan' - threshold: 90 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'This alert monitors container CPU utilization.' - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'microsoft.containerservice/managedclusters' - windowSize: windowSize - } -} - -resource Container_working_set_memory_usage_high_for_clusterName_CI_10 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Container working set memory usage high for ${clusterName} CI-10' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'controllerName' - operator: 'Include' - values: [ - '*' - ] - } - { - name: 'kubernetes namespace' - operator: 'Include' - values: [ - '*' - ] - } - ] - metricName: 'memoryWorkingSetExceededPercentage' - metricNamespace: 'Insights.Container/containers' - name: 'Metric1' - operator: 'GreaterThan' - threshold: 90 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'This alert monitors container working set memory utilization.' - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'microsoft.containerservice/managedclusters' - windowSize: windowSize - } -} - -resource Pods_in_failed_state_for_clusterName_CI_4 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Pods in failed state for ${clusterName} CI-4' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'phase' - operator: 'Include' - values: [ - 'Failed' - ] - } - ] - metricName: 'podCount' - metricNamespace: 'Insights.Container/pods' - name: 'Metric1' - operator: 'GreaterThan' - threshold: 0 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'Pod status monitoring.' - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'microsoft.containerservice/managedclusters' - windowSize: windowSize - } -} - -resource Disk_usage_high_for_clusterName_CI_5 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Disk usage high for ${clusterName} CI-5' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'host' - operator: 'Include' - values: [ - '*' - ] - } - { - name: 'device' - operator: 'Include' - values: [ - '*' - ] - } - ] - metricName: 'DiskUsedPercentage' - metricNamespace: 'Insights.Container/nodes' - name: 'Metric1' - operator: 'GreaterThan' - threshold: 80 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'This alert monitors disk usage for all nodes and storage devices.' - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'microsoft.containerservice/managedclusters' - windowSize: windowSize - } -} - -resource Nodes_in_not_ready_status_for_clusterName_CI_3 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Nodes in not ready status for ${clusterName} CI-3' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'status' - operator: 'Include' - values: [ - 'NotReady' - ] - } - ] - metricName: 'nodesCount' - metricNamespace: 'Insights.Container/nodes' - name: 'Metric1' - operator: 'GreaterThan' - threshold: 0 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'Node status monitoring.' - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'microsoft.containerservice/managedclusters' - windowSize: windowSize - } -} - -resource Containers_getting_OOM_killed_for_clusterName_CI_6 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Containers getting OOM killed for ${clusterName} CI-6' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'kubernetes namespace' - operator: 'Include' - values: [ - '*' - ] - } - { - name: 'controllerName' - operator: 'Include' - values: [ - '*' - ] - } - ] - metricName: 'oomKilledContainerCount' - metricNamespace: 'Insights.Container/pods' - name: 'Metric1' - operator: 'GreaterThan' - threshold: 0 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'This alert monitors number of containers killed due to out of memory (OOM) error.' - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'microsoft.containerservice/managedclusters' - windowSize: windowSize - } -} - -resource Persistent_volume_usage_high_for_clusterName_CI_18 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Persistent volume usage high for ${clusterName} CI-18' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'podName' - operator: 'Include' - values: [ - '*' - ] - } - { - name: 'kubernetesNamespace' - operator: 'Include' - values: [ - '*' - ] - } - ] - metricName: 'pvUsageExceededPercentage' - metricNamespace: 'Insights.Container/persistentvolumes' - name: 'Metric1' - operator: 'GreaterThan' - threshold: 80 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'This alert monitors persistent volume utilization.' - enabled: false - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'microsoft.containerservice/managedclusters' - windowSize: windowSize - } -} - -resource Pods_not_in_ready_state_for_clusterName_CI_8 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Pods not in ready state for ${clusterName} CI-8' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'controllerName' - operator: 'Include' - values: [ - '*' - ] - } - { - name: 'kubernetes namespace' - operator: 'Include' - values: [ - '*' - ] - } - ] - metricName: 'PodReadyPercentage' - metricNamespace: 'Insights.Container/pods' - name: 'Metric1' - operator: 'LessThan' - threshold: 80 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'This alert monitors for excessive pods not in the ready state.' - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'microsoft.containerservice/managedclusters' - windowSize: windowSize - } -} - -resource Restarting_container_count_for_clusterName_CI_7 'Microsoft.Insights/metricAlerts@2018-03-01' = { - name: 'Restarting container count for ${clusterName} CI-7' - location: 'global' - properties: { - criteria: { - allOf: [ - { - criterionType: 'StaticThresholdCriterion' - dimensions: [ - { - name: 'kubernetes namespace' - operator: 'Include' - values: [ - '*' - ] - } - { - name: 'controllerName' - operator: 'Include' - values: [ - '*' - ] - } - ] - metricName: 'restartingContainerCount' - metricNamespace: 'Insights.Container/pods' - name: 'Metric1' - operator: 'GreaterThan' - threshold: 0 - timeAggregation: 'Average' - skipMetricValidation: true - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - description: 'This alert monitors number of containers restarting across the cluster.' - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - AksResourceId - ] - severity: alertSeverityNumber - targetResourceType: 'Microsoft.ContainerService/managedClusters' - windowSize: windowSize - } -} - -resource Container_CPU_usage_violates_the_configured_threshold_for_clustername_CI_19 'microsoft.insights/metricAlerts@2018-03-01' = { - name: 'Container CPU usage violates the configured threshold for ${clusterName} CI-19' - location: 'global' - properties: { - description: 'This alert monitors container CPU usage. It uses the threshold defined in the config map.' - severity: alertSeverityNumber - enabled: true - scopes: [ - AksResourceId - ] - evaluationFrequency: evalFrequency - windowSize: windowSize - criteria: { - allOf: [ - { - threshold: 0 - name: 'Metric1' - metricNamespace: 'Insights.Container/containers' - metricName: 'cpuThresholdViolated' - dimensions: [ - { - name: 'controllerName' - operator: 'Include' - values: [ - '*' - ] - } - { - name: 'kubernetes namespace' - operator: 'Include' - values: [ - '*' - ] - } - ] - operator: 'GreaterThan' - timeAggregation: 'Average' - skipMetricValidation: true - criterionType: 'StaticThresholdCriterion' - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - } -} - -resource Container_working_set_memory_usage_violates_the_configured_threshold_for_clustername_CI_20 'microsoft.insights/metricAlerts@2018-03-01' = { - name: 'Container working set memory usage violates the configured threshold for ${clusterName} CI-20' - location: 'global' - properties: { - description: 'This alert monitors container working set memory usage. It uses the threshold defined in the config map.' - severity: alertSeverityNumber - enabled: metricAlertsEnabled - scopes: [ - AksResourceId - ] - evaluationFrequency: evalFrequency - windowSize: windowSize - criteria: { - allOf: [ - { - threshold: 0 - name: 'Metric1' - metricNamespace: 'Insights.Container/containers' - metricName: 'memoryWorkingSetThresholdViolated' - dimensions: [ - { - name: 'controllerName' - operator: 'Include' - values: [ - '*' - ] - } - { - name: 'kubernetes namespace' - operator: 'Include' - values: [ - '*' - ] - } - ] - operator: 'GreaterThan' - timeAggregation: 'Average' - skipMetricValidation: true - criterionType: 'StaticThresholdCriterion' - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - } -} - - -resource PV_usage_violates_the_configured_threshold_for_clustername_CI_21 'microsoft.insights/metricAlerts@2018-03-01' = { - name: 'PV usage violates the configured threshold for ${clusterName} CI-21' - location: 'global' - properties: { - description: 'This alert monitors PV usage. It uses the threshold defined in the config map.' - severity: alertSeverityNumber - enabled: metricAlertsEnabled - scopes: [ - AksResourceId - ] - evaluationFrequency: evalFrequency - windowSize: windowSize - criteria: { - allOf: [ - { - threshold: 0 - name: 'Metric1' - metricNamespace: 'Insights.Container/persistentvolumes' - metricName: 'pvUsageThresholdViolated' - dimensions: [ - { - name: 'podName' - operator: 'Include' - values: [ - '*' - ] - } - { - name: 'kubernetesNamespace' - operator: 'Include' - values: [ - '*' - ] - } - ] - operator: 'GreaterThan' - timeAggregation: 'Average' - skipMetricValidation: true - criterionType: 'StaticThresholdCriterion' - } - ] - 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' - } - } -} - - -resource Daily_law_datacap 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { - name: 'Daily data cap breached for workspace ${logAnalyticsWorkspaceName} CIQ-1' - location: logAnalyticsWorkspaceLocation - properties: { - displayName: 'Daily data cap breached for workspace ${logAnalyticsWorkspaceName} CIQ-1' - description: 'This alert monitors daily data cap defined on a workspace and fires when the daily data cap is breached.' - severity: 1 - enabled: metricAlertsEnabled - evaluationFrequency: evalFrequency - scopes: [ - resourceId('microsoft.operationalinsights/workspaces', logAnalyticsWorkspaceName) - ] - windowSize: windowSize - autoMitigate: false - criteria: { - allOf: [ - { - query: '_LogOperation | where Operation == "Data collection Status" | where Detail contains "OverQuota"' - timeAggregation: 'Count' - operator: 'GreaterThan' - threshold: 0 - failingPeriods: { - numberOfEvaluationPeriods: 1 - minFailingPeriodsToAlert: 1 - } - } - ] - } - muteActionsDuration: 'P1D' - } -} diff --git a/templates/common/infra/bicep/core/host/aks/aksnetcontrib.bicep b/templates/common/infra/bicep/core/host/aks/aksnetcontrib.bicep deleted file mode 100644 index bae871c898d..00000000000 --- a/templates/common/infra/bicep/core/host/aks/aksnetcontrib.bicep +++ /dev/null @@ -1,44 +0,0 @@ -//using a seperate module file as Byo subnet scenario caters from where the subnet is in the another resource group -//name/rg required to new up an existing reference and form a dependency -//principalid required as it needs to be used to establish a unique roleassignment name -param byoAKSSubnetId string -param user_identity_principalId string - -@allowed([ - 'Subnet' - 'Vnet' -]) -param rbacAssignmentScope string = 'Subnet' - -var networkContributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7') - -var existingAksSubnetName = !empty(byoAKSSubnetId) ? split(byoAKSSubnetId, '/')[10] : '' -var existingAksVnetName = !empty(byoAKSSubnetId) ? split(byoAKSSubnetId, '/')[8] : '' - -resource existingvnet 'Microsoft.Network/virtualNetworks@2021-02-01' existing = { - name: existingAksVnetName -} -resource existingAksSubnet 'Microsoft.Network/virtualNetworks/subnets@2020-08-01' existing = { - parent: existingvnet - name: existingAksSubnetName -} - -resource subnetRbac 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (rbacAssignmentScope == 'subnet') { - name: guid(user_identity_principalId, networkContributorRole, existingAksSubnetName) - scope: existingAksSubnet - properties: { - roleDefinitionId: networkContributorRole - principalId: user_identity_principalId - principalType: 'ServicePrincipal' - } -} - -resource existingVnetRbac 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (rbacAssignmentScope != 'subnet') { - name: guid(user_identity_principalId, networkContributorRole, existingAksVnetName) - scope: existingvnet - properties: { - roleDefinitionId: networkContributorRole - principalId: user_identity_principalId - principalType: 'ServicePrincipal' - } -} diff --git a/templates/common/infra/bicep/core/host/aks/appgw.bicep b/templates/common/infra/bicep/core/host/aks/appgw.bicep deleted file mode 100644 index 82c0e2a250c..00000000000 --- a/templates/common/infra/bicep/core/host/aks/appgw.bicep +++ /dev/null @@ -1,196 +0,0 @@ -param resourceName string -param location string -param appGwSubnetId string -param privateIpApplicationGateway string -param availabilityZones array -param userAssignedIdentity string -param workspaceId string -param appGWcount int -param appGWmaxCount int - -var appgwName = 'agw-${resourceName}' -var appgwResourceId = resourceId('Microsoft.Network/applicationGateways', '${appgwName}') - -resource appgwpip 'Microsoft.Network/publicIPAddresses@2020-07-01' = { - name: 'pip-agw-${resourceName}' - location: location - sku: { - name: 'Standard' - } - properties: { - publicIPAllocationMethod: 'Static' - } -} - -var frontendPublicIpConfig = { - properties: { - publicIPAddress: { - id: '${appgwpip.id}' - } - } - name: 'appGatewayFrontendIP' -} - -var frontendPrivateIpConfig = { - properties: { - privateIPAllocationMethod: 'Static' - privateIPAddress: privateIpApplicationGateway - subnet: { - id: appGwSubnetId - } - } - name: 'appGatewayPrivateIP' -} - -var tier = 'WAF_v2' -var appGWsku = union({ - name: tier - tier: tier -}, appGWmaxCount == 0 ? { - capacity: appGWcount -} : {}) - -// ugh, need to create a variable with the app gateway properies, because of the conditional key 'autoscaleConfiguration' -var appgwProperties = union({ - sku: appGWsku - gatewayIPConfigurations: [ - { - name: 'besubnet' - properties: { - subnet: { - id: appGwSubnetId - } - } - } - ] - frontendIPConfigurations: empty(privateIpApplicationGateway) ? array(frontendPublicIpConfig) : concat(array(frontendPublicIpConfig), array(frontendPrivateIpConfig)) - frontendPorts: [ - { - name: 'appGatewayFrontendPort' - properties: { - port: 80 - } - } - ] - backendAddressPools: [ - { - name: 'defaultaddresspool' - } - ] - backendHttpSettingsCollection: [ - { - name: 'defaulthttpsetting' - properties: { - port: 80 - protocol: 'Http' - cookieBasedAffinity: 'Disabled' - requestTimeout: 30 - pickHostNameFromBackendAddress: true - } - } - ] - httpListeners: [ - { - name: 'hlisten' - properties: { - frontendIPConfiguration: { - id: '${appgwResourceId}/frontendIPConfigurations/appGatewayFrontendIP' - } - frontendPort: { - id: '${appgwResourceId}/frontendPorts/appGatewayFrontendPort' - } - protocol: 'Http' - } - } - ] - requestRoutingRules: [ - { - name: 'appGwRoutingRuleName' - properties: { - ruleType: 'Basic' - httpListener: { - id: '${appgwResourceId}/httpListeners/hlisten' - } - backendAddressPool: { - id: '${appgwResourceId}/backendAddressPools/defaultaddresspool' - } - backendHttpSettings: { - id: '${appgwResourceId}/backendHttpSettingsCollection/defaulthttpsetting' - } - } - } - ] -}, appGWmaxCount > 0 ? { - autoscaleConfiguration: { - minCapacity: appGWcount - maxCapacity: appGWmaxCount - } -} : {}) - -var appGwZones = !empty(availabilityZones) ? availabilityZones : [] - -// 'identity' is always set until this is fixed: -// https://github.com/Azure/bicep/issues/387#issuecomment-885671296 -resource appgw 'Microsoft.Network/applicationGateways@2020-07-01' = if (!empty(userAssignedIdentity)) { - name: appgwName - location: location - zones: appGwZones - identity: !empty(userAssignedIdentity) ? { - type: 'UserAssigned' - userAssignedIdentities: { - '${userAssignedIdentity}': {} - } - } : {} - properties: appgwProperties -} - -param agicPrincipleId string -var contributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') -// https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal -resource appGwAGICContrib 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { - scope: appgw - name: guid(resourceGroup().id, appgwName, 'appgwcont') - properties: { - roleDefinitionId: contributor - principalType: 'ServicePrincipal' - principalId: agicPrincipleId - } -} - -var reader = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') -resource appGwAGICRGReader 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { - scope: resourceGroup() - name: guid(resourceGroup().id, appgwName, 'rgread') - properties: { - roleDefinitionId: reader - principalType: 'ServicePrincipal' - principalId: agicPrincipleId - } -} - -output appgwId string = appgw.id -output ApplicationGatewayName string = appgw.name - -// ------------------------------------------------------------------ AppGW Diagnostics -var diagProperties = { - workspaceId: workspaceId - logs: [ - { - category: 'ApplicationGatewayAccessLog' - enabled: true - } - { - category: 'ApplicationGatewayPerformanceLog' - enabled: true - } - { - category: 'ApplicationGatewayFirewallLog' - enabled: true - } - ] -} -resource appgw_Diag 'Microsoft.Insights/diagnosticSettings@2017-05-01-preview' = if (!empty(workspaceId)) { - scope: appgw - name: 'appgwDiag' - properties: diagProperties -} diff --git a/templates/common/infra/bicep/core/host/aks/bicepconfig.json b/templates/common/infra/bicep/core/host/aks/bicepconfig.json deleted file mode 100644 index a4efe888291..00000000000 --- a/templates/common/infra/bicep/core/host/aks/bicepconfig.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "analyzers": { - "core": { - "enabled": true, - "verbose": false, - "rules": { - "no-hardcoded-location" : { - "level": "error" - }, - "no-loc-expr-outside-params" : { - "level": "warning" - }, - "protect-commandtoexecute-secrets" : { - "level": "warning" - }, - "explicit-values-for-loc-params" : { - "level": "warning" - }, - "adminusername-should-not-be-literal": { - "level": "warning" - }, - "no-hardcoded-env-urls": { - "level": "warning" - }, - "no-unnecessary-dependson": { - "level": "warning" - }, - "no-unused-params": { - "level": "warning" - }, - "no-unused-vars": { - "level": "warning" - }, - "outputs-should-not-contain-secrets": { - "level": "warning" - }, - "prefer-interpolation": { - "level": "warning" - }, - "secure-parameter-default": { - "level": "warning" - }, - "simplify-interpolation": { - "level": "error" - }, - "use-stable-vm-image": { - "level": "warning" - }, - "secure-secrets-in-params" : { - "level": "error" - } - } - } - } - } \ No newline at end of file diff --git a/templates/common/infra/bicep/core/host/aks/calcAzFwIp.bicep b/templates/common/infra/bicep/core/host/aks/calcAzFwIp.bicep deleted file mode 100644 index 762cdb66709..00000000000 --- a/templates/common/infra/bicep/core/host/aks/calcAzFwIp.bicep +++ /dev/null @@ -1,10 +0,0 @@ -// As per https://github.com/Azure/bicep/issues/2189#issuecomment-815962675 this file is being used as a UDF -// Takes a subnet range and returns the AzFirewall private Ip address - -@description('A subnet address for the Azure Firewall') -param vnetFirewallSubnetAddressPrefix string - -var subnetOctets = split(vnetFirewallSubnetAddressPrefix,'.') -var hostIdOctet = '4' - -output FirewallPrivateIp string = '${subnetOctets[0]}.${subnetOctets[1]}.${subnetOctets[2]}.${hostIdOctet}' diff --git a/templates/common/infra/bicep/core/host/aks/dnsZone.bicep b/templates/common/infra/bicep/core/host/aks/dnsZone.bicep deleted file mode 100644 index 2f678304cdb..00000000000 --- a/templates/common/infra/bicep/core/host/aks/dnsZone.bicep +++ /dev/null @@ -1,47 +0,0 @@ -param dnsZoneName string -param principalId string -param isPrivate bool -param vnetId string = '' - -resource dns 'Microsoft.Network/dnsZones@2018-05-01' existing = if (!isPrivate) { - name: dnsZoneName -} - -resource privateDns 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (isPrivate) { - name: dnsZoneName -} - -var DNSZoneContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314') -resource dnsContributor 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (!isPrivate) { - scope: dns - name: guid(dns.id, principalId, DNSZoneContributor) - properties: { - roleDefinitionId: DNSZoneContributor - principalType: 'ServicePrincipal' - principalId: principalId - } -} - -var PrivateDNSZoneContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f') -resource privateDnsContributor 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (isPrivate) { - scope: privateDns - name: guid(privateDns.id, principalId, PrivateDNSZoneContributor) - properties: { - roleDefinitionId: PrivateDNSZoneContributor - principalType: 'ServicePrincipal' - principalId: principalId - } -} - -resource dns_vnet_link 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (isPrivate && !empty(vnetId)) { - parent: privateDns - name: 'privatedns' - tags: {} - location: 'global' - properties: { - virtualNetwork: { - id: vnetId - } - registrationEnabled: false - } -} diff --git a/templates/common/infra/bicep/core/host/aks/dnsZoneRbac.bicep b/templates/common/infra/bicep/core/host/aks/dnsZoneRbac.bicep deleted file mode 100644 index ad4ae4354a5..00000000000 --- a/templates/common/infra/bicep/core/host/aks/dnsZoneRbac.bicep +++ /dev/null @@ -1,26 +0,0 @@ -//This module facilitates RBAC assigned to specific DNS Zones. -//The DNS Zone Id is extracted and the scope is set correctly. - -@description('The full Azure resource ID of the DNS zone to use for the AKS cluster') -param dnsZoneId string - -@description('The id of a virtual network to be linked to a PRIVATE DNS Zone') -param vnetId string - -@description('The AAD identity to create the RBAC against') -param principalId string - -var dnsZoneRg = !empty(dnsZoneId) ? split(dnsZoneId, '/')[4] : '' -var dnsZoneName = !empty(dnsZoneId) ? split(dnsZoneId, '/')[8] : '' -var isDnsZonePrivate = !empty(dnsZoneId) ? split(dnsZoneId, '/')[7] == 'privateDnsZones' : false - -module dnsZone './dnsZone.bicep' = if (!empty(dnsZoneId)) { - name: 'dns-${dnsZoneName}' - scope: resourceGroup(dnsZoneRg) - params: { - dnsZoneName: dnsZoneName - isPrivate: isDnsZonePrivate - vnetId : vnetId - principalId: principalId - } -} diff --git a/templates/common/infra/bicep/core/host/aks/firewall.bicep b/templates/common/infra/bicep/core/host/aks/firewall.bicep deleted file mode 100644 index 2fd934ce6f6..00000000000 --- a/templates/common/infra/bicep/core/host/aks/firewall.bicep +++ /dev/null @@ -1,324 +0,0 @@ -param resourceName string -param location string = resourceGroup().location -param workspaceDiagsId string = '' -param fwSubnetId string -param fwManagementSubnetId string = '' -param vnetAksSubnetAddressPrefix string -param certManagerFW bool = false -param acrPrivatePool bool = false -param acrAgentPoolSubnetAddressPrefix string = '' -param availabilityZones array = [] -param fwSku string - -var firewallPublicIpName = 'pip-afw-${resourceName}' -var firewallManagementPublicIpName = 'pip-mgmt-afw-${resourceName}' - -var managementIpConfig = { - name: 'MgmtIpConf' - properties: { - publicIPAddress: { - id: !empty(fwManagementSubnetId) ? fwManagementIp_pip.id : null - } - subnet:{ - id: !empty(fwManagementSubnetId) ? fwManagementSubnetId : null - } - } -} - -resource fw_pip 'Microsoft.Network/publicIPAddresses@2021-03-01' = { - name: firewallPublicIpName - location: location - sku: { - name: 'Standard' - } - zones: !empty(availabilityZones) ? availabilityZones : [] - properties: { - publicIPAllocationMethod: 'Static' - publicIPAddressVersion: 'IPv4' - } -} - -resource fwManagementIp_pip 'Microsoft.Network/publicIPAddresses@2021-03-01' = if(fwSku=='Basic') { - name: firewallManagementPublicIpName - location: location - sku: { - name: 'Standard' - } - zones: !empty(availabilityZones) ? availabilityZones : [] - properties: { - publicIPAllocationMethod: 'Static' - publicIPAddressVersion: 'IPv4' - } -} - -resource fwDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceDiagsId)) { - scope: fw - name: 'fwDiags' - properties: { - workspaceId: workspaceDiagsId - logs: [ - { - category: 'AzureFirewallApplicationRule' - enabled: true - retentionPolicy: { - days: 10 - enabled: false - } - } - { - category: 'AzureFirewallNetworkRule' - enabled: true - retentionPolicy: { - days: 10 - enabled: false - } - } - ] - metrics: [ - { - category: 'AllMetrics' - enabled: true - retentionPolicy: { - enabled: false - days: 0 - } - } - ] - } -} - -@description('Whitelist dnsZone name (required by cert-manager validation process)') -param appDnsZoneName string = '' - -var fw_name = 'afw-${resourceName}' -resource fw 'Microsoft.Network/azureFirewalls@2022-01-01' = { - name: fw_name - location: location - zones: !empty(availabilityZones) ? availabilityZones : [] - properties: { - sku: { - tier: fwSku - } - ipConfigurations: [ - { - name: 'IpConf1' - properties: { - subnet: { - id: fwSubnetId - } - publicIPAddress: { - id: fw_pip.id - } - } - } - ] - managementIpConfiguration: !empty(fwManagementSubnetId) ? managementIpConfig : null - threatIntelMode: 'Alert' - firewallPolicy: { - id: fwPolicy.id - } - applicationRuleCollections: [] - networkRuleCollections: [] - } -} - -resource fwPolicy 'Microsoft.Network/firewallPolicies@2022-01-01' = { - name: 'afwp-${resourceName}' - location: location - properties: { - sku: { - tier: fwSku - } - threatIntelMode: 'Alert' - threatIntelWhitelist: { - fqdns: [] - ipAddresses: [] - } - } -} - -resource fwpRules 'Microsoft.Network/firewallPolicies/ruleCollectionGroups@2022-01-01' = { - parent: fwPolicy - name: 'AKSConstructionRuleGroup' - properties: { - priority: 200 - ruleCollections: [ - { - ruleCollectionType: 'FirewallPolicyFilterRuleCollection' - name: 'CoreAksNetEgress' - priority: 100 - action: { - type: 'Allow' - } - rules: concat([ - { - name: 'ControlPlaneTCP' - ruleType: 'NetworkRule' - ipProtocols: [ - 'TCP' - ] - sourceAddresses: [ - vnetAksSubnetAddressPrefix - ] - destinationAddresses: [ - 'AzureCloud.${location}' - ] - destinationPorts: [ - '9000' /* For tunneled secure communication between the nodes and the control plane. */ - '22' - ] - } - { - name: 'ControlPlaneUDP' - ruleType: 'NetworkRule' - ipProtocols: [ - 'UDP' - ] - sourceAddresses: [ - vnetAksSubnetAddressPrefix - ] - destinationAddresses: [ - 'AzureCloud.${location}' - ] - destinationPorts: [ - '1194' /* For tunneled secure communication between the nodes and the control plane. */ - ] - } - { - name: 'AzureMonitorForContainers' - ruleType: 'NetworkRule' - ipProtocols: [ - 'TCP' - ] - sourceAddresses: [ - vnetAksSubnetAddressPrefix - ] - destinationAddresses: [ - 'AzureMonitor' - ] - destinationPorts: [ - '443' - ] - } - ], acrPrivatePool ? [ - { - name: 'acr-agentpool' - ruleType: 'NetworkRule' - ipProtocols: [ - 'TCP' - ] - sourceAddresses: [ - acrAgentPoolSubnetAddressPrefix - ] - destinationAddresses: [ - 'AzureKeyVault' - 'Storage' - 'EventHub' - 'AzureActiveDirectory' - 'AzureMonitor' - ] - destinationPorts: [ - '443' - ] - } - ]:[]) - } - { - ruleCollectionType: 'FirewallPolicyFilterRuleCollection' - name: 'CoreAksHttpEgress' - priority: 400 - action: { - type: 'Allow' - } - rules: concat([ - { - name: 'aks' - ruleType: 'ApplicationRule' - protocols: [ - { - port: 443 - protocolType: 'Https' - } - { - port: 80 - protocolType: 'Http' - } - ] - targetFqdns: [] - fqdnTags: [ - 'AzureKubernetesService' - ] - sourceAddresses: [ - vnetAksSubnetAddressPrefix - ] - } - ], certManagerFW ? [ - { - name: 'certman-quay' - ruleType: 'ApplicationRule' - protocols: [ - { - port: 443 - protocolType: 'Https' - } - { - port: 80 - protocolType: 'Http' - } - ] - targetFqdns: [ - 'quay.io' - '*.quay.io' - ] - sourceAddresses: [ - vnetAksSubnetAddressPrefix - ] - } - { - name: 'certman-letsencrypt' - ruleType: 'ApplicationRule' - protocols: [ - { - port: 443 - protocolType: 'Https' - } - { - port: 80 - protocolType: 'Http' - } - ] - targetFqdns: [ - 'letsencrypt.org' - '*.letsencrypt.org' - ] - sourceAddresses: [ - vnetAksSubnetAddressPrefix - ] - } - ] : [], certManagerFW && !empty(appDnsZoneName) ? [ - { - name: 'certman-appDnsZoneName' - ruleType: 'ApplicationRule' - protocols: [ - { - port: 443 - protocolType: 'Https' - } - { - port: 80 - protocolType: 'Http' - } - ] - targetFqdns: [ - appDnsZoneName - '*.${appDnsZoneName}' - ] - sourceAddresses: [ - vnetAksSubnetAddressPrefix - ] - } - ] : []) - } - ] - } -} diff --git a/templates/common/infra/bicep/core/host/aks/keyvault.bicep b/templates/common/infra/bicep/core/host/aks/keyvault.bicep deleted file mode 100644 index 3a6283f3d8e..00000000000 --- a/templates/common/infra/bicep/core/host/aks/keyvault.bicep +++ /dev/null @@ -1,80 +0,0 @@ -@minLength(2) -@description('The location to use for the deployment. defaults to Resource Groups location.') -param location string = resourceGroup().location - -@minLength(3) -@maxLength(20) -@description('Used to name all resources') -param resourceName string - -@description('Enable support for private links') -param privateLinks bool = false - -@description('If soft delete protection is enabled') -param keyVaultSoftDelete bool = true - -@description('If purge protection is enabled') -param keyVaultPurgeProtection bool = true - -@description('Add IP to KV firewall allow-list') -param keyVaultIPAllowlist array = [] - -param logAnalyticsWorkspaceId string = '' - -var akvRawName = 'kv-${replace(resourceName, '-', '')}${uniqueString(resourceGroup().id, resourceName)}' -var akvName = length(akvRawName) > 24 ? substring(akvRawName, 0, 24) : akvRawName - -var kvIPRules = [for kvIp in keyVaultIPAllowlist: { - value: kvIp -}] - -resource kv 'Microsoft.KeyVault/vaults@2021-11-01-preview' = { - name: akvName - location: location - properties: { - tenantId: subscription().tenantId - sku: { - family: 'A' - name: 'standard' - } - // publicNetworkAccess: whether the vault will accept traffic from public internet. If set to 'disabled' all traffic except private endpoint traffic and that that originates from trusted services will be blocked. - publicNetworkAccess: privateLinks && empty(keyVaultIPAllowlist) ? 'disabled' : 'enabled' - - networkAcls: privateLinks && !empty(keyVaultIPAllowlist) ? { - bypass: 'AzureServices' - defaultAction: 'Deny' - ipRules: kvIPRules - virtualNetworkRules: [] - } : {} - - enableRbacAuthorization: true - enabledForDeployment: false - enabledForDiskEncryption: false - enabledForTemplateDeployment: false - enableSoftDelete: keyVaultSoftDelete - enablePurgeProtection: keyVaultPurgeProtection ? true : json('null') - } -} - -resource kvDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(logAnalyticsWorkspaceId)) { - name: 'kvDiags' - scope: kv - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - category: 'AuditEvent' - enabled: true - } - ] - metrics: [ - { - category: 'AllMetrics' - enabled: true - } - ] - } -} - -output keyVaultName string = kv.name -output keyVaultId string = kv.id diff --git a/templates/common/infra/bicep/core/host/aks/keyvaultkey.bicep b/templates/common/infra/bicep/core/host/aks/keyvaultkey.bicep deleted file mode 100644 index f00bf67017a..00000000000 --- a/templates/common/infra/bicep/core/host/aks/keyvaultkey.bicep +++ /dev/null @@ -1,24 +0,0 @@ -param keyVaultName string - -resource kv 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = { - name: keyVaultName -} - -resource kvKmsKey 'Microsoft.KeyVault/vaults/keys@2021-11-01-preview' = { - name: 'kmskey' - parent: kv - properties: { - kty: 'RSA' - keySize: 2048 - keyOps: [ - 'wrapKey' - 'unwrapKey' - 'decrypt' - 'encrypt' - 'verify' - 'sign' - ] - } -} - -output keyVaultKmsKeyUri string = kvKmsKey.properties.keyUriWithVersion diff --git a/templates/common/infra/bicep/core/host/aks/keyvaultrbac.bicep b/templates/common/infra/bicep/core/host/aks/keyvaultrbac.bicep deleted file mode 100644 index 6ef65dd1e08..00000000000 --- a/templates/common/infra/bicep/core/host/aks/keyvaultrbac.bicep +++ /dev/null @@ -1,173 +0,0 @@ -param keyVaultName string - -@description('An array of Service Principal IDs') -#disable-next-line secure-secrets-in-params //Disabling validation of this linter rule as param does not contain a secret. -param rbacSecretUserSps array = [] - -@description('An array of Service Principal IDs') -#disable-next-line secure-secrets-in-params //Disabling validation of this linter rule as param does not contain a secret. -param rbacSecretOfficerSps array = [] - -@description('An array of Service Principal IDs') -param rbacCertOfficerSps array = [] - -@description('An array of Service Principal IDs') -param rbacCryptoUserSps array = [] - -@description('An array of Service Principal IDs') -param rbacCryptoOfficerSps array = [] - -@description('An array of Service Principal IDs') -param rbacCryptoServiceEncryptSps array = [] - -@description('An array of Service Principal IDs') -param rbacKvContributorSps array = [] - -@description('An array of Service Principal IDs') -param rbacAdminSps array = [] - -@description('An array of User IDs') -param rbacCryptoOfficerUsers array = [] - -@description('An array of User IDs') -#disable-next-line secure-secrets-in-params //Disabling validation of this linter rule as param does not contain a secret. -param rbacSecretOfficerUsers array = [] - -@description('An array of User IDs') -param rbacCertOfficerUsers array = [] - -@description('An array of User IDs') -param rbacAdminUsers array = [] - -var keyVaultAdministratorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483') -var keyVaultContributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f25e0fa2-a7c8-4377-a976-54943a77a395') -var keyVaultSecretsUserRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') -var keyVaultSecretsOfficerRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7') -var keyVaultCertsOfficerRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a4417e6f-fecd-4de8-b567-7b0420556985') -var keyVaultCryptoUserRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '12338af0-0e69-4776-bea7-57ae8d297424') -var keyVaultCryptoOfficerRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '14b46e9e-c2b7-41b4-b07b-48a6ebf60603') -var keyVaultCryptoServiceEncrpytionRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions','e147488a-f6f5-4113-8e2d-b22465e65bf6') - -resource kv 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = { - name: keyVaultName -} - -resource rbacSecretUserSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacSecretUserSps : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultSecretsUserRole) - properties: { - roleDefinitionId: keyVaultSecretsUserRole - principalType: 'ServicePrincipal' - principalId: rbacSp - } -}] - -resource rbacSecretOfficerSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacSecretOfficerSps : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultSecretsOfficerRole) - properties: { - roleDefinitionId: keyVaultSecretsOfficerRole - principalType: 'ServicePrincipal' - principalId: rbacSp - } -}] - -resource rbacCertsOfficerSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCertOfficerSps : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultCertsOfficerRole) - properties: { - roleDefinitionId: keyVaultCertsOfficerRole - principalType: 'ServicePrincipal' - principalId: rbacSp - } -}] - -resource rbacCryptoUserSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCryptoUserSps : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultCryptoUserRole) - properties: { - roleDefinitionId: keyVaultCryptoUserRole - principalType: 'ServicePrincipal' - principalId: rbacSp - } -}] - -resource rbacCryptoServiceEncryptionSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCryptoServiceEncryptSps : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultCryptoServiceEncrpytionRole) - properties: { - roleDefinitionId: keyVaultCryptoServiceEncrpytionRole - principalType: 'ServicePrincipal' - principalId: rbacSp - } -}] - -resource rbacKvContributorSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacKvContributorSps : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultContributorRole) - properties: { - roleDefinitionId: keyVaultContributorRole - principalType: 'ServicePrincipal' - principalId: rbacSp - } -}] - -resource rbacCryptoOfficerSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCryptoOfficerSps : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultCryptoUserRole) - properties: { - roleDefinitionId: keyVaultCryptoOfficerRole - principalType: 'ServicePrincipal' - principalId: rbacSp - } -}] - -resource rbacAdminSp 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacAdminSps : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultAdministratorRole) - properties: { - roleDefinitionId: keyVaultAdministratorRole - principalType: 'ServicePrincipal' - principalId: rbacSp - } -}] - -resource rbacCryptoOfficerUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCryptoOfficerUsers : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultCryptoOfficerRole) - properties: { - roleDefinitionId: keyVaultCryptoOfficerRole - principalType: 'User' - principalId: rbacSp - } -}] - -resource rbacSecretOfficerUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacSecretOfficerUsers : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultSecretsOfficerRole) - properties: { - roleDefinitionId: keyVaultSecretsOfficerRole - principalType: 'User' - principalId: rbacSp - } -}] - -resource rbacCertsOfficerUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacCertOfficerUsers : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultCertsOfficerRole) - properties: { - roleDefinitionId: keyVaultCertsOfficerRole - principalType: 'User' - principalId: rbacSp - } -}] - -resource rbacAdminUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for rbacSp in rbacAdminUsers : if(!empty(rbacSp)) { - scope: kv - name: guid(kv.id, rbacSp, keyVaultAdministratorRole) - properties: { - roleDefinitionId: keyVaultAdministratorRole - principalType: 'User' - principalId: rbacSp - } -}] diff --git a/templates/common/infra/bicep/core/host/aks/main.bicep b/templates/common/infra/bicep/core/host/aks/main.bicep deleted file mode 100644 index a90de304ea3..00000000000 --- a/templates/common/infra/bicep/core/host/aks/main.bicep +++ /dev/null @@ -1,1625 +0,0 @@ -@minLength(2) -@description('The location to use for the deployment. defaults to Resource Groups location.') -param location string = resourceGroup().location - -@minLength(3) -@maxLength(20) -@description('Used to name all resources') -param resourceName string - -/* -Resource sections -1. Networking -2. DNS -3. Key Vault -4. Container Registry -5. Firewall -6. Application Gateway -7. AKS -8. Monitoring / Log Analytics -9. Deployment for telemetry -*/ - -/*.__ __. _______ .___________.____ __ ____ ______ .______ __ ___ __ .__ __. _______ -| \ | | | ____|| |\ \ / \ / / / __ \ | _ \ | |/ / | | | \ | | / _____| -| \| | | |__ `---| |----` \ \/ \/ / | | | | | |_) | | ' / | | | \| | | | __ -| . ` | | __| | | \ / | | | | | / | < | | | . ` | | | |_ | -| |\ | | |____ | | \ /\ / | `--' | | |\ \----.| . \ | | | |\ | | |__| | -|__| \__| |_______| |__| \__/ \__/ \______/ | _| `._____||__|\__\ |__| |__| \__| \______| */ -//Networking can either be one of: custom / byo / default - -@description('Are you providing your own vNet CIDR blocks') -param custom_vnet bool = false - -@description('Full resource id path of an existing subnet to use for AKS') -param byoAKSSubnetId string = '' - -@description('Full resource id path of an existing subnet to use for Application Gateway') -param byoAGWSubnetId string = '' - -//--- Custom, BYO networking and PrivateApiZones requires BYO AKS User Identity -var createAksUai = custom_vnet || !empty(byoAKSSubnetId) || !empty(dnsApiPrivateZoneId) || keyVaultKmsCreateAndPrereqs || !empty(keyVaultKmsByoKeyId) -resource aksUai 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' = if (createAksUai) { - name: 'id-aks-${resourceName}' - location: location -} - -//----------------------------------------------------- BYO Vnet -var existingAksVnetRG = !empty(byoAKSSubnetId) ? (length(split(byoAKSSubnetId, '/')) > 4 ? split(byoAKSSubnetId, '/')[4] : '') : '' - -module aksnetcontrib './aksnetcontrib.bicep' = if (!empty(byoAKSSubnetId) && createAksUai) { - name: 'addAksNetContributor' - scope: resourceGroup(existingAksVnetRG) - params: { - byoAKSSubnetId: byoAKSSubnetId - user_identity_principalId: createAksUai ? aksUai.properties.principalId : '' - rbacAssignmentScope: uaiNetworkScopeRbac - } -} - -//------------------------------------------------------ Create custom vnet -@minLength(9) -@maxLength(18) -@description('The address range for the custom vnet') -param vnetAddressPrefix string = '10.240.0.0/16' - -@minLength(9) -@maxLength(18) -@description('The address range for AKS in your custom vnet') -param vnetAksSubnetAddressPrefix string = '10.240.0.0/22' - -@minLength(9) -@maxLength(18) -@description('The address range for the App Gateway in your custom vnet') -param vnetAppGatewaySubnetAddressPrefix string = '10.240.5.0/24' - -@minLength(9) -@maxLength(18) -@description('The address range for the ACR in your custom vnet') -param acrAgentPoolSubnetAddressPrefix string = '10.240.4.64/26' - -@minLength(9) -@maxLength(18) -@description('The address range for Azure Bastion in your custom vnet') -param bastionSubnetAddressPrefix string = '10.240.4.128/26' - -@minLength(9) -@maxLength(18) -@description('The address range for private link in your custom vnet') -param privateLinkSubnetAddressPrefix string = '10.240.4.192/26' - -@minLength(9) -@maxLength(18) -@description('The address range for Azure Firewall in your custom vnet') -param vnetFirewallSubnetAddressPrefix string = '10.240.50.0/24' - -@minLength(9) -@maxLength(18) -@description('The address range for Azure Firewall Management in your custom vnet') -param vnetFirewallManagementSubnetAddressPrefix string = '10.240.51.0/26' - -@description('Enable support for private links (required custom_vnet)') -param privateLinks bool = false - -@description('Enable support for ACR private pool') -param acrPrivatePool bool = false - -@description('Deploy Azure Bastion to your vnet. (works with Custom Networking only, not BYO)') -param bastion bool = false - -@description('Deploy NSGs to your vnet subnets. (works with Custom Networking only, not BYO)') -param CreateNetworkSecurityGroups bool = false - -@description('Configure Flow Logs for Network Security Groups in the NetworkWatcherRG resource group. Requires Contributor RBAC on NetworkWatcherRG and Reader on Subscription.') -param CreateNetworkSecurityGroupFlowLogs bool = false - -module network './network.bicep' = if (custom_vnet) { - name: 'network' - params: { - resourceName: resourceName - location: location - networkPluginIsKubenet: networkPlugin == 'kubenet' - vnetAddressPrefix: vnetAddressPrefix - aksPrincipleId: createAksUai ? aksUai.properties.principalId : '' - vnetAksSubnetAddressPrefix: vnetAksSubnetAddressPrefix - ingressApplicationGateway: ingressApplicationGateway - vnetAppGatewaySubnetAddressPrefix: vnetAppGatewaySubnetAddressPrefix - azureFirewalls: azureFirewalls - vnetFirewallSubnetAddressPrefix: vnetFirewallSubnetAddressPrefix - vnetFirewallManagementSubnetAddressPrefix: vnetFirewallManagementSubnetAddressPrefix - privateLinks: privateLinks - privateLinkSubnetAddressPrefix: privateLinkSubnetAddressPrefix - privateLinkAcrId: privateLinks && !empty(registries_sku) ? acr.id : '' - privateLinkAkvId: privateLinks && keyVaultCreate ? kv.outputs.keyVaultId : '' - acrPrivatePool: acrPrivatePool - acrAgentPoolSubnetAddressPrefix: acrAgentPoolSubnetAddressPrefix - bastion: bastion - bastionSubnetAddressPrefix: bastionSubnetAddressPrefix - availabilityZones: availabilityZones - workspaceName: createLaw ? aks_law.name : '' - workspaceResourceGroupName: createLaw ? resourceGroup().name : '' - networkSecurityGroups: CreateNetworkSecurityGroups - CreateNsgFlowLogs: CreateNetworkSecurityGroups && CreateNetworkSecurityGroupFlowLogs - ingressApplicationGatewayPublic: empty(privateIpApplicationGateway) - natGateway: createNatGateway - natGatewayIdleTimeoutMins: natGwIdleTimeout - natGatewayPublicIps: natGwIpCount - } -} -output CustomVnetId string = custom_vnet ? network.outputs.vnetId : '' -output CustomVnetPrivateLinkSubnetId string = custom_vnet ? network.outputs.privateLinkSubnetId : '' - -var aksSubnetId = custom_vnet ? network.outputs.aksSubnetId : byoAKSSubnetId -var appGwSubnetId = ingressApplicationGateway ? (custom_vnet ? network.outputs.appGwSubnetId : byoAGWSubnetId) : '' - -/*______ .__ __. _______. ________ ______ .__ __. _______ _______. -| \ | \ | | / | | / / __ \ | \ | | | ____| / | -| .--. || \| | | (----` `---/ / | | | | | \| | | |__ | (----` -| | | || . ` | \ \ / / | | | | | . ` | | __| \ \ -| '--' || |\ | .----) | / /----.| `--' | | |\ | | |____ .----) | -|_______/ |__| \__| |_______/ /________| \______/ |__| \__| |_______||_______/ */ - -@description('The full Azure resource ID of the DNS zone to use for the AKS cluster') -param dnsZoneId string = '' -var isDnsZonePrivate = !empty(dnsZoneId) ? split(dnsZoneId, '/')[7] == 'privateDnsZones' : false - -module dnsZone './dnsZoneRbac.bicep' = if (!empty(dnsZoneId)) { - name: 'addDnsContributor' - params: { - dnsZoneId: dnsZoneId - vnetId: isDnsZonePrivate ? (!empty(byoAKSSubnetId) ? split(byoAKSSubnetId, '/subnets')[0] : (custom_vnet ? network.outputs.vnetId : '')) : '' - principalId: any(aks.properties.identityProfile.kubeletidentity).objectId - } -} - -/*__ __ _______ ____ ____ ____ ____ ___ __ __ __ .___________. -| |/ / | ____|\ \ / / \ \ / / / \ | | | | | | | | -| ' / | |__ \ \/ / \ \/ / / ^ \ | | | | | | `---| |----` -| < | __| \_ _/ \ / / /_\ \ | | | | | | | | -| . \ | |____ | | \ / / _____ \ | `--' | | `----. | | -|__|\__\ |_______| |__| \__/ /__/ \__\ \______/ |_______| |__| */ - -@description('Creates a KeyVault') -param keyVaultCreate bool = false - -@description('If soft delete protection is enabled') -param keyVaultSoftDelete bool = true - -@description('If purge protection is enabled') -param keyVaultPurgeProtection bool = true - -@description('Add IP to KV firewall allow-list') -param keyVaultIPAllowlist array = [] - -@description('Installs the AKS KV CSI provider') -param keyVaultAksCSI bool = false - -@description('Rotation poll interval for the AKS KV CSI provider') -param keyVaultAksCSIPollInterval string = '2m' - -@description('Creates a KeyVault for application secrets (eg. CSI)') -module kv 'keyvault.bicep' = if (keyVaultCreate) { - name: 'keyvaultApps' - params: { - resourceName: resourceName - keyVaultPurgeProtection: keyVaultPurgeProtection - keyVaultSoftDelete: keyVaultSoftDelete - keyVaultIPAllowlist: keyVaultIPAllowlist - location: location - privateLinks: privateLinks - } -} - -@description('The principal ID of the user or service principal that requires access to the Key Vault. Set automatedDeployment to toggle between user and service prinicpal') -param keyVaultOfficerRolePrincipalId string = '' -var keyVaultOfficerRolePrincipalIds = [ - keyVaultOfficerRolePrincipalId -] - -@description('Parsing an array with union ensures that duplicates are removed, which is great when dealing with highly conditional elements') -var rbacSecretUserSps = union([ deployAppGw && appgwKVIntegration ? appGwIdentity.properties.principalId : '' ], [ keyVaultAksCSI ? aks.properties.addonProfiles.azureKeyvaultSecretsProvider.identity.objectId : '' ]) - -@description('A seperate module is used for RBAC to avoid delaying the KeyVault creation and causing a circular reference.') -module kvRbac 'keyvaultrbac.bicep' = if (keyVaultCreate) { - name: 'KeyVaultAppsRbac' - params: { - keyVaultName: keyVaultCreate ? kv.outputs.keyVaultName : '' - - //service principals - rbacSecretUserSps: rbacSecretUserSps - rbacSecretOfficerSps: !empty(keyVaultOfficerRolePrincipalId) && automatedDeployment ? keyVaultOfficerRolePrincipalIds : [] - rbacCertOfficerSps: !empty(keyVaultOfficerRolePrincipalId) && automatedDeployment ? keyVaultOfficerRolePrincipalIds : [] - - //users - rbacSecretOfficerUsers: !empty(keyVaultOfficerRolePrincipalId) && !automatedDeployment ? keyVaultOfficerRolePrincipalIds : [] - rbacCertOfficerUsers: !empty(keyVaultOfficerRolePrincipalId) && !automatedDeployment ? keyVaultOfficerRolePrincipalIds : [] - } -} - -output keyVaultName string = keyVaultCreate ? kv.outputs.keyVaultName : '' -output keyVaultId string = keyVaultCreate ? kv.outputs.keyVaultId : '' - -/* KeyVault for KMS Etcd*/ - -@description('Enable encryption at rest for Kubernetes etcd data') -param keyVaultKmsCreate bool = false - -@description('Bring an existing Key from an existing Key Vault') -param keyVaultKmsByoKeyId string = '' - -@description('The resource group for the existing KMS Key Vault') -param keyVaultKmsByoRG string = resourceGroup().name - -@description('The PrincipalId of the deploying user, which is necessary when creating a Kms Key') -param keyVaultKmsOfficerRolePrincipalId string = '' - -@description('The extracted name of the existing Key Vault') -var keyVaultKmsByoName = !empty(keyVaultKmsByoKeyId) ? split(split(keyVaultKmsByoKeyId, '/')[2], '.')[0] : '' - -@description('The deployment delay to introduce when creating a new keyvault for kms key') -var kmsRbacWaitSeconds = 30 - -@description('This indicates if the deploying user has provided their PrincipalId in order for the key to be created') -var keyVaultKmsCreateAndPrereqs = keyVaultKmsCreate && !empty(keyVaultKmsOfficerRolePrincipalId) && privateLinks == false - -resource kvKmsByo 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = if (!empty(keyVaultKmsByoName)) { - name: keyVaultKmsByoName - scope: resourceGroup(keyVaultKmsByoRG) -} - -@description('Creates a new Key vault for a new KMS Key') -module kvKms 'keyvault.bicep' = if (keyVaultKmsCreateAndPrereqs) { - name: 'keyvaultKms-${resourceName}' - params: { - resourceName: 'kms${resourceName}' - keyVaultPurgeProtection: keyVaultPurgeProtection - keyVaultSoftDelete: keyVaultSoftDelete - location: location - privateLinks: privateLinks - //aksUaiObjectId: aksUai.properties.principalId - } -} - -module kvKmsCreatedRbac 'keyvaultrbac.bicep' = if (keyVaultKmsCreateAndPrereqs) { - name: 'keyvaultKmsRbacs-${resourceName}' - params: { - keyVaultName: keyVaultKmsCreate ? kvKms.outputs.keyVaultName : '' - //We can't create a kms kv and key and do privatelink. Private Link is a BYO scenario - // rbacKvContributorSps : [ - // createAksUai && privateLinks ? aksUai.properties.principalId : '' - // ] - //This allows the Aks Cluster to access the key vault key - rbacCryptoUserSps: [ - createAksUai ? aksUai.properties.principalId : '' - ] - //This allows the Deploying user to create the key vault key - rbacCryptoOfficerUsers: [ - !empty(keyVaultKmsOfficerRolePrincipalId) && !automatedDeployment ? keyVaultKmsOfficerRolePrincipalId : '' - ] - //This allows the Deploying sp to create the key vault key - rbacCryptoOfficerSps: [ - !empty(keyVaultKmsOfficerRolePrincipalId) && automatedDeployment ? keyVaultKmsOfficerRolePrincipalId : '' - ] - } -} - -module kvKmsByoRbac 'keyvaultrbac.bicep' = if (!empty(keyVaultKmsByoKeyId)) { - name: 'keyvaultKmsByoRbacs-${resourceName}' - scope: resourceGroup(keyVaultKmsByoRG) - params: { - keyVaultName: kvKmsByo.name - //Contribuor allows AKS to create the private link - rbacKvContributorSps: [ - createAksUai && privateLinks ? aksUai.properties.principalId : '' - ] - //This allows the Aks Cluster to access the key vault key - rbacCryptoUserSps: [ - createAksUai ? aksUai.properties.principalId : '' - ] - } -} - -@description('It can take time for the RBAC to propagate, this delays the deployment to avoid this problem') -module waitForKmsRbac 'br/public:deployment-scripts/wait:1.0.1' = if (keyVaultKmsCreateAndPrereqs && kmsRbacWaitSeconds > 0) { - name: 'keyvaultKmsRbac-waits-${resourceName}' - params: { - waitSeconds: kmsRbacWaitSeconds - location: location - } - dependsOn: [ - kvKmsCreatedRbac - ] -} - -@description('Adding a key to the keyvault... We can only do this for public key vaults') -module kvKmsKey 'keyvaultkey.bicep' = if (keyVaultKmsCreateAndPrereqs) { - name: 'keyvaultKmsKeys-${resourceName}' - params: { - keyVaultName: keyVaultKmsCreateAndPrereqs ? kvKms.outputs.keyVaultName : '' - } - dependsOn: [ waitForKmsRbac ] -} - -var azureKeyVaultKms = { - securityProfile: { - azureKeyVaultKms: { - enabled: keyVaultKmsCreateAndPrereqs || !empty(keyVaultKmsByoKeyId) - keyId: keyVaultKmsCreateAndPrereqs ? kvKmsKey.outputs.keyVaultKmsKeyUri : !empty(keyVaultKmsByoKeyId) ? keyVaultKmsByoKeyId : '' - keyVaultNetworkAccess: privateLinks ? 'private' : 'public' - keyVaultResourceId: privateLinks && !empty(keyVaultKmsByoKeyId) ? kvKmsByo.id : '' - } - } -} - -@description('The name of the Kms Key Vault') -output keyVaultKmsName string = keyVaultKmsCreateAndPrereqs ? kvKms.outputs.keyVaultName : !empty(keyVaultKmsByoKeyId) ? keyVaultKmsByoName : '' - -@description('Indicates if the user has supplied all the correct parameter to use a AKSC Created KMS') -output kmsCreatePrerequisitesMet bool = keyVaultKmsCreateAndPrereqs - -/* ___ ______ .______ - / \ / | | _ \ - / ^ \ | ,----' | |_) | - / /_\ \ | | | / - / _____ \ __| `----. __ | |\ \----. __ -/__/ \__\ (__)\______|(__)| _| `._____|(__)*/ - -@allowed([ - '' - 'Basic' - 'Standard' - 'Premium' -]) -@description('The SKU to use for the Container Registry') -param registries_sku string = '' - -@description('Enable the ACR Content Trust Policy, SKU must be set to Premium') -param enableACRTrustPolicy bool = false -var acrContentTrustEnabled = enableACRTrustPolicy && registries_sku == 'Premium' ? 'enabled' : 'disabled' - -//param enableACRZoneRedundancy bool = true -var acrZoneRedundancyEnabled = !empty(availabilityZones) && registries_sku == 'Premium' ? 'Enabled' : 'Disabled' - -@description('Enable removing of untagged manifests from ACR') -param acrUntaggedRetentionPolicyEnabled bool = false - -@description('The number of days to retain untagged manifests for') -param acrUntaggedRetentionPolicy int = 30 - -@description('Enable admin user for ACR push/pull') -param acrAdminUserEnabled bool = true - -var acrName = 'cr${replace(resourceName, '-', '')}${uniqueString(resourceGroup().id, resourceName)}' - -resource acr 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = if (!empty(registries_sku)) { - name: acrName - location: location - sku: { - #disable-next-line BCP036 //Disabling validation of this parameter to cope with empty string to indicate no ACR required. - name: registries_sku - } - properties: { - policies: { - trustPolicy: enableACRTrustPolicy ? { - status: acrContentTrustEnabled - type: 'Notary' - } : {} - retentionPolicy: acrUntaggedRetentionPolicyEnabled ? { - status: 'enabled' - days: acrUntaggedRetentionPolicy - } : json('null') - } - publicNetworkAccess: privateLinks /* && empty(acrIPWhitelist)*/ ? 'Disabled' : 'Enabled' - zoneRedundancy: acrZoneRedundancyEnabled - adminUserEnabled: acrAdminUserEnabled - /* - networkRuleSet: { - defaultAction: 'Deny' - - ipRules: empty(acrIPWhitelist) ? [] : [ - { - action: 'Allow' - value: acrIPWhitelist - } - ] - virtualNetworkRules: [] - } - */ - } -} -output containerRegistryName string = !empty(registries_sku) ? acr.name : '' -output containerRegistryId string = !empty(registries_sku) ? acr.id : '' - -resource acrDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (createLaw && !empty(registries_sku)) { - name: 'acrDiags' - scope: acr - properties: { - workspaceId: aks_law.id - logs: [ - { - category: 'ContainerRegistryRepositoryEvents' - enabled: true - } - { - category: 'ContainerRegistryLoginEvents' - enabled: true - } - ] - metrics: [ - { - category: 'AllMetrics' - enabled: true - timeGrain: 'PT1M' - } - ] - } -} - -//resource acrPool 'Microsoft.ContainerRegistry/registries/agentPools@2019-06-01-preview' = if (custom_vnet && (!empty(registries_sku)) && privateLinks && acrPrivatePool) { -module acrPool 'acragentpool.bicep' = if (custom_vnet && (!empty(registries_sku)) && privateLinks && acrPrivatePool) { - name: 'acrprivatepool' - params: { - acrName: acr.name - acrPoolSubnetId: custom_vnet ? network.outputs.acrPoolSubnetId : '' - location: location - } -} - -var AcrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') -var KubeletObjectId = any(aks.properties.identityProfile.kubeletidentity).objectId - -resource aks_acr_pull 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(registries_sku)) { - scope: acr // Use when specifying a scope that is different than the deployment scope - name: guid(aks.id, 'Acr', AcrPullRole) - properties: { - roleDefinitionId: AcrPullRole - principalType: 'ServicePrincipal' - principalId: KubeletObjectId - } -} - -var AcrPushRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec') - -@description('The principal ID of the service principal to assign the push role to the ACR') -param acrPushRolePrincipalId string = '' - -resource aks_acr_push 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(registries_sku) && !empty(acrPushRolePrincipalId)) { - scope: acr // Use when specifying a scope that is different than the deployment scope - name: guid(aks.id, 'Acr', AcrPushRole) - properties: { - roleDefinitionId: AcrPushRole - principalType: automatedDeployment ? 'ServicePrincipal' : 'User' - principalId: acrPushRolePrincipalId - } -} - -param imageNames array = [] - -module acrImport 'br/public:deployment-scripts/import-acr:2.0.1' = if (!empty(registries_sku) && !empty(imageNames)) { - name: 'testAcrImportMulti' - params: { - acrName: acr.name - location: location - images: imageNames - } -} - -/*______ __ .______ _______ ____ __ ____ ___ __ __ -| ____|| | | _ \ | ____|\ \ / \ / / / \ | | | | -| |__ | | | |_) | | |__ \ \/ \/ / / ^ \ | | | | -| __| | | | / | __| \ / / /_\ \ | | | | -| | | | | |\ \----.| |____ \ /\ / / _____ \ | `----.| `----. -|__| |__| | _| `._____||_______| \__/ \__/ /__/ \__\ |_______||_______|*/ - -@description('Create an Azure Firewall, requires custom_vnet') -param azureFirewalls bool = false - -@description('Add application rules to the firewall for certificate management.') -param certManagerFW bool = false - -// @allowed([ -// 'AllowAllIn' -// 'AllowAcrSubnetIn' -// '' -// ]) -// @description('Allow Http traffic (80/443) into AKS from specific sources') -// param inboundHttpFW string = '' - -@allowed([ - 'Basic' - 'Premium' - 'Standard' -]) -param azureFirewallSku string = 'Standard' - -module firewall './firewall.bicep' = if (azureFirewalls && custom_vnet) { - name: 'firewall' - params: { - resourceName: resourceName - location: location - workspaceDiagsId: createLaw ? aks_law.id : '' - fwSubnetId: azureFirewalls && custom_vnet ? network.outputs.fwSubnetId : '' - fwSku: azureFirewallSku - fwManagementSubnetId: azureFirewalls && custom_vnet && azureFirewallSku == 'Basic' ? network.outputs.fwMgmtSubnetId : '' - vnetAksSubnetAddressPrefix: vnetAksSubnetAddressPrefix - certManagerFW: certManagerFW - appDnsZoneName: !empty(dnsZoneId) ? split(dnsZoneId, '/')[8] : '' - acrPrivatePool: acrPrivatePool - acrAgentPoolSubnetAddressPrefix: acrAgentPoolSubnetAddressPrefix - // inboundHttpFW: inboundHttpFW - availabilityZones: availabilityZones - } -} - -/* ___ .______ .______ _______ ____ __ ____ - / \ | _ \ | _ \ / _____|\ \ / \ / / - / ^ \ | |_) | | |_) | | | __ \ \/ \/ / - / /_\ \ | ___/ | ___/ | | |_ | \ / - / _____ \ | | | | __ | |__| | \ /\ / __ -/__/ \__\ | _| | _| (__) \______| \__/ \__/ (__)*/ - -@description('Create an Application Gateway') -param ingressApplicationGateway bool = false - -@description('The number of applciation gateway instances') -param appGWcount int = 2 - -@description('The maximum number of application gateway instances') -param appGWmaxCount int = 0 - -@maxLength(15) -@description('A known private ip in the Application Gateway subnet range to be allocated for internal traffic') -param privateIpApplicationGateway string = '' - -@description('Enable key vault integration for application gateway') -param appgwKVIntegration bool = false - -@allowed([ - 'Standard_v2' - 'WAF_v2' -]) -@description('The SKU for AppGw') -param appGWsku string = 'WAF_v2' - -@description('Enable the WAF Firewall, valid for WAF_v2 SKUs') -param appGWenableFirewall bool = true - -var deployAppGw = ingressApplicationGateway && (custom_vnet || !empty(byoAGWSubnetId)) -var appGWenableWafFirewall = appGWsku == 'Standard_v2' ? false : appGWenableFirewall - -// If integrating App Gateway with KeyVault, create a Identity App Gateway will use to access keyvault -// 'identity' is always created (adding: "|| deployAppGw") until this is fixed: -// https://github.com/Azure/bicep/issues/387#issuecomment-885671296 -resource appGwIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = if (deployAppGw) { - name: 'id-appgw-${resourceName}' - location: location -} - -var appgwName = 'agw-${resourceName}' -var appgwResourceId = deployAppGw ? resourceId('Microsoft.Network/applicationGateways', '${appgwName}') : '' - -resource appgwpip 'Microsoft.Network/publicIPAddresses@2021-02-01' = if (deployAppGw) { - name: 'pip-agw-${resourceName}' - location: location - sku: { - name: 'Standard' - } - zones: !empty(availabilityZones) ? availabilityZones : [] - properties: { - publicIPAllocationMethod: 'Static' - } -} - -var frontendPublicIpConfig = { - properties: { - publicIPAddress: { - id: appgwpip.id - } - } - name: 'appGatewayFrontendIP' -} - -var frontendPrivateIpConfig = { - properties: { - privateIPAllocationMethod: 'Static' - privateIPAddress: privateIpApplicationGateway - subnet: { - id: appGwSubnetId - } - } - name: 'appGatewayPrivateIP' -} - -@allowed([ - 'Prevention' - 'Detection' -]) -param appGwFirewallMode string = 'Prevention' - -var appGwFirewallConfigOwasp = { - enabled: appGWenableWafFirewall - firewallMode: appGwFirewallMode - ruleSetType: 'OWASP' - ruleSetVersion: '3.2' - requestBodyCheck: true - maxRequestBodySizeInKb: 128 - disabledRuleGroups: [] -} - -var appGWskuObj = union({ - name: appGWsku - tier: appGWsku - }, appGWmaxCount == 0 ? { - capacity: appGWcount - } : {}) - -// ugh, need to create a variable with the app gateway properies, because of the conditional key 'autoscaleConfiguration' -var appgwProperties = union({ - sku: appGWskuObj - sslPolicy: { - policyType: 'Predefined' - policyName: 'AppGwSslPolicy20170401S' - } - webApplicationFirewallConfiguration: appGWenableWafFirewall ? appGwFirewallConfigOwasp : json('null') - gatewayIPConfigurations: [ - { - name: 'besubnet' - properties: { - subnet: { - id: appGwSubnetId - } - } - } - ] - frontendIPConfigurations: empty(privateIpApplicationGateway) ? array(frontendPublicIpConfig) : concat(array(frontendPublicIpConfig), array(frontendPrivateIpConfig)) - frontendPorts: [ - { - name: 'appGatewayFrontendPort' - properties: { - port: 80 - } - } - ] - backendAddressPools: [ - { - name: 'defaultaddresspool' - } - ] - backendHttpSettingsCollection: [ - { - name: 'defaulthttpsetting' - properties: { - port: 80 - protocol: 'Http' - cookieBasedAffinity: 'Disabled' - requestTimeout: 30 - pickHostNameFromBackendAddress: true - } - } - ] - httpListeners: [ - { - name: 'hlisten' - properties: { - frontendIPConfiguration: { - id: empty(privateIpApplicationGateway) ? '${appgwResourceId}/frontendIPConfigurations/appGatewayFrontendIP' : '${appgwResourceId}/frontendIPConfigurations/appGatewayPrivateIP' - } - frontendPort: { - id: '${appgwResourceId}/frontendPorts/appGatewayFrontendPort' - } - protocol: 'Http' - } - } - ] - requestRoutingRules: [ - { - name: 'appGwRoutingRuleName' - properties: { - ruleType: 'Basic' - httpListener: { - id: '${appgwResourceId}/httpListeners/hlisten' - } - backendAddressPool: { - id: '${appgwResourceId}/backendAddressPools/defaultaddresspool' - } - backendHttpSettings: { - id: '${appgwResourceId}/backendHttpSettingsCollection/defaulthttpsetting' - } - } - } - ] - }, appGWmaxCount > 0 ? { - autoscaleConfiguration: { - minCapacity: appGWcount - maxCapacity: appGWmaxCount - } - } : {}) - -// 'identity' is always set until this is fixed: https://github.com/Azure/bicep/issues/387#issuecomment-885671296 -resource appgw 'Microsoft.Network/applicationGateways@2021-02-01' = if (deployAppGw) { - name: appgwName - location: location - zones: !empty(availabilityZones) ? availabilityZones : [] - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${appGwIdentity.id}': {} - } - } - properties: appgwProperties -} - -var contributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') -// https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal -// AGIC's identity requires "Contributor" permission over Application Gateway. -resource appGwAGICContrib 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (ingressApplicationGateway && deployAppGw) { - scope: appgw - name: guid(aks.id, 'Agic', contributor) - properties: { - roleDefinitionId: contributor - principalType: 'ServicePrincipal' - principalId: aks.properties.addonProfiles.ingressApplicationGateway.identity.objectId - } -} - -// AGIC's identity requires "Reader" permission over Application Gateway's resource group. -var reader = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') -resource appGwAGICRGReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (ingressApplicationGateway && deployAppGw) { - scope: resourceGroup() - name: guid(aks.id, 'Agic', reader) - properties: { - roleDefinitionId: reader - principalType: 'ServicePrincipal' - principalId: aks.properties.addonProfiles.ingressApplicationGateway.identity.objectId - } -} - -// AGIC's identity requires "Managed Identity Operator" permission over the user assigned identity of Application Gateway. -var managedIdentityOperator = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f1a07417-d97a-45cb-824c-7a7467783830') -resource appGwAGICMIOp 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (ingressApplicationGateway && deployAppGw) { - scope: appGwIdentity - name: guid(aks.id, 'Agic', managedIdentityOperator) - properties: { - roleDefinitionId: managedIdentityOperator - principalType: 'ServicePrincipal' - principalId: aks.properties.addonProfiles.ingressApplicationGateway.identity.objectId - } -} - -// AppGW Diagnostics -resource appgw_Diag 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (createLaw && deployAppGw) { - scope: appgw - name: 'appgwDiag' - properties: { - workspaceId: aks_law.id - logs: [ - { - category: 'ApplicationGatewayAccessLog' - enabled: true - } - { - category: 'ApplicationGatewayPerformanceLog' - enabled: true - } - { - category: 'ApplicationGatewayFirewallLog' - enabled: true - } - ] - } -} - -// ================================================= - -// Prevent error: AGIC Identity needs atleast has 'Contributor' access to Application Gateway and 'Reader' access to Application Gateway's Resource Group - -output ApplicationGatewayName string = deployAppGw ? appgw.name : '' - -/*_ ___ __ __ .______ _______ .______ .__ __. _______ .___________. _______ _______. -| |/ / | | | | | _ \ | ____|| _ \ | \ | | | ____|| || ____| / | -| ' / | | | | | |_) | | |__ | |_) | | \| | | |__ `---| |----`| |__ | (----` -| < | | | | | _ < | __| | / | . ` | | __| | | | __| \ \ -| . \ | `--' | | |_) | | |____ | |\ \----.| |\ | | |____ | | | |____ .----) | -|__|\__\ \______/ |______/ |_______|| _| `._____||__| \__| |_______| |__| |_______||_______/ */ - -@description('DNS prefix. Defaults to {resourceName}-dns') -param dnsPrefix string = '${resourceName}-dns' - -@description('Kubernetes Version') -param kubernetesVersion string = '1.23.12' - -@description('Enable Azure AD integration on AKS') -param enable_aad bool = false - -@description('The ID of the Azure AD tenant') -param aad_tenant_id string = '' - -@description('Create, and use a new Log Analytics workspace for AKS logs') -param omsagent bool = false - -@description('Enables the ContainerLogsV2 table to be of type Basic') -param containerLogsV2BasicLogs bool = false - -@description('Enable RBAC using AAD') -param enableAzureRBAC bool = false - -@description('Enables Kubernetes Event-driven Autoscaling (KEDA)') -param kedaAddon bool = false - -@description('Enables Open Service Mesh') -param openServiceMeshAddon bool = false - -@description('Enables the Blob CSI driver') -param blobCSIDriver bool = false - -@description('Enables the File CSI driver') -param fileCSIDriver bool = true - -@description('Enables the Disk CSI driver') -param diskCSIDriver bool = true - -@allowed([ - 'none' - 'patch' - 'stable' - 'rapid' - 'node-image' -]) -@description('AKS upgrade channel') -param upgradeChannel string = 'none' - -@allowed([ - 'Ephemeral' - 'Managed' -]) -@description('OS disk type') -param osDiskType string = 'Ephemeral' - -@description('VM SKU') -param agentVMSize string = 'Standard_DS3_v2' - -@description('Disk size in GB') -param osDiskSizeGB int = 0 - -@description('The number of agents for the user node pool') -param agentCount int = 3 - -@description('The maximum number of nodes for the user node pool') -param agentCountMax int = 0 -var autoScale = agentCountMax > agentCount - -@minLength(3) -@maxLength(12) -@description('Name for user node pool') -param nodePoolName string = 'npuser01' - -@description('Allocate pod ips dynamically') -param cniDynamicIpAllocation bool = false - -@minValue(10) -@maxValue(250) -@description('The maximum number of pods per node.') -param maxPods int = 30 - -@allowed([ - 'azure' - 'kubenet' -]) -@description('The network plugin type') -param networkPlugin string = 'azure' - -@allowed([ - '' - 'Overlay' -]) -@description('The network plugin type') -param networkPluginMode string = '' - -@allowed([ - '' - 'cilium' -]) -@description('Use Cilium dataplane (requires azure networkPlugin)') -param ebpfDataplane string = '' - -@allowed([ - '' - 'azure' - 'calico' -]) -@description('The network policy to use.') -param networkPolicy string = '' - -@allowed([ - '' - 'audit' - 'deny' -]) -@description('Enable the Azure Policy addon') -param azurepolicy string = '' - -@allowed([ - 'Baseline' - 'Restricted' -]) -param azurePolicyInitiative string = 'Baseline' - -@description('The IP addresses that are allowed to access the API server') -param authorizedIPRanges array = [] - -@description('Enable private cluster') -param enablePrivateCluster bool = false - -@allowed([ - 'system' - 'none' - 'privateDnsZone' -]) -@description('Private cluster dns advertisment method, leverages the dnsApiPrivateZoneId parameter') -param privateClusterDnsMethod string = 'system' - -@description('The full Azure resource ID of the privatelink DNS zone to use for the AKS cluster API Server') -param dnsApiPrivateZoneId string = '' - -@description('The zones to use for a node pool') -param availabilityZones array = [] - -@description('Disable local K8S accounts for AAD enabled clusters') -param AksDisableLocalAccounts bool = false - -@description('Use the paid sku for SLA rather than SLO') -param AksPaidSkuForSLA bool = false - -@minLength(9) -@maxLength(18) -@description('The address range to use for pods') -param podCidr string = '10.240.100.0/22' - -@minLength(9) -@maxLength(18) -@description('The address range to use for services') -param serviceCidr string = '172.10.0.0/16' - -@minLength(7) -@maxLength(15) -@description('The IP address to reserve for DNS') -param dnsServiceIP string = '172.10.0.10' - -@minLength(9) -@maxLength(18) -@description('The address range to use for the docker bridge') -param dockerBridgeCidr string = '172.17.0.1/16' - -@description('Enable Microsoft Defender for Containers (preview)') -param defenderForContainers bool = false - -@description('Only use the system node pool') -param JustUseSystemPool bool = false - -@allowed([ - 'CostOptimised' - 'Standard' - 'HighSpec' - 'Custom' -]) -@description('The System Pool Preset sizing') -param SystemPoolType string = 'CostOptimised' - -@description('A custom system pool spec') -param SystemPoolCustomPreset object = {} - -param AutoscaleProfile object = { - 'balance-similar-node-groups': 'true' - expander: 'random' - 'max-empty-bulk-delete': '10' - 'max-graceful-termination-sec': '600' - 'max-node-provision-time': '15m' - 'max-total-unready-percentage': '45' - 'new-pod-scale-up-delay': '0s' - 'ok-total-unready-count': '3' - 'scale-down-delay-after-add': '10m' - 'scale-down-delay-after-delete': '20s' - 'scale-down-delay-after-failure': '3m' - 'scale-down-unneeded-time': '10m' - 'scale-down-unready-time': '20m' - 'scale-down-utilization-threshold': '0.5' - 'scan-interval': '10s' - 'skip-nodes-with-local-storage': 'true' - 'skip-nodes-with-system-pods': 'true' -} - -@allowed([ - 'loadBalancer' - 'managedNATGateway' - 'userAssignedNATGateway' -]) -@description('Outbound traffic type for the egress traffic of your cluster') -param aksOutboundTrafficType string = 'loadBalancer' - -@description('Create a new Nat Gateway, applies to custom networking only') -param createNatGateway bool = false - -@minValue(1) -@maxValue(16) -@description('The effective outbound IP resources of the cluster NAT gateway') -param natGwIpCount int = 2 - -@minValue(4) -@maxValue(120) -@description('Outbound flow idle timeout in minutes for NatGw') -param natGwIdleTimeout int = 30 - -@description('Configures the cluster as an OIDC issuer for use with Workload Identity') -param oidcIssuer bool = false - -@description('Installs Azure Workload Identity into the cluster') -param workloadIdentity bool = false - -@description('Assign a public IP per node for user node pools') -param enableNodePublicIP bool = false - -param warIngressNginx bool = false - -@description('System Pool presets are derived from the recommended system pool specs') -var systemPoolPresets = { - CostOptimised: { - vmSize: 'Standard_B4ms' - count: 1 - minCount: 1 - maxCount: 3 - enableAutoScaling: true - availabilityZones: [] - } - Standard: { - vmSize: 'Standard_DS2_v2' - count: 3 - minCount: 3 - maxCount: 5 - enableAutoScaling: true - availabilityZones: [ - '1' - '2' - '3' - ] - } - HighSpec: { - vmSize: 'Standard_D4s_v3' - count: 3 - minCount: 3 - maxCount: 5 - enableAutoScaling: true - availabilityZones: [ - '1' - '2' - '3' - ] - } -} - -var systemPoolBase = { - name: JustUseSystemPool ? nodePoolName : 'npsystem' - vmSize: agentVMSize - count: agentCount - mode: 'System' - osType: 'Linux' - maxPods: 30 - type: 'VirtualMachineScaleSets' - vnetSubnetID: !empty(aksSubnetId) ? aksSubnetId : json('null') - upgradeSettings: { - maxSurge: '33%' - } - nodeTaints: [ - JustUseSystemPool ? '' : 'CriticalAddonsOnly=true:NoSchedule' - ] -} - -var agentPoolProfiles = JustUseSystemPool ? array(systemPoolBase) : concat(array(union(systemPoolBase, SystemPoolType == 'Custom' && SystemPoolCustomPreset != {} ? SystemPoolCustomPreset : systemPoolPresets[SystemPoolType]))) - -output userNodePoolName string = nodePoolName -output systemNodePoolName string = JustUseSystemPool ? nodePoolName : 'npsystem' - -var akssku = AksPaidSkuForSLA ? 'Paid' : 'Free' - -var aks_addons = union({ - azurepolicy: { - config: { - version: !empty(azurepolicy) ? 'v2' : json('null') - } - enabled: !empty(azurepolicy) - } - azureKeyvaultSecretsProvider: { - config: { - enableSecretRotation: 'true' - rotationPollInterval: keyVaultAksCSIPollInterval - } - enabled: keyVaultAksCSI - } - openServiceMesh: { - enabled: openServiceMeshAddon - config: {} - } - }, createLaw && omsagent ? { - omsagent: { - enabled: createLaw && omsagent - config: { - logAnalyticsWorkspaceResourceID: createLaw && omsagent ? aks_law.id : json('null') - } - } } : {}) - -var aks_addons1 = ingressApplicationGateway ? union(aks_addons, deployAppGw ? { - ingressApplicationGateway: { - config: { - applicationGatewayId: appgw.id - } - enabled: true - } - } : { - // AKS RP will deploy the AppGateway for us (not creating custom or BYO VNET) - ingressApplicationGateway: { - enabled: true - config: { - applicationGatewayName: appgwName - subnetCIDR: '10.225.0.0/16' - } - } - }) : aks_addons - -var aks_identity = { - type: 'UserAssigned' - userAssignedIdentities: { - '${aksUai.id}': {} - } -} - -@description('Sets the private dns zone id if provided') -var aksPrivateDnsZone = privateClusterDnsMethod == 'privateDnsZone' ? (!empty(dnsApiPrivateZoneId) ? dnsApiPrivateZoneId : 'system') : privateClusterDnsMethod -output aksPrivateDnsZone string = aksPrivateDnsZone - -@description('Needing to seperately declare and union this because of https://github.com/Azure/AKS-Construction/issues/344') -var managedNATGatewayProfile = { - natGatewayProfile: { - managedOutboundIPProfile: { - count: natGwIpCount - } - idleTimeoutInMinutes: natGwIdleTimeout - } -} - -@description('Needing to seperately declare and union this because of https://github.com/Azure/AKS/issues/2774') -var azureDefenderSecurityProfile = { - securityProfile: { - defender: { - logAnalyticsWorkspaceResourceId: createLaw ? aks_law.id : null - securityMonitoring: { - enabled: defenderForContainers - } - } - } -} - -var aksProperties = union({ - kubernetesVersion: kubernetesVersion - enableRBAC: true - dnsPrefix: dnsPrefix - aadProfile: enable_aad ? { - managed: true - enableAzureRBAC: enableAzureRBAC - tenantID: aad_tenant_id - } : null - apiServerAccessProfile: !empty(authorizedIPRanges) ? { - authorizedIPRanges: authorizedIPRanges - } : { - enablePrivateCluster: enablePrivateCluster - privateDNSZone: enablePrivateCluster ? aksPrivateDnsZone : '' - enablePrivateClusterPublicFQDN: enablePrivateCluster && privateClusterDnsMethod == 'none' - } - agentPoolProfiles: agentPoolProfiles - workloadAutoScalerProfile: { - keda: { - enabled: kedaAddon - } - } - networkProfile: { - loadBalancerSku: 'standard' - networkPlugin: networkPlugin - #disable-next-line BCP036 //Disabling validation of this parameter to cope with empty string to indicate no Network Policy required. - networkPolicy: networkPolicy - networkPluginMode: networkPlugin == 'azure' ? networkPluginMode : '' - podCidr: networkPlugin == 'kubenet' || cniDynamicIpAllocation ? podCidr : json('null') - serviceCidr: serviceCidr - dnsServiceIP: dnsServiceIP - dockerBridgeCidr: dockerBridgeCidr - outboundType: aksOutboundTrafficType - ebpfDataplane: networkPlugin == 'azure' ? ebpfDataplane : '' - } - disableLocalAccounts: AksDisableLocalAccounts && enable_aad - autoUpgradeProfile: { upgradeChannel: upgradeChannel } - addonProfiles: !empty(aks_addons1) ? aks_addons1 : aks_addons - autoScalerProfile: autoScale ? AutoscaleProfile : {} - oidcIssuerProfile: { - enabled: oidcIssuer - } - securityProfile: { - workloadIdentity: { - enabled: workloadIdentity - } - } - ingressProfile: { - webAppRouting: { - enabled: warIngressNginx - } - } - storageProfile: { - blobCSIDriver: { - enabled: blobCSIDriver - } - diskCSIDriver: { - enabled: diskCSIDriver - } - fileCSIDriver: { - enabled: fileCSIDriver - } - } - }, - aksOutboundTrafficType == 'managedNATGateway' ? managedNATGatewayProfile : {}, - defenderForContainers && createLaw ? azureDefenderSecurityProfile : {}, - keyVaultKmsCreateAndPrereqs || !empty(keyVaultKmsByoKeyId) ? azureKeyVaultKms : {} -) - -resource aks 'Microsoft.ContainerService/managedClusters@2022-10-02-preview' = { - name: 'aks-${resourceName}' - location: location - properties: aksProperties - identity: createAksUai ? aks_identity : { - type: 'SystemAssigned' - } - sku: { - name: 'Basic' - tier: akssku - } - dependsOn: [ - privateDnsZoneRbac - waitForKmsRbac - ] -} -output aksClusterName string = aks.name -output aksOidcIssuerUrl string = oidcIssuer ? aks.properties.oidcIssuerProfile.issuerURL : '' - -@description('The AKS cluster identity') -output aksClusterIdentity object = { - clientId: aks.properties.identityProfile.kubeletidentity.clientId - objectId: aks.properties.identityProfile.kubeletidentity.objectId - resourceId: aks.properties.identityProfile.kubeletidentity.resourceId -} - -@allowed([ 'Linux', 'Windows' ]) -@description('The User Node pool OS') -param osType string = 'Linux' - -@allowed([ 'Ubuntu', 'Windows2019', 'Windows2022' ]) -@description('The User Node pool OS SKU') -param osSKU string = 'Ubuntu' - -var poolName = osType == 'Linux' ? nodePoolName : take(nodePoolName, 6) - -module userNodePool 'aksagentpool.bicep' = if (!JustUseSystemPool) { - name: 'userNodePool' - params: { - AksName: aks.name - PoolName: poolName - subnetId: aksSubnetId - agentCount: agentCount - agentCountMax: agentCountMax - agentVMSize: agentVMSize - maxPods: maxPods - osDiskType: osDiskType - osType: osType - osSKU: osSKU - enableNodePublicIP: enableNodePublicIP - osDiskSizeGB: osDiskSizeGB - } -} - -@description('This output can be directly leveraged when creating a ManagedId Federated Identity') -output aksOidcFedIdentityProperties object = { - issuer: oidcIssuer ? aks.properties.oidcIssuerProfile.issuerURL : '' - audiences: [ 'api://AzureADTokenExchange' ] - subject: 'system:serviceaccount:ns:svcaccount' -} - -@description('The name of the managed resource group AKS uses') -output aksNodeResourceGroup string = aks.properties.nodeResourceGroup - -@description('The Azure resource id for the AKS cluster') -output aksResourceId string = aks.id - -//output aksNodePools array = [for nodepool in agentPoolProfiles: name] - -@description('Not giving Rbac at the vnet level when using private dns results in ReconcilePrivateDNS. Therefore we need to upgrade the scope when private dns is being used, because it wants to set up the dns->vnet integration.') -var uaiNetworkScopeRbac = enablePrivateCluster && !empty(dnsApiPrivateZoneId) ? 'Vnet' : 'Subnet' -module privateDnsZoneRbac './dnsZoneRbac.bicep' = if (enablePrivateCluster && !empty(dnsApiPrivateZoneId) && createAksUai) { - name: 'addPrivateK8sApiDnsContributor' - params: { - vnetId: '' - dnsZoneId: dnsApiPrivateZoneId - principalId: createAksUai ? aksUai.properties.principalId : '' - } -} - -var policySetBaseline = '/providers/Microsoft.Authorization/policySetDefinitions/a8640138-9b0a-4a28-b8cb-1666c838647d' -var policySetRestrictive = '/providers/Microsoft.Authorization/policySetDefinitions/42b8ef37-b724-4e24-bbc8-7a7708edfe00' - -resource aks_policies 'Microsoft.Authorization/policyAssignments@2020-09-01' = if (!empty(azurepolicy)) { - name: '${resourceName}-${azurePolicyInitiative}' - location: location - properties: { - policyDefinitionId: azurePolicyInitiative == 'Baseline' ? policySetBaseline : policySetRestrictive - parameters: { - excludedNamespaces: { - value: [ - 'kube-system' - 'gatekeeper-system' - 'azure-arc' - 'cluster-baseline-setting' - ] - } - effect: { - value: azurepolicy - } - } - metadata: { - assignedBy: 'Aks Construction' - } - displayName: 'Kubernetes cluster pod security ${azurePolicyInitiative} standards for Linux-based workloads' - description: 'As per: https://github.com/Azure/azure-policy/blob/master/built-in-policies/policySetDefinitions/Kubernetes/' - } -} - -@description('If automated deployment, for the 3 automated user assignments, set Principal Type on each to "ServicePrincipal" rarter than "User"') -param automatedDeployment bool = false - -@description('The principal ID to assign the AKS admin role.') -param adminPrincipalId string = '' -// for AAD Integrated Cluster wusing 'enableAzureRBAC', add Cluster admin to the current user! -var buildInAKSRBACClusterAdmin = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b1ff04bb-8a4e-4dc4-8eb5-8693973ce19b') -resource aks_admin_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableAzureRBAC && !empty(adminPrincipalId)) { - scope: aks // Use when specifying a scope that is different than the deployment scope - name: guid(aks.id, 'aksadmin', buildInAKSRBACClusterAdmin) - properties: { - roleDefinitionId: buildInAKSRBACClusterAdmin - principalType: automatedDeployment ? 'ServicePrincipal' : 'User' - principalId: adminPrincipalId - } -} - -param fluxGitOpsAddon bool = false - -resource fluxAddon 'Microsoft.KubernetesConfiguration/extensions@2022-04-02-preview' = if (fluxGitOpsAddon) { - name: 'flux' - scope: aks - properties: { - extensionType: 'microsoft.flux' - autoUpgradeMinorVersion: true - releaseTrain: 'Stable' - scope: { - cluster: { - releaseNamespace: 'flux-system' - } - } - configurationProtectedSettings: {} - } - dependsOn: [ daprExtension ] //Chaining dependencies because of: https://github.com/Azure/AKS-Construction/issues/385 -} -output fluxReleaseNamespace string = fluxGitOpsAddon ? fluxAddon.properties.scope.cluster.releaseNamespace : '' - -@description('Add the Dapr extension') -param daprAddon bool = false -@description('Enable high availability (HA) mode for the Dapr control plane') -param daprAddonHA bool = false - -resource daprExtension 'Microsoft.KubernetesConfiguration/extensions@2022-04-02-preview' = if (daprAddon) { - name: 'dapr' - scope: aks - properties: { - extensionType: 'Microsoft.Dapr' - autoUpgradeMinorVersion: true - releaseTrain: 'Stable' - configurationSettings: { - 'global.ha.enabled': '${daprAddonHA}' - } - scope: { - cluster: { - releaseNamespace: 'dapr-system' - } - } - configurationProtectedSettings: {} - } -} - -output daprReleaseNamespace string = daprAddon ? daprExtension.properties.scope.cluster.releaseNamespace : '' - -/*__ ___. ______ .__ __. __ .___________. ______ .______ __ .__ __. _______ -| \/ | / __ \ | \ | | | | | | / __ \ | _ \ | | | \ | | / _____| -| \ / | | | | | | \| | | | `---| |----`| | | | | |_) | | | | \| | | | __ -| |\/| | | | | | | . ` | | | | | | | | | | / | | | . ` | | | |_ | -| | | | | `--' | | |\ | | | | | | `--' | | |\ \----.| | | |\ | | |__| | -|__| |__| \______/ |__| \__| |__| |__| \______/ | _| `._____||__| |__| \__| \______| */ - -@description('Diagnostic categories to log') -param AksDiagCategories array = [ - 'cluster-autoscaler' - 'kube-controller-manager' - 'kube-audit-admin' - 'guard' -] - -resource AksDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (createLaw && omsagent) { - name: 'aksDiags' - scope: aks - properties: { - workspaceId: aks_law.id - logs: [for aksDiagCategory in AksDiagCategories: { - category: aksDiagCategory - enabled: true - }] - metrics: [ - { - category: 'AllMetrics' - enabled: true - } - ] - } -} - -@description('Enable Metric Alerts') -param createAksMetricAlerts bool = true - -@allowed([ - 'Short' - 'Long' -]) -@description('Which Metric polling frequency model to use') -param AksMetricAlertMetricFrequencyModel string = 'Long' - -var AlertFrequencyLookup = { - Short: { - evalFrequency: 'PT1M' - windowSize: 'PT5M' - } - Long: { - evalFrequency: 'PT15M' - windowSize: 'PT1H' - } -} -var AlertFrequency = AlertFrequencyLookup[AksMetricAlertMetricFrequencyModel] - -module aksmetricalerts './aksmetricalerts.bicep' = if (createLaw) { - name: 'aksmetricalerts' - scope: resourceGroup() - params: { - clusterName: aks.name - logAnalyticsWorkspaceName: aks_law.name - metricAlertsEnabled: createAksMetricAlerts - evalFrequency: AlertFrequency.evalFrequency - windowSize: AlertFrequency.windowSize - alertSeverity: 'Informational' - logAnalyticsWorkspaceLocation: location - } -} - -//---------------------------------------------------------------------------------- Container Insights - -@description('The Log Analytics retention period') -param retentionInDays int = 30 - -@description('The Log Analytics daily data cap (GB) (0=no limit)') -param logDataCap int = 0 - -var aks_law_name = 'log-${resourceName}' - -var createLaw = (omsagent || deployAppGw || azureFirewalls || CreateNetworkSecurityGroups || defenderForContainers) - -resource aks_law 'Microsoft.OperationalInsights/workspaces@2022-10-01' = if (createLaw) { - name: aks_law_name - location: location - properties: union({ - retentionInDays: retentionInDays - }, - logDataCap > 0 ? { workspaceCapping: { - dailyQuotaGb: logDataCap - } } : {} - ) -} - -resource containerLogsV2_Basiclogs 'Microsoft.OperationalInsights/workspaces/tables@2022-10-01' = if (containerLogsV2BasicLogs) { - name: '${aks_law_name}/ContainerLogV2' - properties: { - plan: 'Basic' - } - dependsOn: [ - aks - ] -} - -//This role assignment enables AKS->LA Fast Alerting experience -var MonitoringMetricsPublisherRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3913510d-42f4-4e42-8a64-420c390055eb') -resource FastAlertingRole_Aks_Law 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (omsagent) { - scope: aks - name: guid(aks.id, 'omsagent', MonitoringMetricsPublisherRole) - properties: { - roleDefinitionId: MonitoringMetricsPublisherRole - principalId: aks.properties.addonProfiles.omsagent.identity.objectId - principalType: 'ServicePrincipal' - } -} - -output LogAnalyticsName string = (createLaw) ? aks_law.name : '' -output LogAnalyticsGuid string = (createLaw) ? aks_law.properties.customerId : '' -output LogAnalyticsId string = (createLaw) ? aks_law.id : '' - -//---------------------------------------------------------------------------------- AKS events with Event Grid -// Supported events : https://docs.microsoft.com/en-gb/azure/event-grid/event-schema-aks?tabs=event-grid-event-schema#available-event-types - -@description('Create an Event Grid System Topic for AKS events') -param createEventGrid bool = false - -resource eventGrid 'Microsoft.EventGrid/systemTopics@2021-12-01' = if (createEventGrid) { - name: 'evgt-${aks.name}' - location: location - identity: { - type: 'SystemAssigned' - } - properties: { - source: aks.id - topicType: 'Microsoft.ContainerService.ManagedClusters' - } -} - -output eventGridName string = createEventGrid ? eventGrid.name : '' - -resource eventGridDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (createLaw && createEventGrid) { - name: 'eventGridDiags' - scope: eventGrid - properties: { - workspaceId: aks_law.id - logs: [ - { - category: 'DeliveryFailures' - enabled: true - } - ] - metrics: [ - { - category: 'AllMetrics' - enabled: true - } - ] - } -} - -@description('Enable usage and telemetry feedback to Microsoft.') -param enableTelemetry bool = true - -var telemetryId = '3c1e2fc6-1c4b-44f9-8694-25d00ae30a3a-${location}' - -/*.___________. _______ __ _______ .___ ___. _______ .___________..______ ____ ____ _______ _______ .______ __ ______ ____ ____ .___ ___. _______ .__ __. .___________. -| || ____|| | | ____|| \/ | | ____|| || _ \ \ \ / / | \ | ____|| _ \ | | / __ \ \ \ / / | \/ | | ____|| \ | | | | -`---| |----`| |__ | | | |__ | \ / | | |__ `---| |----`| |_) | \ \/ / | .--. || |__ | |_) | | | | | | | \ \/ / | \ / | | |__ | \| | `---| |----` - | | | __| | | | __| | |\/| | | __| | | | / \_ _/ | | | || __| | ___/ | | | | | | \_ _/ | |\/| | | __| | . ` | | | - | | | |____ | `----.| |____ | | | | | |____ | | | |\ \----. | | | '--' || |____ | | | `----.| `--' | | | | | | | | |____ | |\ | | | - |__| |_______||_______||_______||__| |__| |_______| |__| | _| `._____| |__| |_______/ |_______|| _| |_______| \______/ |__| |__| |__| |_______||__| \__| |__| */ - -// Telemetry Deployment -resource telemetrydeployment 'Microsoft.Resources/deployments@2021-04-01' = if (enableTelemetry) { - name: telemetryId - properties: { - mode: 'Incremental' - template: { - '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' - contentVersion: '1.0.0.0' - resources: {} - } - } -} - -//ACSCII Art link : https://textkool.com/en/ascii-art-generator?hl=default&vl=default&font=Star%20Wars&text=changeme diff --git a/templates/common/infra/bicep/core/host/aks/network.bicep b/templates/common/infra/bicep/core/host/aks/network.bicep deleted file mode 100644 index 0ec70091a08..00000000000 --- a/templates/common/infra/bicep/core/host/aks/network.bicep +++ /dev/null @@ -1,506 +0,0 @@ -param resourceName string -param location string = resourceGroup().location - -param networkPluginIsKubenet bool = false -param aksPrincipleId string = '' - -param vnetAddressPrefix string -param vnetAksSubnetAddressPrefix string - -//Nsg -param workspaceName string = '' -param workspaceResourceGroupName string = '' -param networkSecurityGroups bool = true - -//Firewall -param azureFirewalls bool = false -param azureFirewallsSku string = 'Basic' -param azureFirewallsManagementSeperation bool = azureFirewalls && azureFirewallsSku=='Basic' -param vnetFirewallSubnetAddressPrefix string = '' -param vnetFirewallManagementSubnetAddressPrefix string = '' - -//Ingress -param ingressApplicationGateway bool = false -param ingressApplicationGatewayPublic bool = false -param vnetAppGatewaySubnetAddressPrefix string ='' - -//Private Link -param privateLinks bool = false -param privateLinkSubnetAddressPrefix string = '' -param privateLinkAcrId string = '' -param privateLinkAkvId string = '' - -//ACR -param acrPrivatePool bool = false -param acrAgentPoolSubnetAddressPrefix string = '' - -//NatGatewayEgress -param natGateway bool = false -param natGatewayPublicIps int = 2 -param natGatewayIdleTimeoutMins int = 30 - -//Bastion -param bastion bool =false -param bastionSubnetAddressPrefix string = '' - -@description('Used by the Bastion Public IP') -param availabilityZones array = [] - - -var bastion_subnet_name = 'AzureBastionSubnet' -var bastion_baseSubnet = { - name: bastion_subnet_name - properties: { - addressPrefix: bastionSubnetAddressPrefix - } -} -var bastion_subnet = bastion && networkSecurityGroups ? union(bastion_baseSubnet, nsgBastion.outputs.nsgSubnetObj) : bastion_baseSubnet - -var acrpool_subnet_name = 'acrpool-sn' -var acrpool_baseSubnet = { - name: acrpool_subnet_name - properties: { - addressPrefix: acrAgentPoolSubnetAddressPrefix - } -} -var acrpool_subnet = privateLinks && networkSecurityGroups ? union(acrpool_baseSubnet, nsgAcrPool.outputs.nsgSubnetObj) : acrpool_baseSubnet - -var private_link_subnet_name = 'privatelinks-sn' -var private_link_baseSubnet = { - name: private_link_subnet_name - properties: { - addressPrefix: privateLinkSubnetAddressPrefix - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Enabled' - } -} -var private_link_subnet = privateLinks && networkSecurityGroups ? union(private_link_baseSubnet, nsgPrivateLinks.outputs.nsgSubnetObj) : private_link_baseSubnet - - -var appgw_subnet_name = 'appgw-sn' -var appgw_baseSubnet = { - name: appgw_subnet_name - properties: { - addressPrefix: vnetAppGatewaySubnetAddressPrefix - } -} -var appgw_subnet = ingressApplicationGateway && networkSecurityGroups ? union(appgw_baseSubnet, nsgAppGw.outputs.nsgSubnetObj) : appgw_baseSubnet - -var fw_subnet_name = 'AzureFirewallSubnet' // Required by FW -var fw_subnet = { - name: fw_subnet_name - properties: { - addressPrefix: vnetFirewallSubnetAddressPrefix - } -} - -/// ---- Firewall VNET config -module calcAzFwIp './calcAzFwIp.bicep' = if (azureFirewalls) { - name: 'calcAzFwIp' - params: { - vnetFirewallSubnetAddressPrefix: vnetFirewallSubnetAddressPrefix - } -} - -var fwmgmt_subnet_name = 'AzureFirewallManagementSubnet' // Required by FW -var fwmgmt_subnet = { - name: fwmgmt_subnet_name - properties: { - addressPrefix: vnetFirewallManagementSubnetAddressPrefix - } -} - -var routeFwTableName = 'rt-afw-${resourceName}' -resource vnet_udr 'Microsoft.Network/routeTables@2022-07-01' = if (azureFirewalls) { - name: routeFwTableName - location: location - properties: { - routes: [ - { - name: 'AKSNodesEgress' - properties: { - addressPrefix: '0.0.0.0/1' - nextHopType: 'VirtualAppliance' - nextHopIpAddress: azureFirewalls ? calcAzFwIp.outputs.FirewallPrivateIp : null - } - } - ] - } -} - -var contributorRoleId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') - -@description('Required for kubenet networking.') -resource vnet_udr_rbac 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (azureFirewalls && !empty(aksPrincipleId) && networkPluginIsKubenet) { - scope: vnet_udr - name: guid(vnet_udr.id, aksPrincipleId, contributorRoleId) - properties: { - principalId: aksPrincipleId - roleDefinitionId: contributorRoleId - principalType: 'ServicePrincipal' - } -} - -var aks_subnet_name = 'aks-sn' -var aks_baseSubnet = { - name: aks_subnet_name - properties: union({ - addressPrefix: vnetAksSubnetAddressPrefix - }, privateLinks ? { - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Enabled' - } : {}, natGateway ? { - natGateway: { - id: natGw.id - } - } : {}, azureFirewalls ? { - routeTable: { - id: vnet_udr.id //resourceId('Microsoft.Network/routeTables', routeFwTableName) - } - }: {}) -} - -var aks_subnet = networkSecurityGroups ? union(aks_baseSubnet, nsgAks.outputs.nsgSubnetObj) : aks_baseSubnet - -var subnets = union( - array(aks_subnet), - azureFirewalls ? array(fw_subnet) : [], - privateLinks ? array(private_link_subnet) : [], - acrPrivatePool ? array(acrpool_subnet) : [], - bastion ? array(bastion_subnet) : [], - ingressApplicationGateway ? array(appgw_subnet) : [], - azureFirewallsManagementSeperation ? array(fwmgmt_subnet) : [] -) -output debugSubnets array = subnets - -var vnetName = 'vnet-${resourceName}' -resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = { - name: vnetName - location: location - properties: { - addressSpace: { - addressPrefixes: [ - vnetAddressPrefix - ] - } - subnets: subnets - } -} -output vnetId string = vnet.id -output vnetName string = vnet.name -output aksSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, aks_subnet_name) -output fwSubnetId string = azureFirewalls ? '${vnet.id}/subnets/${fw_subnet_name}' : '' -output fwMgmtSubnetId string = azureFirewalls ? '${vnet.id}/subnets/${fwmgmt_subnet_name}' : '' -output acrPoolSubnetId string = acrPrivatePool ? '${vnet.id}/subnets/${acrpool_subnet_name}' : '' -output appGwSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, appgw_subnet_name) -output privateLinkSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, private_link_subnet_name) - -module aks_vnet_con 'networksubnetrbac.bicep' = if (!empty(aksPrincipleId)) { - name: '${resourceName}-subnetRbac' - params: { - servicePrincipalId: aksPrincipleId - subnetName: aks_subnet_name - vnetName: vnet.name - } -} - -/* -------------------------------------------------------------------------- Private Link for ACR */ -var privateLinkAcrName = 'pl-acr-${resourceName}' -resource privateLinkAcr 'Microsoft.Network/privateEndpoints@2021-08-01' = if (!empty(privateLinkAcrId)) { - name: privateLinkAcrName - location: location - properties: { - customNetworkInterfaceName: 'nic-${privateLinkAcrName}' - privateLinkServiceConnections: [ - { - name: 'Acr-Connection' - properties: { - privateLinkServiceId: privateLinkAcrId - groupIds: [ - 'registry' - ] - } - } - ] - subnet: { - id: '${vnet.id}/subnets/${private_link_subnet_name}' - } - } -} - -resource privateDnsAcr 'Microsoft.Network/privateDnsZones@2020-06-01' = if (!empty(privateLinkAcrId)) { - name: 'privatelink.azurecr.io' - location: 'global' -} - -var privateDnsAcrLinkName = 'vnet-dnscr-${resourceName}' -resource privateDnsAcrLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (!empty(privateLinkAcrId)) { - parent: privateDnsAcr - name: privateDnsAcrLinkName - location: 'global' - properties: { - registrationEnabled: false - virtualNetwork: { - id: vnet.id - } - } -} - -resource privateDnsAcrZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2021-08-01' = if (!empty(privateLinkAcrId)) { - parent: privateLinkAcr - name: 'default' - properties: { - privateDnsZoneConfigs: [ - { - name: 'vnet-pl-acr' - properties: { - privateDnsZoneId: privateDnsAcr.id - } - } - ] - } -} - - -/* -------------------------------------------------------------------------- Private Link for KeyVault */ -var privateLinkAkvName = 'pl-akv-${resourceName}' -resource privateLinkAkv 'Microsoft.Network/privateEndpoints@2021-08-01' = if (!empty(privateLinkAkvId)) { - name: privateLinkAkvName - location: location - properties: { - customNetworkInterfaceName: 'nic-${privateLinkAkvName}' - privateLinkServiceConnections: [ - { - name: 'Akv-Connection' - properties: { - privateLinkServiceId: privateLinkAkvId - groupIds: [ - 'vault' - ] - } - } - ] - subnet: { - id: '${vnet.id}/subnets/${private_link_subnet_name}' - } - } -} - -resource privateDnsAkv 'Microsoft.Network/privateDnsZones@2020-06-01' = if (!empty(privateLinkAkvId)) { - name: 'privatelink.vaultcore.azure.net' - location: 'global' -} - -var privateDnsAkvLinkName = 'vnet-dnscr-${resourceName}' -resource privateDnsAkvLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (!empty(privateLinkAkvId)) { - parent: privateDnsAkv - name: privateDnsAkvLinkName - location: 'global' - properties: { - registrationEnabled: false - virtualNetwork: { - id: vnet.id - } - } -} - -resource privateDnsAkvZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2021-08-01' = if (!empty(privateLinkAkvId)) { - parent: privateLinkAkv - name: 'default' - properties: { - privateDnsZoneConfigs: [ - { - name: 'vnet-pl-akv' - properties: { - privateDnsZoneId: privateDnsAkv.id - } - } - ] - } -} - -param bastionHostName string = 'bas-${resourceName}' -var publicIpAddressName = 'pip-${bastionHostName}' - -@allowed([ - 'Standard' - 'Basic' -]) -param bastionSku string = 'Standard' - -resource bastionPip 'Microsoft.Network/publicIPAddresses@2021-03-01' = if(bastion) { - name: publicIpAddressName - location: location - sku: { - name: 'Standard' - } - zones: !empty(availabilityZones) ? availabilityZones : [] - properties: { - publicIPAllocationMethod: 'Static' - } -} - -resource bastionHost 'Microsoft.Network/bastionHosts@2021-05-01' = if(bastion) { - name: bastionHostName - location: location - sku: { - name: bastionSku - } - properties: { - enableTunneling: true - ipConfigurations: [ - { - name: 'IpConf' - properties: { - subnet: { - id: '${vnet.id}/subnets/${bastion_subnet_name}' - } - publicIPAddress: { - id: bastionPip.id - } - } - } - ] - } -} - -resource log 'Microsoft.OperationalInsights/workspaces@2021-06-01' existing = if(networkSecurityGroups && !empty(workspaceName)) { - name: workspaceName - scope: resourceGroup(workspaceResourceGroupName) -} - -param CreateNsgFlowLogs bool = false - -var flowLogStorageRawName = replace(toLower('stflow${resourceName}${uniqueString(resourceGroup().id, resourceName)}'),'-','') -var flowLogStorageName = length(flowLogStorageRawName) > 24 ? substring(flowLogStorageRawName, 0, 24) : flowLogStorageRawName -resource flowLogStor 'Microsoft.Storage/storageAccounts@2021-08-01' = if(CreateNsgFlowLogs && networkSecurityGroups) { - name: flowLogStorageName - kind: 'StorageV2' - sku: { - name: 'Standard_LRS' - } - location: location - properties: { - minimumTlsVersion: 'TLS1_2' - } -} - -//NSG's -module nsgAks 'nsg.bicep' = if(networkSecurityGroups) { - name: 'nsgAks' - params: { - location: location - resourceName: '${aks_subnet_name}-${resourceName}' - workspaceId: !empty(workspaceName) ? log.properties.customerId : '' - workspaceRegion: !empty(workspaceName) ? log.location : '' - workspaceResourceId: !empty(workspaceName) ? log.id : '' - ruleInAllowInternetHttp: true - ruleInAllowInternetHttps: true - ruleInDenySsh: true - FlowLogStorageAccountId: CreateNsgFlowLogs ? flowLogStor.id : '' - } -} - -module nsgAcrPool 'nsg.bicep' = if(acrPrivatePool && networkSecurityGroups) { - name: 'nsgAcrPool' - params: { - location: location - resourceName: '${acrpool_subnet_name}-${resourceName}' - workspaceId: !empty(workspaceName) ? log.properties.customerId : '' - workspaceRegion: !empty(workspaceName) ? log.location : '' - workspaceResourceId: !empty(workspaceName) ? log.id : '' - FlowLogStorageAccountId: CreateNsgFlowLogs ? flowLogStor.id : '' - } - dependsOn: [ - nsgAks - ] -} - -module nsgAppGw 'nsg.bicep' = if(ingressApplicationGateway && networkSecurityGroups) { - name: 'nsgAppGw' - params: { - location: location - resourceName: '${appgw_subnet_name}-${resourceName}' - workspaceId: !empty(workspaceName) ? log.properties.customerId : '' - workspaceRegion: !empty(workspaceName) ? log.location : '' - workspaceResourceId: !empty(workspaceName) ? log.id : '' - ruleInAllowInternetHttp: ingressApplicationGatewayPublic - ruleInAllowInternetHttps: ingressApplicationGatewayPublic - ruleInAllowGwManagement: true - ruleInAllowAzureLoadBalancer: true - ruleInDenyInternet: true - ruleInGwManagementPort: '65200-65535' - FlowLogStorageAccountId: CreateNsgFlowLogs ? flowLogStor.id : '' - } - dependsOn: [ - nsgAcrPool - ] -} - -module nsgBastion 'nsg.bicep' = if(bastion && networkSecurityGroups) { - name: 'nsgBastion' - params: { - location: location - resourceName: '${bastion_subnet_name}-${resourceName}' - workspaceId: !empty(workspaceName) ? log.properties.customerId : '' - workspaceRegion: !empty(workspaceName) ? log.location : '' - workspaceResourceId: !empty(workspaceName) ? log.id : '' - ruleInAllowBastionHostComms: true - ruleInAllowInternetHttps: true - ruleInAllowGwManagement: true - ruleInAllowAzureLoadBalancer: true - ruleOutAllowBastionComms: true - ruleInGwManagementPort: '443' - FlowLogStorageAccountId: CreateNsgFlowLogs ? flowLogStor.id : '' - } - dependsOn: [ - nsgAppGw - ] -} - -module nsgPrivateLinks 'nsg.bicep' = if(privateLinks && networkSecurityGroups) { - name: 'nsgPrivateLinks' - params: { - location: location - resourceName: '${private_link_subnet_name}-${resourceName}' - workspaceId: !empty(workspaceName) ? log.properties.customerId : '' - workspaceRegion: !empty(workspaceName) ? log.location : '' - workspaceResourceId: !empty(workspaceName) ? log.id : '' - FlowLogStorageAccountId: CreateNsgFlowLogs ? flowLogStor.id : '' - } - dependsOn: [ - nsgBastion - ] -} - -resource natGwIp 'Microsoft.Network/publicIPAddresses@2021-08-01' = [for i in range(0, natGatewayPublicIps): if(natGateway) { - name: 'pip-${natGwName}-${i+1}' - location: location - sku: { - name: 'Standard' - } - zones: !empty(availabilityZones) ? availabilityZones : [] - properties: { - publicIPAllocationMethod: 'Static' - } -}] - -var natGwName = 'ng-${resourceName}' -resource natGw 'Microsoft.Network/natGateways@2021-08-01' = if(natGateway) { - name: natGwName - location: location - sku: { - name: 'Standard' - } - zones: !empty(availabilityZones) ? availabilityZones : [] - properties: { - publicIpAddresses: [for i in range(0, natGatewayPublicIps): { - id: natGwIp[i].id - }] - idleTimeoutInMinutes: natGatewayIdleTimeoutMins - } - dependsOn: [ - natGwIp - ] -} - diff --git a/templates/common/infra/bicep/core/host/aks/networksubnetrbac.bicep b/templates/common/infra/bicep/core/host/aks/networksubnetrbac.bicep deleted file mode 100644 index b19cfef527a..00000000000 --- a/templates/common/infra/bicep/core/host/aks/networksubnetrbac.bicep +++ /dev/null @@ -1,19 +0,0 @@ -param vnetName string -param subnetName string -param servicePrincipalId string - -var networkContributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7') - -resource subnet 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' existing = { - name: '${vnetName}/${subnetName}' -} - -resource aks_vnet_cont 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(subnet.id, servicePrincipalId, networkContributorRole) - scope: subnet - properties: { - roleDefinitionId: networkContributorRole - principalId: servicePrincipalId - principalType: 'ServicePrincipal' - } -} diff --git a/templates/common/infra/bicep/core/host/aks/networkwatcherflowlog.bicep b/templates/common/infra/bicep/core/host/aks/networkwatcherflowlog.bicep deleted file mode 100644 index 8c77e80d9dd..00000000000 --- a/templates/common/infra/bicep/core/host/aks/networkwatcherflowlog.bicep +++ /dev/null @@ -1,46 +0,0 @@ -param name string -param location string = resourceGroup().location -param nsgId string -param storageId string -param trafficAnalytics bool -param trafficAnalyticsInterval int = 60 - -@description('The resource guid of the attached workspace.') -param workspaceId string = '' - -@description('Resource Id of the attached workspace.') -param workspaceResourceId string = '' -param workspaceRegion string = resourceGroup().location - -resource networkWatcher 'Microsoft.Network/networkWatchers@2022-01-01' = { - name: 'NetworkWatcher_${location}' - location: location - properties: {} -} - -resource nsgFlowLogs 'Microsoft.Network/networkWatchers/flowLogs@2021-05-01' = { - name: '${networkWatcher.name}/${name}' - location: location - properties: { - targetResourceId: nsgId - storageId: storageId - enabled: true - retentionPolicy: { - days: 2 - enabled: true - } - format: { - type: 'JSON' - version: 2 - } - flowAnalyticsConfiguration: { - networkWatcherFlowAnalyticsConfiguration: { - enabled: trafficAnalytics - workspaceId: workspaceId - trafficAnalyticsInterval: trafficAnalyticsInterval - workspaceRegion: workspaceRegion - workspaceResourceId: workspaceResourceId - } - } - } -} diff --git a/templates/common/infra/bicep/core/host/aks/nsg.bicep b/templates/common/infra/bicep/core/host/aks/nsg.bicep deleted file mode 100644 index e90e57f5d2d..00000000000 --- a/templates/common/infra/bicep/core/host/aks/nsg.bicep +++ /dev/null @@ -1,283 +0,0 @@ -param resourceName string -param location string = resourceGroup().location -param workspaceId string = '' -param workspaceResourceId string = '' -param workspaceRegion string = resourceGroup().location - -var nsgName = 'nsg-${resourceName}' - -resource nsg 'Microsoft.Network/networkSecurityGroups@2021-05-01' = { - name: nsgName - location: location -} -output nsgId string = nsg.id - -param ruleInAllowGwManagement bool = false -param ruleInGwManagementPort string = '443,65200-65535' -resource ruleAppGwManagement 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInAllowGwManagement) { - parent: nsg - name: 'Allow_AppGatewayManagement' - properties: { - protocol: '*' - sourcePortRange: '*' - destinationPortRange: ruleInGwManagementPort - sourceAddressPrefix: 'GatewayManager' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 110 - direction: 'Inbound' - } -} - -param ruleInAllowAzureLoadBalancer bool = false -resource ruleAzureLoadBalancer 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if (ruleInAllowAzureLoadBalancer) { - parent: nsg - name: 'Allow_AzureLoadBalancer' - properties: { - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefix: 'AzureLoadBalancer' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 120 - direction: 'Inbound' - sourcePortRanges: [] - destinationPortRanges: [] - sourceAddressPrefixes: [] - destinationAddressPrefixes: [] - } -} - -param ruleInDenyInternet bool = false -resource ruleDenyInternet 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInDenyInternet) { - parent: nsg - name: 'Deny_AllInboundInternet' - properties: { - description: 'Azure infrastructure communication' - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefix: 'Internet' - destinationAddressPrefix: '*' - access: 'Deny' - priority: 4096 - direction: 'Inbound' - sourcePortRanges: [] - destinationPortRanges: [] - sourceAddressPrefixes: [] - destinationAddressPrefixes: [] - } -} - -param ruleInAllowInternetHttp bool = false -resource ruleInternetHttp 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInAllowInternetHttp) { - parent: nsg - name: 'Allow_Internet_Http' - properties: { - protocol: 'Tcp' - sourcePortRange: '*' - sourceAddressPrefix: 'Internet' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 200 - direction: 'Inbound' - sourcePortRanges: [] - destinationPortRanges: [ - '80' - ] - sourceAddressPrefixes: [] - destinationAddressPrefixes: [] - } -} - -param ruleInAllowInternetHttps bool = false -resource ruleInternetHttps 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInAllowInternetHttps) { - parent: nsg - name: 'Allow_Internet_Https' - properties: { - protocol: 'Tcp' - sourcePortRange: '*' - sourceAddressPrefix: 'Internet' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 210 - direction: 'Inbound' - sourcePortRanges: [] - destinationPortRanges: [ - '443' - ] - sourceAddressPrefixes: [] - destinationAddressPrefixes: [] - } -} - -param ruleInAllowBastionHostComms bool = false -resource ruleBastionHost 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInAllowBastionHostComms) { - parent: nsg - name: 'Allow_Bastion_Host_Communication' - properties: { - protocol: '*' - sourcePortRange: '*' - sourceAddressPrefix: 'VirtualNetwork' - destinationAddressPrefix: 'VirtualNetwork' - access: 'Allow' - priority: 700 - direction: 'Inbound' - sourcePortRanges: [] - destinationPortRanges: [ - '8080' - '5701' - ] - sourceAddressPrefixes: [] - destinationAddressPrefixes: [] - } -} - -param ruleOutAllowBastionComms bool = false -resource ruleBastionEgressSshRdp 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleOutAllowBastionComms) { - parent: nsg - name: 'Allow_SshRdp_Outbound' - properties: { - protocol: '*' - sourcePortRange: '*' - sourceAddressPrefix: '*' - destinationAddressPrefix: 'VirtualNetwork' - access: 'Allow' - priority: 200 - direction: 'Outbound' - sourcePortRanges: [] - destinationPortRanges: [ - '22' - '3389' - ] - sourceAddressPrefixes: [] - destinationAddressPrefixes: [] - } -} - -resource ruleBastionEgressAzure 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleOutAllowBastionComms) { - parent: nsg - name: 'Allow_Azure_Cloud_Outbound' - properties: { - protocol: 'Tcp' - sourcePortRange: '*' - sourceAddressPrefix: '*' - destinationAddressPrefix: 'AzureCloud' - access: 'Allow' - priority: 210 - direction: 'Outbound' - sourcePortRanges: [] - destinationPortRanges: [ - '443' - ] - sourceAddressPrefixes: [] - destinationAddressPrefixes: [] - } -} - -resource ruleBastionEgressBastionComms 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleOutAllowBastionComms) { - parent: nsg - name: 'Allow_Bastion_Communication' - properties: { - protocol: '*' - sourcePortRange: '*' - sourceAddressPrefix: 'VirtualNetwork' - destinationAddressPrefix: 'VirtualNetwork' - access: 'Allow' - priority: 220 - direction: 'Outbound' - sourcePortRanges: [] - destinationPortRanges: [ - '8080' - '5701' - ] - sourceAddressPrefixes: [] - destinationAddressPrefixes: [] - } -} - -resource ruleBastionEgressSessionInfo 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleOutAllowBastionComms) { - parent: nsg - name: 'Allow_Get_Session_Info' - properties: { - protocol: '*' - sourcePortRange: '*' - sourceAddressPrefix: '*' - destinationAddressPrefix: 'Internet' - access: 'Allow' - priority: 230 - direction: 'Outbound' - sourcePortRanges: [] - destinationPortRanges: [ - '80' - ] - sourceAddressPrefixes: [] - destinationAddressPrefixes: [] - } -} - -param ruleInDenySsh bool = false -resource ruleSshIngressDeny 'Microsoft.Network/networkSecurityGroups/securityRules@2020-11-01' = if(ruleInDenySsh) { - parent: nsg - name: 'DenySshInbound' - properties: { - protocol: '*' - sourcePortRange: '*' - sourceAddressPrefix: '*' - destinationAddressPrefix: '*' - access: 'Deny' - priority: 100 - direction: 'Inbound' - sourcePortRanges: [] - destinationPortRanges: [ - '22' - ] - sourceAddressPrefixes: [] - destinationAddressPrefixes: [] - } -} - -param NsgDiagnosticCategories array = [ - 'NetworkSecurityGroupEvent' - 'NetworkSecurityGroupRuleCounter' -] - -resource nsgDiags 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceResourceId)) { - name: 'diags-${nsgName}' - scope: nsg - properties: { - workspaceId: workspaceResourceId - logs: [for diagCategory in NsgDiagnosticCategories: { - category: diagCategory - enabled: true - }] - } -} - -//If multiple NSG's are trying to add flow logs at the same, this will result in CONFLICT: AnotherOperationInProgress -//Therefore advised to create FlowLogs outside of NSG creation, to better coordinate the creation - or sequence the NSG creation with DependsOn -param FlowLogStorageAccountId string = '' -param FlowLogTrafficAnalytics bool = !empty(FlowLogStorageAccountId) -module nsgFlow 'networkwatcherflowlog.bicep' = if(!empty(FlowLogStorageAccountId)) { - name: 'flow-${nsgName}' - scope: resourceGroup('NetworkWatcherRG') - params: { - location:location - name: 'flowNsg-${nsgName}' - nsgId: nsg.id - storageId: FlowLogStorageAccountId - trafficAnalytics: FlowLogTrafficAnalytics - workspaceId: workspaceId - workspaceResourceId: workspaceResourceId - workspaceRegion: workspaceRegion - } -} - -output nsgSubnetObj object = { - properties: { - networkSecurityGroup: { - id: nsg.id - } - } -} diff --git a/templates/common/infra/bicep/core/host/container-registry.bicep b/templates/common/infra/bicep/core/host/container-registry.bicep index 01c32139795..6dcd8f0bcb1 100644 --- a/templates/common/infra/bicep/core/host/container-registry.bicep +++ b/templates/common/infra/bicep/core/host/container-registry.bicep @@ -15,6 +15,9 @@ param sku object = { } param zoneRedundancy string = 'Disabled' +@description('The log analytics workspace id used for logging & monitoring') +param workspaceId string = '' + // 2022-02-01-preview needed for anonymousPullEnabled resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = { name: name @@ -32,5 +35,31 @@ resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-pr } } +resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { + name: 'registry-diagnostics' + tags: tags + scope: containerRegistry + properties: { + workspaceId: workspaceId + logs: [ + { + category: 'ContainerRegistryRepositoryEvents' + enabled: true + } + { + category: 'ContainerRegistryLoginEvents' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + timeGrain: 'PT1M' + } + ] + } +} + output loginServer string = containerRegistry.properties.loginServer output name string = containerRegistry.name diff --git a/templates/common/infra/bicep/core/host/managed-cluster.bicep b/templates/common/infra/bicep/core/host/managed-cluster.bicep new file mode 100644 index 00000000000..1fc3400a4be --- /dev/null +++ b/templates/common/infra/bicep/core/host/managed-cluster.bicep @@ -0,0 +1,134 @@ +@description('The name for the AKS managed cluster') +param name string + +@description('The name of the resource group for the managed resources of the AKS cluster') +param nodeResourceGroupName string = '' + +@description('The Azure region/location for the AKS resources') +param location string = resourceGroup().location + +@description('Custom tags to apply to the AKS resources') +param tags object = {} + +@description('Kubernetes Version') +param kubernetesVersion string = '1.23.12' + +@description('Whether RBAC is enabled for local accounts') +param enableRbac bool = true + +// Add-ons +@description('Whether web app routing (preview) add-on is enabled') +param webAppRoutingAddon bool = true + +// AAD Integration +@description('Enable Azure Active Directory integration') +param enableAad bool = false + +@description('Enable RBAC using AAD') +param enableAzureRbac bool = false + +@description('The Tenant ID associated to the Azure Active Directory') +param aadTenantId string = '' + +@description('The load balancer SKU to use for ingress into the AKS cluster') +@allowed([ 'basic', 'standard' ]) +param loadBalancerSku string = 'standard' + +@description('Network plugin used for building the Kubernetes network.') +@allowed([ 'azure', 'kubenet', 'none' ]) +param networkPlugin string = 'azure' + +@description('Network policy used for building the Kubernetes network.') +@allowed([ 'azure', 'calico' ]) +param networkPolicy string = 'azure' + +@description('If set to true, getting static credentials will be disabled for this cluster.') +param disableLocalAccounts bool = false + +@description('The managed cluster SKU.') +@allowed([ 'Paid', 'Free' ]) +param sku string = 'Free' + +@description('Configuration of AKS add-ons') +param addOns object = {} + +@description('The log analytics workspace id used for logging & monitoring') +param workspaceId string = '' + +@description('The node pool configuration for the System agent pool') +param systemPoolConfig object + +resource aks 'Microsoft.ContainerService/managedClusters@2022-11-02-preview' = { + name: name + location: location + tags: tags + identity: { + type: 'SystemAssigned' + } + sku: { + name: 'Basic' + tier: sku + } + properties: { + nodeResourceGroup: !empty(nodeResourceGroupName) ? nodeResourceGroupName : 'rg-mc-${name}' + kubernetesVersion: kubernetesVersion + dnsPrefix: '${name}-dns' + enableRBAC: enableRbac + aadProfile: enableAad ? { + managed: true + enableAzureRBAC: enableAzureRbac + tenantID: aadTenantId + } : null + agentPoolProfiles: [ + systemPoolConfig + ] + networkProfile: { + loadBalancerSku: loadBalancerSku + networkPlugin: networkPlugin + networkPolicy: networkPolicy + } + disableLocalAccounts: disableLocalAccounts && enableAad + addonProfiles: addOns + ingressProfile: { + webAppRouting: { + enabled: webAppRoutingAddon + } + } + } +} + +var aksDiagCategories = [ + 'cluster-autoscaler' + 'kube-controller-manager' + 'kube-audit-admin' + 'guard' +] + +resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { + name: 'aks-diagnostics' + tags: tags + scope: aks + properties: { + workspaceId: workspaceId + logs: [for category in aksDiagCategories: { + category: category + enabled: true + }] + metrics: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } +} + +@description('The resource name of the AKS cluster') +output aksClusterName string = aks.name + +@description('The AKS cluster identity') +output aksClusterIdentity object = { + clientId: aks.properties.identityProfile.kubeletidentity.clientId + objectId: aks.properties.identityProfile.kubeletidentity.objectId + resourceId: aks.properties.identityProfile.kubeletidentity.resourceId +} diff --git a/templates/common/infra/bicep/core/security/registry-access.bicep b/templates/common/infra/bicep/core/security/registry-access.bicep new file mode 100644 index 00000000000..621bc80e278 --- /dev/null +++ b/templates/common/infra/bicep/core/security/registry-access.bicep @@ -0,0 +1,18 @@ +param containerRegistryName string +param principalId string + +var AcrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + +resource aks_acr_pull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: containerRegistry // Use when specifying a scope that is different than the deployment scope + name: guid(principalId, 'Acr', AcrPullRole) + properties: { + roleDefinitionId: AcrPullRole + principalType: 'ServicePrincipal' + principalId: principalId + } +} + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { + name: containerRegistryName +} From 4b17f5149c6d5926b2005671230f26f994cf7985 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 22 Feb 2023 16:12:16 -0800 Subject: [PATCH 12/25] Updates AKS project main bicep --- .../.repo/bicep/infra/main.bicep | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep index c31d4832249..d8019f3e8e2 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep @@ -13,6 +13,12 @@ param location string // "resourceGroupName": { // "value": "myGroupName" // } +@description('The resource name of the AKS cluster') +param clusterName string = '' + +@description('The resource name of the Container Registry (ACR)') +param containerRegistryName string = '' + param applicationInsightsDashboardName string = '' param applicationInsightsName string = '' param cosmosAccountName string = '' @@ -35,27 +41,17 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { tags: tags } -module cluster '../../../../../../common/infra/bicep/core/host/aks/main.bicep' = { +// The AKS cluster to host applications +module cluster '../../../../../../common/infra/bicep/core/host/aks.bicep' = { name: 'aks' scope: rg params: { location: location - resourceName: resourceToken - upgradeChannel: 'stable' - warIngressNginx: true - adminPrincipalId: principalId - acrPushRolePrincipalId: principalId - registries_sku: 'Standard' - } -} - -// Give the AKS Cluster access to KeyVault -module clusterKeyVaultAccess '../../../../../../common/infra/bicep/core/security/keyvault-access.bicep' = { - name: 'cluster-keyvault-access' - scope: rg - params: { + name: !empty(clusterName) ? clusterName : '${abbrs.containerServiceManagedClusters}${resourceToken}' + agentPoolType: 'Standard' + containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' + logAnalyticsName: monitoring.outputs.logAnalyticsWorkspaceName keyVaultName: keyVault.outputs.name - principalId: cluster.outputs.aksClusterIdentity.objectId } } From e421535c5c14077a4552e93fef5fc306d40ea798 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 22 Feb 2023 16:16:10 -0800 Subject: [PATCH 13/25] Updates AKS project main bicep --- .../todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep index d8019f3e8e2..7ef3bfbc390 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep @@ -105,7 +105,7 @@ output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId output AZURE_AKS_CLUSTER_NAME string = cluster.outputs.aksClusterName output AZURE_AKS_IDENTITY_CLIENT_ID string = cluster.outputs.aksClusterIdentity.clientId -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = '${cluster.outputs.containerRegistryName}.azurecr.io' +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = cluster.outputs.containerRegistryLoginServer output AZURE_CONTAINER_REGISTRY_NAME string = cluster.outputs.containerRegistryName output REACT_APP_API_BASE_URL string = '' output REACT_APP_APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString From 408adbd8eb631779dda6842bf195e93322f0355a Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 22 Feb 2023 16:26:45 -0800 Subject: [PATCH 14/25] Updates spelling linter dictionary --- templates/cspell-templates.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/cspell-templates.txt b/templates/cspell-templates.txt index 9792d2a6dc3..403d7afed3a 100644 --- a/templates/cspell-templates.txt +++ b/templates/cspell-templates.txt @@ -54,6 +54,7 @@ uuidv venv virtuals VSIX +webapprouting webfonts webui wwwroot From a5e2afa2f820a4fb6dc5a939ca1a7fc36162ab0e Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 24 Feb 2023 12:15:08 -0800 Subject: [PATCH 15/25] Adds support for waiting for deployment & ingress to complete --- cli/azd/pkg/project/service_config.go | 2 + cli/azd/pkg/project/service_target_aks.go | 278 ++++++++++++++++++++-- cli/azd/pkg/tools/kubectl/kube_config.go | 38 --- cli/azd/pkg/tools/kubectl/kubectl.go | 9 +- cli/azd/pkg/tools/kubectl/models.go | 152 +++++++++++- cli/azd/pkg/tools/kubectl/util.go | 148 ++++++++++++ schemas/v1.0/azure.yaml.json | 3 +- 7 files changed, 556 insertions(+), 74 deletions(-) create mode 100644 cli/azd/pkg/tools/kubectl/util.go diff --git a/cli/azd/pkg/project/service_config.go b/cli/azd/pkg/project/service_config.go index fa1ccc568af..eeba705b81e 100644 --- a/cli/azd/pkg/project/service_config.go +++ b/cli/azd/pkg/project/service_config.go @@ -40,6 +40,8 @@ type ServiceConfig struct { Module string `yaml:"module"` // The optional docker options Docker DockerProjectOptions `yaml:"docker"` + // The optional K8S / AKS options + K8s AksOptions `yaml:"k8s"` // The infrastructure provisioning configuration Infra provisioning.Options `yaml:"infra"` // Hook configuration for service diff --git a/cli/azd/pkg/project/service_target_aks.go b/cli/azd/pkg/project/service_target_aks.go index d8bfa43a56d..53787d2d1c1 100644 --- a/cli/azd/pkg/project/service_target_aks.go +++ b/cli/azd/pkg/project/service_target_aks.go @@ -2,10 +2,12 @@ package project import ( "context" + "errors" "fmt" "log" + "net/url" "path/filepath" - "time" + "strings" "github.com/azure/azure-dev/cli/azd/pkg/azure" "github.com/azure/azure-dev/cli/azd/pkg/environment" @@ -14,8 +16,29 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/tools/azcli" "github.com/azure/azure-dev/cli/azd/pkg/tools/docker" "github.com/azure/azure-dev/cli/azd/pkg/tools/kubectl" + "github.com/benbjohnson/clock" ) +type AksOptions struct { + Namespace string `yaml:"namespace"` + Ingress AksIngressOptions `yaml:"ingress"` + Deployment AksDeploymentOptions `yaml:"deployment"` + Service AksServiceOptions `yaml:"service"` +} + +type AksIngressOptions struct { + Name string `yaml:"name"` + RelativePath string `yaml:"relativePath"` +} + +type AksDeploymentOptions struct { + Name string `yaml:"name"` +} + +type AksServiceOptions struct { + Name string `yaml:"name"` +} + type aksTarget struct { config *ServiceConfig env *environment.Environment @@ -24,6 +47,7 @@ type aksTarget struct { az azcli.AzCli docker docker.Docker kubectl kubectl.KubectlCli + clock clock.Clock } func (t *aksTarget) RequiredExternalTools() []tools.ExternalTool { @@ -37,7 +61,7 @@ func (t *aksTarget) Deploy( progress chan<- string, ) (ServiceDeploymentResult, error) { // Login to AKS cluster - namespace := t.config.Project.Name + namespace := t.getK8sNamespace() clusterName, has := t.env.Values[environment.AksClusterEnvVarName] if !has { return ServiceDeploymentResult{}, fmt.Errorf( @@ -123,29 +147,37 @@ func (t *aksTarget) Deploy( return ServiceDeploymentResult{}, fmt.Errorf("logging into registry '%s': %w", loginServer, err) } - resourceName := t.scope.ResourceName() - if resourceName == "" { - resourceName = t.config.Name + imageTag, err := t.generateImageTag() + if err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("generating image tag: %w", err) + } + + fullTag := fmt.Sprintf( + "%s/%s", + loginServer, + imageTag, + ) + + // Tag image. + log.Printf("tagging image %s as %s", path, fullTag) + progress <- "Tagging image" + if err := t.docker.Tag(ctx, t.config.Path(), path, fullTag); err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("tagging image: %w", err) } - tags := []string{ - fmt.Sprintf("%s/%s/%s:azdev-deploy-%d", loginServer, t.config.Project.Name, resourceName, time.Now().Unix()), - fmt.Sprintf("%s/%s/%s:latest", loginServer, t.config.Project.Name, resourceName), + log.Printf("pushing %s to registry", fullTag) + + // Push image. + progress <- "Pushing container image" + if err := t.docker.Push(ctx, t.config.Path(), fullTag); err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("pushing image: %w", err) } - for _, tag := range tags { - // Tag image. - log.Printf("tagging image %s as %s", path, tag) - progress <- "Tagging image" - if err := t.docker.Tag(ctx, t.config.Path(), path, tag); err != nil { - return ServiceDeploymentResult{}, fmt.Errorf("tagging image: %w", err) - } + // Save the name of the image we pushed into the environment with a well known key. + t.env.SetServiceProperty(t.config.Name, "IMAGE_NAME", fullTag) - // Push image. - progress <- "Pushing container image" - if err := t.docker.Push(ctx, t.config.Path(), tag); err != nil { - return ServiceDeploymentResult{}, fmt.Errorf("pushing image: %w", err) - } + if err := t.env.Save(); err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("saving image name to environment: %w", err) } progress <- "Applying k8s manifests" @@ -159,6 +191,18 @@ func (t *aksTarget) Deploy( return ServiceDeploymentResult{}, fmt.Errorf("failed applying kube manifests: %w", err) } + deploymentName := t.config.K8s.Deployment.Name + if deploymentName == "" { + deploymentName = t.config.Name + } + + // It is not a requirement for a AZD deploy to contain a deployment object + // If we don't find any deployment within the namespace we will continue + deployment, err := t.waitForDeployment(ctx, namespace, deploymentName) + if err != nil && !errors.Is(err, kubectl.ErrResourceNotFound) { + return ServiceDeploymentResult{}, err + } + endpoints, err := t.Endpoints(ctx) if err != nil { return ServiceDeploymentResult{}, err @@ -171,14 +215,201 @@ func (t *aksTarget) Deploy( t.scope.ResourceName(), ), Kind: ContainerAppTarget, - Details: nil, + Details: deployment, Endpoints: endpoints, }, nil } func (t *aksTarget) Endpoints(ctx context.Context) ([]string, error) { - // TODO Update - return []string{"https://aks.azure.com/sample"}, nil + namespace := t.getK8sNamespace() + + serviceName := t.config.K8s.Service.Name + if serviceName == "" { + serviceName = t.config.Name + } + + ingressName := t.config.K8s.Service.Name + if ingressName == "" { + ingressName = t.config.Name + } + + // Find endpoints for any matching services + // These endpoints would typically be internal cluster accessible endpoints + serviceEndpoints, err := t.getServiceEndpoints(ctx, namespace, serviceName) + if err != nil && !errors.Is(err, kubectl.ErrResourceNotFound) { + return nil, fmt.Errorf("failed retrieving service endpoints, %w", err) + } + + // Find endpoints for any matching ingress controllers + // These endpoints would typically be publicly accessible endpoints + ingressEndpoints, err := t.getIngressEndpoints(ctx, namespace, ingressName) + if err != nil && !errors.Is(err, kubectl.ErrResourceNotFound) { + return nil, fmt.Errorf("failed retrieving ingress endpoints, %w", err) + } + + endpoints := append(serviceEndpoints, ingressEndpoints...) + + return endpoints, nil +} + +// Finds a deployment using the specified deploymentNameFilter string +// Waits until the deployment rollout is complete nad all replicas are accessible +func (t *aksTarget) waitForDeployment( + ctx context.Context, + namespace string, + deploymentNameFilter string, +) (*kubectl.Deployment, error) { + return kubectl.WaitForResource( + ctx, t.kubectl, namespace, kubectl.ResourceTypeDeployment, + func(deployment *kubectl.Deployment) bool { + return strings.Contains(deployment.Metadata.Name, deploymentNameFilter) + }, + func(deployment *kubectl.Deployment) bool { + return deployment.Status.AvailableReplicas == deployment.Spec.Replicas + }, + ) +} + +// Finds an ingress using the specified ingressNameFilter string +// Waits until the ingress LoadBalancer has assigned a valid IP address +func (t *aksTarget) waitForIngress( + ctx context.Context, + namespace string, + ingressNameFilter string, +) (*kubectl.Ingress, error) { + return kubectl.WaitForResource( + ctx, t.kubectl, namespace, kubectl.ResourceTypeIngress, + func(ingress *kubectl.Ingress) bool { + return strings.Contains(ingress.Metadata.Name, ingressNameFilter) + }, + func(ingress *kubectl.Ingress) bool { + var ipAddress string + for _, config := range ingress.Status.LoadBalancer.Ingress { + if config.Ip != "" { + ipAddress = config.Ip + break + } + } + + return ipAddress != "" + }, + ) +} + +// Finds a service using the specified serviceNameFilter string +// Waits until the service is available +func (t *aksTarget) waitForService( + ctx context.Context, + namespace string, + serviceNameFilter string, +) (*kubectl.Service, error) { + return kubectl.WaitForResource( + ctx, t.kubectl, namespace, kubectl.ResourceTypeService, + func(service *kubectl.Service) bool { + return strings.Contains(service.Metadata.Name, serviceNameFilter) + }, + func(service *kubectl.Service) bool { + // If the service is not a load balancer it should be immediately available + if service.Spec.Type != kubectl.ServiceTypeLoadBalancer { + return true + } + + // Load balancer can take some time to be provision by AKS + var ipAddress string + for _, config := range service.Status.LoadBalancer.Ingress { + if config.Ip != "" { + ipAddress = config.Ip + break + } + } + + return ipAddress != "" + }, + ) +} + +// Retrieve any service endpoints for the specified namespace and serviceNameFilter +// Supports service types for LoadBalancer and ClusterIP +func (t *aksTarget) getServiceEndpoints(ctx context.Context, namespace string, serviceNameFilter string) ([]string, error) { + service, err := t.waitForService(ctx, namespace, serviceNameFilter) + if err != nil { + return nil, err + } + + var endpoints []string + if service.Spec.Type == kubectl.ServiceTypeLoadBalancer { + for _, resource := range service.Status.LoadBalancer.Ingress { + endpoints = append(endpoints, fmt.Sprintf("http://%s (Service, Type: LoadBalancer)", resource.Ip)) + } + } else if service.Spec.Type == kubectl.ServiceTypeClusterIp { + for index, ip := range service.Spec.ClusterIps { + endpoints = append(endpoints, fmt.Sprintf("http://%s:%d (Service, Type: ClusterIP)", ip, service.Spec.Ports[index].Port)) + } + } + + return endpoints, nil +} + +// Retrieve any ingress endpoints for the specified namespace and serviceNameFilter +// Supports service types for LoadBalancer, supports Hosts and/or IP address +func (t *aksTarget) getIngressEndpoints(ctx context.Context, namespace string, resourceFilter string) ([]string, error) { + ingress, err := t.waitForIngress(ctx, namespace, resourceFilter) + if err != nil { + return nil, err + } + + var endpoints []string + var protocol string + if ingress.Spec.Tls == nil { + protocol = "http" + } else { + protocol = "https" + } + + for index, resource := range ingress.Status.LoadBalancer.Ingress { + var baseUrl string + if ingress.Spec.Rules[index].Host == nil { + baseUrl = fmt.Sprintf("%s://%s", protocol, resource.Ip) + } else { + baseUrl = fmt.Sprintf("%s://%s", *ingress.Spec.Rules[index].Host, resource.Ip) + } + + endpointUrl, err := url.JoinPath(baseUrl, t.config.K8s.Ingress.RelativePath) + if err != nil { + return nil, fmt.Errorf("failed constructing service endpoints, %w", err) + } + + endpoints = append(endpoints, fmt.Sprintf("%s (Ingress, Type: LoadBalancer)", endpointUrl)) + } + + return endpoints, nil +} + +func (t *aksTarget) generateImageTag() (string, error) { + configuredTag, err := t.config.Docker.Tag.Envsubst(t.env.Getenv) + if err != nil { + return "", err + } + + if configuredTag != "" { + return configuredTag, nil + } + + return fmt.Sprintf("%s/%s-%s:azdev-deploy-%d", + strings.ToLower(t.config.Project.Name), + strings.ToLower(t.config.Name), + strings.ToLower(t.env.GetEnvName()), + t.clock.Now().Unix(), + ), nil +} + +func (t *aksTarget) getK8sNamespace() string { + namespace := t.config.K8s.Namespace + if namespace == "" { + namespace = t.config.Project.Name + } + + return namespace } func NewAksTarget( @@ -198,5 +429,6 @@ func NewAksTarget( containerService: containerService, docker: docker, kubectl: kubectlCli, + clock: clock.New(), } } diff --git a/cli/azd/pkg/tools/kubectl/kube_config.go b/cli/azd/pkg/tools/kubectl/kube_config.go index 04de12dfe38..bed4f2bedd3 100644 --- a/cli/azd/pkg/tools/kubectl/kube_config.go +++ b/cli/azd/pkg/tools/kubectl/kube_config.go @@ -11,44 +11,6 @@ import ( "gopkg.in/yaml.v3" ) -type KubeConfig struct { - ApiVersion string `yaml:"apiVersion"` - Clusters []*KubeCluster `yaml:"clusters"` - Contexts []*KubeContext `yaml:"contexts"` - Users []*KubeUser `yaml:"users"` - Kind string `yaml:"kind"` - CurrentContext string `yaml:"current-context"` - Preferences KubePreferences `yaml:"preferences"` -} - -type KubeCluster struct { - Name string `yaml:"name"` - Cluster KubeClusterData `yaml:"cluster"` -} - -type KubeClusterData struct { - CertificateAuthorityData string `yaml:"certificate-authority-data"` - Server string `yaml:"server"` -} - -type KubeContext struct { - Name string `yaml:"name"` - Context KubeContextData `yaml:"context"` -} - -type KubeContextData struct { - Cluster string `yaml:"cluster"` - User string `yaml:"user"` -} - -type KubeUser struct { - Name string `yaml:"name"` - KubeUserData KubeUserData `yaml:"user"` -} - -type KubeUserData map[string]any -type KubePreferences map[string]any - type KubeConfigManager struct { cli KubectlCli configPath string diff --git a/cli/azd/pkg/tools/kubectl/kubectl.go b/cli/azd/pkg/tools/kubectl/kubectl.go index 6e4dd1f62e0..e87064e7b18 100644 --- a/cli/azd/pkg/tools/kubectl/kubectl.go +++ b/cli/azd/pkg/tools/kubectl/kubectl.go @@ -27,6 +27,7 @@ type KubectlCli interface { secrets []string, flags *KubeCliFlags, ) (*exec.RunResult, error) + Exec(ctx context.Context, flags *KubeCliFlags, args ...string) (exec.RunResult, error) } type kubectlCli struct { @@ -57,7 +58,7 @@ func (cli *kubectlCli) Cwd(cwd string) { } func (cli *kubectlCli) ConfigUseContext(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) { - res, err := cli.executeCommand(ctx, flags, "config", "use-context", name) + res, err := cli.Exec(ctx, flags, "config", "use-context", name) if err != nil { return nil, fmt.Errorf("failed setting kubectl context: %w", err) } @@ -164,7 +165,7 @@ func (cli *kubectlCli) CreateSecretGenericFromLiterals( args = append(args, fmt.Sprintf("--from-literal=%s", secret)) } - res, err := cli.executeCommand(ctx, flags, args...) + res, err := cli.Exec(ctx, flags, args...) if err != nil { return nil, fmt.Errorf("kubectl create secret generic --from-env-file: %w", err) } @@ -181,7 +182,7 @@ type KubeCliFlags struct { func (cli *kubectlCli) CreateNamespace(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) { args := []string{"create", "namespace", name} - res, err := cli.executeCommand(ctx, flags, args...) + res, err := cli.Exec(ctx, flags, args...) if err != nil { return nil, fmt.Errorf("kubectl create namespace: %w", err) } @@ -189,7 +190,7 @@ func (cli *kubectlCli) CreateNamespace(ctx context.Context, name string, flags * return &res, nil } -func (cli *kubectlCli) executeCommand(ctx context.Context, flags *KubeCliFlags, args ...string) (exec.RunResult, error) { +func (cli *kubectlCli) Exec(ctx context.Context, flags *KubeCliFlags, args ...string) (exec.RunResult, error) { runArgs := exec. NewRunArgs("kubectl"). AppendParams(args...) diff --git a/cli/azd/pkg/tools/kubectl/models.go b/cli/azd/pkg/tools/kubectl/models.go index 09fb0b969e5..e38610838b5 100644 --- a/cli/azd/pkg/tools/kubectl/models.go +++ b/cli/azd/pkg/tools/kubectl/models.go @@ -1,14 +1,150 @@ package kubectl -type Node struct { - Name string - Status string - Roles []string - Version string -} +type ResourceType string + +const ( + ResourceTypeDeployment ResourceType = "deployment" + ResourceTypeIngress ResourceType = "ing" + ResourceTypeService ResourceType = "svc" +) -type ListResult struct { +type Resource struct { ApiVersion string `json:"apiVersion"` Kind string `json:"kind"` - Items []map[string]any `json:"items"` + Metadata ResourceMetadata `json:"metadata"` +} + +type List[T any] struct { + Resource + Items []T `json:"items"` +} + +type ResourceWithSpec[T any, S any] struct { + Resource + Spec T `json:"spec"` + Status S `json:"status"` +} + +type ResourceMetadata struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Annotations map[string]any +} + +type Deployment ResourceWithSpec[DeploymentSpec, DeploymentStatus] + +type DeploymentSpec struct { + Replicas int `yaml:"replicas"` +} + +type DeploymentStatus struct { + AvailableReplicas int `yaml:"availableReplicas"` + ReadyReplicas int `yaml:"readyReplicas"` + Replicas int `yaml:"replicas"` + UpdatedReplicas int `yaml:"updatedReplicas"` +} + +type Ingress ResourceWithSpec[IngressSpec, IngressStatus] + +type IngressSpec struct { + IngressClassName string `json:"ingressClassName"` + Tls *IngressTls + Rules []IngressRule +} + +type IngressTls struct { + Hosts []string `yaml:"hosts"` + SecretName string `yaml:"secretName"` +} + +type IngressRule struct { + Host *string `yaml:"host"` + Http IngressRuleHttp `yaml:"http"` +} + +type IngressRuleHttp struct { + Paths []IngressPath `yaml:"paths"` +} + +type IngressPath struct { + Path string `yaml:"path"` + PathType string `yaml:"pathType"` +} + +type IngressStatus struct { + LoadBalancer LoadBalancer `json:"loadBalancer"` } + +type LoadBalancer struct { + Ingress []LoadBalancerIngress `json:"ingress"` +} + +type LoadBalancerIngress struct { + Ip string `json:"ip"` +} + +type Service ResourceWithSpec[ServiceSpec, ServiceStatus] + +type ServiceType string + +const ( + ServiceTypeClusterIp ServiceType = "ClusterIP" + ServiceTypeLoadBalancer ServiceType = "LoadBalancer" + ServiceTypeNodePort ServiceType = "NodePort" + ServiceTypeExternalName ServiceType = "ExternalName" +) + +type ServiceSpec struct { + Type ServiceType `json:"type"` + ClusterIp string `json:"clusterIP"` + ClusterIps []string `json:"clusterIPs"` + Ports []Port `json:"ports"` +} + +type ServiceStatus struct { + LoadBalancer LoadBalancer `json:"loadBalancer"` +} + +type Port struct { + Port int `json:"port"` + TargetPort int `json:"targetPort"` + Protocol string `json:"protocol"` +} + +type KubeConfig struct { + ApiVersion string `yaml:"apiVersion"` + Clusters []*KubeCluster `yaml:"clusters"` + Contexts []*KubeContext `yaml:"contexts"` + Users []*KubeUser `yaml:"users"` + Kind string `yaml:"kind"` + CurrentContext string `yaml:"current-context"` + Preferences KubePreferences `yaml:"preferences"` +} + +type KubeCluster struct { + Name string `yaml:"name"` + Cluster KubeClusterData `yaml:"cluster"` +} + +type KubeClusterData struct { + CertificateAuthorityData string `yaml:"certificate-authority-data"` + Server string `yaml:"server"` +} + +type KubeContext struct { + Name string `yaml:"name"` + Context KubeContextData `yaml:"context"` +} + +type KubeContextData struct { + Cluster string `yaml:"cluster"` + User string `yaml:"user"` +} + +type KubeUser struct { + Name string `yaml:"name"` + KubeUserData KubeUserData `yaml:"user"` +} + +type KubeUserData map[string]any +type KubePreferences map[string]any diff --git a/cli/azd/pkg/tools/kubectl/util.go b/cli/azd/pkg/tools/kubectl/util.go new file mode 100644 index 00000000000..48d46bca721 --- /dev/null +++ b/cli/azd/pkg/tools/kubectl/util.go @@ -0,0 +1,148 @@ +package kubectl + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/sethvargo/go-retry" + "gopkg.in/yaml.v3" +) + +var ( + ErrResourceNotFound = errors.New("cannot find resource") + ErrResourceNotReady = errors.New("resource is not ready") +) + +func GetResource[T any]( + ctx context.Context, + cli KubectlCli, + resourceType ResourceType, + resourceName string, + flags *KubeCliFlags, +) (T, error) { + if flags == nil { + flags = &KubeCliFlags{} + } + + if flags.Output == "" { + flags.Output = "json" + } + + var zero T + + res, err := cli.Exec(ctx, flags, "get", string(resourceType), resourceName) + if err != nil { + return zero, fmt.Errorf("failed getting resources, %w", err) + } + + var resource T + + switch flags.Output { + case "json": + err = json.Unmarshal([]byte(res.Stdout), &resource) + if err != nil { + return zero, fmt.Errorf("failed unmarshalling resources JSON, %w", err) + } + case "yaml": + err = yaml.Unmarshal([]byte(res.Stdout), &resource) + if err != nil { + return zero, fmt.Errorf("failed unmarshalling resources YAML, %w", err) + } + default: + return zero, fmt.Errorf("failed unmarshalling resources. Output format '%s' is not supported", flags.Output) + } + + return resource, nil +} + +func GetResources[T any]( + ctx context.Context, + cli KubectlCli, + resourceType ResourceType, + flags *KubeCliFlags, +) (*List[T], error) { + if flags == nil { + flags = &KubeCliFlags{} + } + + if flags.Output == "" { + flags.Output = "json" + } + + res, err := cli.Exec(ctx, flags, "get", string(resourceType)) + if err != nil { + return nil, fmt.Errorf("failed getting resources, %w", err) + } + + var list List[T] + + switch flags.Output { + case "json": + err = json.Unmarshal([]byte(res.Stdout), &list) + if err != nil { + return nil, fmt.Errorf("failed unmarshalling resources JSON, %w", err) + } + case "yaml": + err = yaml.Unmarshal([]byte(res.Stdout), &list) + if err != nil { + return nil, fmt.Errorf("failed unmarshalling resources YAML, %w", err) + } + default: + return nil, fmt.Errorf("failed unmarshalling resources. Output format '%s' is not supported", flags.Output) + } + + return &list, nil +} + +type ResourceFilterFn[T comparable] func(resource T) bool + +func WaitForResource[T comparable]( + ctx context.Context, + cli KubectlCli, + namespace string, + resourceType ResourceType, + resourceFilter ResourceFilterFn[T], + readyStatusFilter ResourceFilterFn[T], +) (T, error) { + var resource T + var zero T + err := retry.Do( + ctx, + retry.WithMaxDuration(time.Minute*10, retry.NewConstant(time.Second*10)), + func(ctx context.Context) error { + result, err := GetResources[T](ctx, cli, resourceType, &KubeCliFlags{ + Namespace: namespace, + }) + + if err != nil { + return fmt.Errorf("failed waiting for deployment, %w", err) + } + + for _, r := range result.Items { + if resourceFilter(r) { + resource = r + break + } + } + + if resource == zero { + return fmt.Errorf("cannot find resource for '%s', %w", resourceType, ErrResourceNotFound) + } + + if !readyStatusFilter(resource) { + return retry.RetryableError(fmt.Errorf("resource '%s' is not ready, %w", resourceType, ErrResourceNotReady)) + } + + return nil + }, + ) + + if err != nil { + return zero, fmt.Errorf("failed waiting for deployment, %w", err) + } + + return resource, nil +} diff --git a/schemas/v1.0/azure.yaml.json b/schemas/v1.0/azure.yaml.json index b394714073d..cdddcd70de1 100644 --- a/schemas/v1.0/azure.yaml.json +++ b/schemas/v1.0/azure.yaml.json @@ -88,7 +88,8 @@ "appservice", "containerapp", "function", - "staticwebapp" + "staticwebapp", + "aks" ] }, "language": { From 8135f84c3936378d83f96a785970754f06c98901 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 24 Feb 2023 12:36:12 -0800 Subject: [PATCH 16/25] Updated bicep based on PR feedback --- .../{agent-pool.bicep => aks-agent-pool.bicep} | 0 ...ged-cluster.bicep => aks-managed-cluster.bicep} | 4 ++-- templates/common/infra/bicep/core/host/aks.bicep | 14 +++++++------- .../bicep/core/security/registry-access.bicep | 8 ++++---- .../nodejs-mongo-aks/.repo/bicep/infra/main.bicep | 10 +++++----- .../src/api/manifests/deployment.yaml | 4 ++-- .../src/web/manifests/deployment.yaml | 4 ++-- 7 files changed, 22 insertions(+), 22 deletions(-) rename templates/common/infra/bicep/core/host/{agent-pool.bicep => aks-agent-pool.bicep} (100%) rename templates/common/infra/bicep/core/host/{managed-cluster.bicep => aks-managed-cluster.bicep} (97%) diff --git a/templates/common/infra/bicep/core/host/agent-pool.bicep b/templates/common/infra/bicep/core/host/aks-agent-pool.bicep similarity index 100% rename from templates/common/infra/bicep/core/host/agent-pool.bicep rename to templates/common/infra/bicep/core/host/aks-agent-pool.bicep diff --git a/templates/common/infra/bicep/core/host/managed-cluster.bicep b/templates/common/infra/bicep/core/host/aks-managed-cluster.bicep similarity index 97% rename from templates/common/infra/bicep/core/host/managed-cluster.bicep rename to templates/common/infra/bicep/core/host/aks-managed-cluster.bicep index 1fc3400a4be..b3cc4725eef 100644 --- a/templates/common/infra/bicep/core/host/managed-cluster.bicep +++ b/templates/common/infra/bicep/core/host/aks-managed-cluster.bicep @@ -124,10 +124,10 @@ resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' } @description('The resource name of the AKS cluster') -output aksClusterName string = aks.name +output clusterName string = aks.name @description('The AKS cluster identity') -output aksClusterIdentity object = { +output clusterIdentity object = { clientId: aks.properties.identityProfile.kubeletidentity.clientId objectId: aks.properties.identityProfile.kubeletidentity.objectId resourceId: aks.properties.identityProfile.kubeletidentity.resourceId diff --git a/templates/common/infra/bicep/core/host/aks.bicep b/templates/common/infra/bicep/core/host/aks.bicep index 9ee00afe5cc..b7b64e5a04c 100644 --- a/templates/common/infra/bicep/core/host/aks.bicep +++ b/templates/common/infra/bicep/core/host/aks.bicep @@ -96,7 +96,7 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-previ var systemPoolSpec = !empty(systemPoolConfig) ? systemPoolConfig : nodePoolPresets[systemPoolType] // Create the primary AKS cluster resources and system node pool -module managedCluster 'managed-cluster.bicep' = { +module managedCluster 'aks-managed-cluster.bicep' = { name: 'managed-cluster' params: { name: name @@ -116,10 +116,10 @@ var hasAgentPool = !empty(agentPoolConfig) || !empty(agentPoolType) var agentPoolSpec = hasAgentPool && !empty(agentPoolConfig) ? agentPoolConfig : nodePoolPresets[agentPoolType] // Create additional user agent pool when specified -module agentPool 'agent-pool.bicep' = if (hasAgentPool) { +module agentPool 'aks-agent-pool.bicep' = if (hasAgentPool) { name: 'aks-node-pool' params: { - clusterName: managedCluster.outputs.aksClusterName + clusterName: managedCluster.outputs.clusterName name: 'npuserpool' config: union({ name: 'npuser', mode: 'User' }, nodePoolBase, agentPoolSpec) } @@ -141,7 +141,7 @@ module containerRegistryAccess '../security/registry-access.bicep' = { name: 'cluster-container-registry-access' params: { containerRegistryName: containerRegistry.outputs.name - principalId: managedCluster.outputs.aksClusterIdentity.objectId + principalId: managedCluster.outputs.clusterIdentity.objectId } } @@ -150,7 +150,7 @@ module clusterKeyVaultAccess '../security/keyvault-access.bicep' = { name: 'cluster-keyvault-access' params: { keyVaultName: keyVaultName - principalId: managedCluster.outputs.aksClusterIdentity.objectId + principalId: managedCluster.outputs.clusterIdentity.objectId } } @@ -201,10 +201,10 @@ var nodePoolPresets = { // Module outputs @description('The resource name of the AKS cluster') -output aksClusterName string = managedCluster.outputs.aksClusterName +output clusterName string = managedCluster.outputs.clusterName @description('The AKS cluster identity') -output aksClusterIdentity object = managedCluster.outputs.aksClusterIdentity +output clusterIdentity object = managedCluster.outputs.clusterIdentity @description('The resource name of the ACR') output containerRegistryName string = containerRegistry.outputs.name diff --git a/templates/common/infra/bicep/core/security/registry-access.bicep b/templates/common/infra/bicep/core/security/registry-access.bicep index 621bc80e278..056bd6c32ed 100644 --- a/templates/common/infra/bicep/core/security/registry-access.bicep +++ b/templates/common/infra/bicep/core/security/registry-access.bicep @@ -1,13 +1,13 @@ param containerRegistryName string param principalId string -var AcrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') +var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') -resource aks_acr_pull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: containerRegistry // Use when specifying a scope that is different than the deployment scope - name: guid(principalId, 'Acr', AcrPullRole) + name: guid(principalId, 'Acr', acrPullRole) properties: { - roleDefinitionId: AcrPullRole + roleDefinitionId: acrPullRole principalType: 'ServicePrincipal' principalId: principalId } diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep index 7ef3bfbc390..9014e81e088 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/infra/main.bicep @@ -42,7 +42,7 @@ resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { } // The AKS cluster to host applications -module cluster '../../../../../../common/infra/bicep/core/host/aks.bicep' = { +module aks '../../../../../../common/infra/bicep/core/host/aks.bicep' = { name: 'aks' scope: rg params: { @@ -103,10 +103,10 @@ output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId -output AZURE_AKS_CLUSTER_NAME string = cluster.outputs.aksClusterName -output AZURE_AKS_IDENTITY_CLIENT_ID string = cluster.outputs.aksClusterIdentity.clientId -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = cluster.outputs.containerRegistryLoginServer -output AZURE_CONTAINER_REGISTRY_NAME string = cluster.outputs.containerRegistryName +output AZURE_AKS_CLUSTER_NAME string = aks.outputs.clusterName +output AZURE_AKS_IDENTITY_CLIENT_ID string = aks.outputs.clusterIdentity.clientId +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = aks.outputs.containerRegistryLoginServer +output AZURE_CONTAINER_REGISTRY_NAME string = aks.outputs.containerRegistryName output REACT_APP_API_BASE_URL string = '' output REACT_APP_APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString output REACT_APP_WEB_BASE_URL string = '' diff --git a/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml index 76332addab4..809f242de05 100644 --- a/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml +++ b/templates/todo/projects/nodejs-mongo-aks/src/api/manifests/deployment.yaml @@ -3,7 +3,7 @@ kind: Deployment metadata: name: todo-api spec: - replicas: 1 + replicas: 2 selector: matchLabels: app: todo-api @@ -14,7 +14,7 @@ spec: spec: containers: - name: todo-api - image: ${AZURE_CONTAINER_REGISTRY_ENDPOINT}/todo-nodejs-mongo-aks/api:latest + image: ${SERVICE_API_IMAGE_NAME} ports: - containerPort: 3100 env: diff --git a/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml b/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml index 9e14d550fce..b64f541c77f 100644 --- a/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml +++ b/templates/todo/projects/nodejs-mongo-aks/src/web/manifests/deployment.yaml @@ -3,7 +3,7 @@ kind: Deployment metadata: name: todo-web spec: - replicas: 1 + replicas: 2 selector: matchLabels: app: todo-web @@ -14,7 +14,7 @@ spec: spec: containers: - name: todo-web - image: ${AZURE_CONTAINER_REGISTRY_ENDPOINT}/todo-nodejs-mongo-aks/web:latest + image: ${SERVICE_WEB_IMAGE_NAME} ports: - containerPort: 3000 env: From 9fe185450b264c41d1ca51333bb895ec67455ce9 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Mon, 27 Feb 2023 11:07:58 -0800 Subject: [PATCH 17/25] Updates diagram and azure.yaml --- .../nodejs-mongo-aks/.repo/bicep/azure.yaml | 7 +++++-- .../nodejs-mongo-aks/assets/resources.png | Bin 123324 -> 153573 bytes 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml index 16167b806a1..7e5fefc6976 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml @@ -5,11 +5,14 @@ metadata: template: todo-nodejs-mongo-aks@0.0.1-beta services: web: - project: ../../web/react-fluent + project: ./src/web dist: build language: js host: aks api: - project: ../../api/js + project: ./src/api language: js host: aks + k8s: + ingress: + relativePath: api \ No newline at end of file diff --git a/templates/todo/projects/nodejs-mongo-aks/assets/resources.png b/templates/todo/projects/nodejs-mongo-aks/assets/resources.png index 07f06cb01a671f5a3db821b615a8e65e9c52626c..1f2c85a4693ed872ac7dd25b4075dc658a50cada 100644 GIT binary patch literal 153573 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⋙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 literal 123324 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 z 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 From 612b0013a126a668f8271f291610750ffced1b73 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Mon, 27 Feb 2023 12:51:18 -0800 Subject: [PATCH 18/25] Adds TODO for adding resource diagnostics --- .../common/infra/bicep/core/host/aks-managed-cluster.bicep | 3 +++ .../common/infra/bicep/core/host/container-registry.bicep | 3 +++ 2 files changed, 6 insertions(+) diff --git a/templates/common/infra/bicep/core/host/aks-managed-cluster.bicep b/templates/common/infra/bicep/core/host/aks-managed-cluster.bicep index b3cc4725eef..49f51f1de34 100644 --- a/templates/common/infra/bicep/core/host/aks-managed-cluster.bicep +++ b/templates/common/infra/bicep/core/host/aks-managed-cluster.bicep @@ -104,6 +104,9 @@ var aksDiagCategories = [ 'guard' ] +// TODO: Update diagnostics to be its own module +// Blocking issue: https://github.com/Azure/bicep/issues/622 +// Unable to pass in a `resource` scope or unable to use string interpolation in resource types resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { name: 'aks-diagnostics' tags: tags diff --git a/templates/common/infra/bicep/core/host/container-registry.bicep b/templates/common/infra/bicep/core/host/container-registry.bicep index 6dcd8f0bcb1..dd122166170 100644 --- a/templates/common/infra/bicep/core/host/container-registry.bicep +++ b/templates/common/infra/bicep/core/host/container-registry.bicep @@ -35,6 +35,9 @@ resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-pr } } +// TODO: Update diagnostics to be its own module +// Blocking issue: https://github.com/Azure/bicep/issues/622 +// Unable to pass in a `resource` scope or unable to use string interpolation in resource types resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { name: 'registry-diagnostics' tags: tags From 1801621e2ae712cddbda0d310c0801f90c8ca804 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Mon, 27 Feb 2023 13:07:33 -0800 Subject: [PATCH 19/25] Minor bicep updates --- .../common/infra/bicep/core/host/aks-managed-cluster.bicep | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/common/infra/bicep/core/host/aks-managed-cluster.bicep b/templates/common/infra/bicep/core/host/aks-managed-cluster.bicep index 49f51f1de34..3303227560e 100644 --- a/templates/common/infra/bicep/core/host/aks-managed-cluster.bicep +++ b/templates/common/infra/bicep/core/host/aks-managed-cluster.bicep @@ -58,6 +58,9 @@ param workspaceId string = '' @description('The node pool configuration for the System agent pool') param systemPoolConfig object +@description('The DNS prefix to associate with the AKS cluster') +param dnsPrefix string = '' + resource aks 'Microsoft.ContainerService/managedClusters@2022-11-02-preview' = { name: name location: location @@ -72,7 +75,7 @@ resource aks 'Microsoft.ContainerService/managedClusters@2022-11-02-preview' = { properties: { nodeResourceGroup: !empty(nodeResourceGroupName) ? nodeResourceGroupName : 'rg-mc-${name}' kubernetesVersion: kubernetesVersion - dnsPrefix: '${name}-dns' + dnsPrefix: empty(dnsPrefix) ? '${name}-dns' : dnsPrefix enableRBAC: enableRbac aadProfile: enableAad ? { managed: true From a4be153ca963cb3a3a2cacb99432763302b063d2 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Mon, 27 Feb 2023 14:37:04 -0800 Subject: [PATCH 20/25] Fixes long lines --- cli/azd/cmd/container.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cli/azd/cmd/container.go b/cli/azd/cmd/container.go index 8711d7cbe8b..818365d930a 100644 --- a/cli/azd/cmd/container.go +++ b/cli/azd/cmd/container.go @@ -132,9 +132,7 @@ func registerCommonDependencies(container *ioc.NestedContainer) { credProvider auth.MultiTenantCredentialProvider) (azcore.TokenCredential, error) { if env == nil { //nolint:lll - panic( - "command asked for azcore.TokenCredential, but prerequisite dependency environment.Environment was not registered.", - ) + panic("command asked for azcore.TokenCredential, but prerequisite dependency environment. Environment was not registered.") } subscriptionId := env.GetSubscriptionId() From 74715bf81fc709e6955b4c41f80b71cf42a1e6bb Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Mon, 27 Feb 2023 17:46:25 -0800 Subject: [PATCH 21/25] Adds happy path deployment test for AKS target --- cli/azd/cmd/container.go | 4 +- cli/azd/pkg/azure/resource_ids.go | 9 + cli/azd/pkg/project/service_target_aks.go | 4 +- .../pkg/project/service_target_aks_test.go | 396 ++++++++++++++++++ cli/azd/pkg/tools/kubectl/util.go | 4 +- 5 files changed, 412 insertions(+), 5 deletions(-) create mode 100644 cli/azd/pkg/project/service_target_aks_test.go diff --git a/cli/azd/cmd/container.go b/cli/azd/cmd/container.go index 818365d930a..56be47ff101 100644 --- a/cli/azd/cmd/container.go +++ b/cli/azd/cmd/container.go @@ -132,7 +132,9 @@ func registerCommonDependencies(container *ioc.NestedContainer) { credProvider auth.MultiTenantCredentialProvider) (azcore.TokenCredential, error) { if env == nil { //nolint:lll - panic("command asked for azcore.TokenCredential, but prerequisite dependency environment. Environment was not registered.") + panic( + "command asked for azcore.TokenCredential, but prerequisite dependency environment. Environment was not registered.", + ) } subscriptionId := env.GetSubscriptionId() diff --git a/cli/azd/pkg/azure/resource_ids.go b/cli/azd/pkg/azure/resource_ids.go index 079ed67dc95..af680615f6e 100644 --- a/cli/azd/pkg/azure/resource_ids.go +++ b/cli/azd/pkg/azure/resource_ids.go @@ -69,6 +69,15 @@ func ContainerAppRID(subscriptionId, resourceGroupName, containerAppName string) return returnValue } +func KubernetesServiceRID(subscriptionId, resourceGroupName, clusterName string) string { + returnValue := fmt.Sprintf( + "%s/providers/Microsoft.ContainerService/managedClusters/%s", + ResourceGroupRID(subscriptionId, resourceGroupName), + clusterName, + ) + return returnValue +} + func StaticWebAppRID(subscriptionId, resourceGroupName, staticSiteName string) string { returnValue := fmt.Sprintf( "%s/providers/Microsoft.Web/staticSites/%s", diff --git a/cli/azd/pkg/project/service_target_aks.go b/cli/azd/pkg/project/service_target_aks.go index 53787d2d1c1..37a0d2b8d1b 100644 --- a/cli/azd/pkg/project/service_target_aks.go +++ b/cli/azd/pkg/project/service_target_aks.go @@ -209,12 +209,12 @@ func (t *aksTarget) Deploy( } return ServiceDeploymentResult{ - TargetResourceId: azure.ContainerAppRID( + TargetResourceId: azure.KubernetesServiceRID( t.env.GetSubscriptionId(), t.scope.ResourceGroupName(), t.scope.ResourceName(), ), - Kind: ContainerAppTarget, + Kind: AksTarget, Details: deployment, Endpoints: endpoints, }, nil diff --git a/cli/azd/pkg/project/service_target_aks_test.go b/cli/azd/pkg/project/service_target_aks_test.go new file mode 100644 index 00000000000..3f7e2c3791f --- /dev/null +++ b/cli/azd/pkg/project/service_target_aks_test.go @@ -0,0 +1,396 @@ +package project + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v2" + "github.com/azure/azure-dev/cli/azd/pkg/convert" + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" + "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/infra" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" + "github.com/azure/azure-dev/cli/azd/pkg/tools/azcli" + "github.com/azure/azure-dev/cli/azd/pkg/tools/docker" + "github.com/azure/azure-dev/cli/azd/pkg/tools/kubectl" + "github.com/azure/azure-dev/cli/azd/test/mocks" + "github.com/azure/azure-dev/cli/azd/test/ostest" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func Test_NewAksTarget(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + serviceTarget, serviceConfig, err := createServiceTarget(mockContext, "") + + require.NoError(t, err) + require.NotNil(t, serviceTarget) + require.NotNil(t, serviceConfig) +} + +func Test_Deploy_HappyPath(t *testing.T) { + tempDir := t.TempDir() + ostest.Chdir(t, tempDir) + + mockContext := mocks.NewMockContext(context.Background()) + err := setupMocks(mockContext) + require.NoError(t, err) + + serviceTarget, serviceConfig, err := createServiceTarget(mockContext, tempDir) + require.NoError(t, err) + + err = setupK8sManifests(t, serviceConfig) + require.NoError(t, err) + + azdContext := azdcontext.NewAzdContextWithDirectory(tempDir) + progressChan := make(chan (string)) + + go func() { + for value := range progressChan { + log.Println(value) + } + }() + + result, err := serviceTarget.Deploy(*mockContext.Context, azdContext, "", progressChan) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, AksTarget, result.Kind) + + // TODO: + // 1. env var for container is set. +} + +func setupK8sManifests(t *testing.T, serviceConfig *ServiceConfig) error { + manifestsDir := filepath.Join(serviceConfig.RelativePath, "manifests") + err := os.MkdirAll(manifestsDir, osutil.PermissionDirectory) + require.NoError(t, err) + + filenames := []string{"deployment.yaml", "service.yaml", "ingress.yaml"} + + for _, filename := range filenames { + err = os.WriteFile(filepath.Join(manifestsDir, filename), []byte(""), osutil.PermissionFile) + require.NoError(t, err) + } + + return nil +} + +func setupMocks(mockContext *mocks.MockContext) error { + kubeConfig := createTestCluster("cluster1", "user1") + kubeConfigBytes, err := yaml.Marshal(kubeConfig) + if err != nil { + return err + } + + // Get Admin cluster credentials + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.Method == http.MethodPost && strings.Contains(request.URL.Path, "listClusterAdminCredential") + }).RespondFn(func(request *http.Request) (*http.Response, error) { + creds := armcontainerservice.CredentialResults{ + Kubeconfigs: []*armcontainerservice.CredentialResult{ + { + Name: convert.RefOf("context"), + Value: kubeConfigBytes, + }, + }, + } + + return mocks.CreateHttpResponseWithBody(request, http.StatusOK, creds) + }) + + // Config view + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "kubectl config view") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.NewRunResult(0, "", ""), nil + }) + + // Config use context + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "kubectl config use-context") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.NewRunResult(0, "", ""), nil + }) + + // Create Namespace + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "kubectl create namespace") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.NewRunResult(0, "", ""), nil + }) + + // Apply Pipe + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "kubectl apply -f -") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.NewRunResult(0, "", ""), nil + }) + + // Create Secret + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "kubectl create secret generic") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.NewRunResult(0, "", ""), nil + }) + + // List container registries + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.Method == http.MethodGet && + strings.Contains(request.URL.Path, "Microsoft.ContainerRegistry/registries") + }).RespondFn(func(request *http.Request) (*http.Response, error) { + result := armcontainerregistry.RegistryListResult{ + NextLink: nil, + Value: []*armcontainerregistry.Registry{ + { + ID: convert.RefOf( + //nolint:lll + "/subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP/providers/Microsoft.ContainerRegistry/registries/REGISTRY", + ), + Location: convert.RefOf("eastus2"), + Name: convert.RefOf("REGISTRY"), + Properties: &armcontainerregistry.RegistryProperties{ + LoginServer: convert.RefOf("REGISTRY.azcurecr.io"), + }, + }, + }, + } + + return mocks.CreateHttpResponseWithBody(request, http.StatusOK, result) + }) + + // List container credentials + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.Method == http.MethodPost && strings.Contains(request.URL.Path, "listCredentials") + }).RespondFn(func(request *http.Request) (*http.Response, error) { + result := armcontainerregistry.RegistryListCredentialsResult{ + Username: convert.RefOf("admin"), + Passwords: []*armcontainerregistry.RegistryPassword{ + { + Name: convert.RefOf(armcontainerregistry.PasswordName("admin")), + Value: convert.RefOf("password"), + }, + }, + } + + return mocks.CreateHttpResponseWithBody(request, http.StatusOK, result) + }) + + // Docker Tag + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "docker login") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.NewRunResult(0, "", ""), nil + }) + + // Docker Tag + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "docker tag") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.NewRunResult(0, "", ""), nil + }) + + // Push Container Image + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "docker push") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.NewRunResult(0, "", ""), nil + }) + + // Get deployments + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "kubectl get deployment") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + deployment := &kubectl.Deployment{ + Resource: kubectl.Resource{ + ApiVersion: "apps/v1", + Kind: "Deployment", + Metadata: kubectl.ResourceMetadata{ + Name: "svc-deployment", + Namespace: "svc-namespace", + }, + }, + Spec: kubectl.DeploymentSpec{ + Replicas: 2, + }, + Status: kubectl.DeploymentStatus{ + AvailableReplicas: 2, + ReadyReplicas: 2, + Replicas: 2, + UpdatedReplicas: 2, + }, + } + deploymentList := createK8sResourceList(deployment) + jsonBytes, _ := json.Marshal(deploymentList) + + return exec.NewRunResult(0, string(jsonBytes), ""), nil + }) + + // Get services + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "kubectl get svc") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + service := &kubectl.Service{ + Resource: kubectl.Resource{ + ApiVersion: "v1", + Kind: "Service", + Metadata: kubectl.ResourceMetadata{ + Name: "svc-service", + Namespace: "svc-namespace", + }, + }, + Spec: kubectl.ServiceSpec{ + Type: kubectl.ServiceTypeClusterIp, + ClusterIps: []string{ + "10.10.10.10", + }, + Ports: []kubectl.Port{ + { + Port: 80, + TargetPort: 3000, + Protocol: "http", + }, + }, + }, + } + serviceList := createK8sResourceList(service) + jsonBytes, _ := json.Marshal(serviceList) + + return exec.NewRunResult(0, string(jsonBytes), ""), nil + }) + + // Get Ingress + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "kubectl get ing") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + ingress := &kubectl.Ingress{ + Resource: kubectl.Resource{ + ApiVersion: "networking.k8s.io/v1", + Kind: "Ingress", + Metadata: kubectl.ResourceMetadata{ + Name: "svc-ingress", + Namespace: "svc-namespace", + }, + }, + Spec: kubectl.IngressSpec{ + IngressClassName: "ingressclass", + Rules: []kubectl.IngressRule{ + { + Http: kubectl.IngressRuleHttp{ + Paths: []kubectl.IngressPath{ + { + Path: "/", + PathType: "Prefix", + }, + }, + }, + }, + }, + }, + Status: kubectl.IngressStatus{ + LoadBalancer: kubectl.LoadBalancer{ + Ingress: []kubectl.LoadBalancerIngress{ + { + Ip: "1.1.1.1", + }, + }, + }, + }, + } + ingressList := createK8sResourceList(ingress) + jsonBytes, _ := json.Marshal(ingressList) + + return exec.NewRunResult(0, string(jsonBytes), ""), nil + }) + + return nil +} + +func createK8sResourceList[T any](resource T) *kubectl.List[T] { + return &kubectl.List[T]{ + Resource: kubectl.Resource{ + ApiVersion: "list", + Kind: "List", + Metadata: kubectl.ResourceMetadata{ + Name: "list", + Namespace: "namespace", + }, + }, + Items: []T{ + resource, + }, + } +} + +func createServiceTarget(mockContext *mocks.MockContext, projectDirectory string) (ServiceTarget, *ServiceConfig, error) { + serviceConfig := ServiceConfig{ + Project: &ProjectConfig{ + Name: "project", + Path: projectDirectory, + }, + Name: "svc", + RelativePath: "./src", + Host: string(AksTarget), + Language: "js", + } + + env := environment.EphemeralWithValues("test", map[string]string{ + environment.TenantIdEnvVarName: "TENANT_ID", + environment.SubscriptionIdEnvVarName: "SUBSCRIPTION_ID", + environment.LocationEnvVarName: "LOCATION", + environment.ResourceGroupEnvVarName: "RESOURCE_GROUP", + environment.AksClusterEnvVarName: "AKS_CLUSTER", + environment.ContainerRegistryEndpointEnvVarName: "REGISTRY.azcurecr.io", + }) + scope := environment.NewTargetResource("SUB_ID", "RG_ID", "CLUSTER_NAME", string(infra.AzureResourceTypeManagedCluster)) + azCli := azcli.NewAzCli(mockContext.Credentials, azcli.NewAzCliArgs{}) + containerServiceClient, err := azCli.ContainerService(*mockContext.Context, env.GetSubscriptionId()) + + if err != nil { + return nil, nil, err + } + + kubeCtl := kubectl.NewKubectl(mockContext.CommandRunner) + docker := docker.NewDocker(mockContext.CommandRunner) + + return NewAksTarget(&serviceConfig, env, scope, azCli, containerServiceClient, kubeCtl, docker), &serviceConfig, nil +} + +func createTestCluster(clusterName, username string) *kubectl.KubeConfig { + return &kubectl.KubeConfig{ + ApiVersion: "v1", + Kind: "Config", + CurrentContext: clusterName, + Preferences: kubectl.KubePreferences{}, + Clusters: []*kubectl.KubeCluster{ + { + Name: clusterName, + Cluster: kubectl.KubeClusterData{ + Server: fmt.Sprintf("https://%s.eastus2.azmk8s.io:443", clusterName), + }, + }, + }, + Users: []*kubectl.KubeUser{ + { + Name: fmt.Sprintf("%s_%s", clusterName, username), + }, + }, + Contexts: []*kubectl.KubeContext{ + { + Name: clusterName, + Context: kubectl.KubeContextData{ + Cluster: clusterName, + User: fmt.Sprintf("%s_%s", clusterName, username), + }, + }, + }, + } +} diff --git a/cli/azd/pkg/tools/kubectl/util.go b/cli/azd/pkg/tools/kubectl/util.go index 48d46bca721..c2721bc8611 100644 --- a/cli/azd/pkg/tools/kubectl/util.go +++ b/cli/azd/pkg/tools/kubectl/util.go @@ -118,7 +118,7 @@ func WaitForResource[T comparable]( }) if err != nil { - return fmt.Errorf("failed waiting for deployment, %w", err) + return fmt.Errorf("failed waiting for resource, %w", err) } for _, r := range result.Items { @@ -141,7 +141,7 @@ func WaitForResource[T comparable]( ) if err != nil { - return zero, fmt.Errorf("failed waiting for deployment, %w", err) + return zero, fmt.Errorf("failed waiting for resource, %w", err) } return resource, nil From 28a069295f583650daf0042d6132b0d8ae24156c Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 28 Feb 2023 13:22:17 -0800 Subject: [PATCH 22/25] Adds kubectl unit tests and aks service target tests --- cli/azd/pkg/project/service_target_aks.go | 28 +-- .../pkg/project/service_target_aks_test.go | 144 ++++++++++++-- cli/azd/pkg/tools/kubectl/kube_config_test.go | 74 +++++++ cli/azd/pkg/tools/kubectl/kubectl.go | 52 +++-- cli/azd/pkg/tools/kubectl/kubectl_test.go | 188 ++++++++++++++---- .../nodejs-mongo-aks/.repo/bicep/repo.yaml | 8 +- 6 files changed, 408 insertions(+), 86 deletions(-) create mode 100644 cli/azd/pkg/tools/kubectl/kube_config_test.go diff --git a/cli/azd/pkg/project/service_target_aks.go b/cli/azd/pkg/project/service_target_aks.go index 37a0d2b8d1b..7dfec400fba 100644 --- a/cli/azd/pkg/project/service_target_aks.go +++ b/cli/azd/pkg/project/service_target_aks.go @@ -70,11 +70,20 @@ func (t *aksTarget) Deploy( ) } + // Login to container registry. + loginServer, has := t.env.Values[environment.ContainerRegistryEndpointEnvVarName] + if !has { + return ServiceDeploymentResult{}, fmt.Errorf( + "could not determine container registry endpoint, ensure %s is set as an output of your infrastructure", + environment.ContainerRegistryEndpointEnvVarName, + ) + } + log.Printf("getting AKS credentials %s\n", clusterName) progress <- "Getting AKS credentials" credentials, err := t.containerService.GetAdminCredentials(ctx, t.scope.ResourceGroupName(), clusterName) if err != nil { - return ServiceDeploymentResult{}, err + return ServiceDeploymentResult{}, fmt.Errorf("failed retrieving cluster admin credentials, %w", err) } kubeConfigManager, err := kubectl.NewKubeConfigManager(t.kubectl) @@ -131,25 +140,16 @@ func (t *aksTarget) Deploy( return ServiceDeploymentResult{}, fmt.Errorf("failed applying kube secrets: %w", err) } - // Login to container registry. - loginServer, has := t.env.Values[environment.ContainerRegistryEndpointEnvVarName] - if !has { - return ServiceDeploymentResult{}, fmt.Errorf( - "could not determine container registry endpoint, ensure %s is set as an output of your infrastructure", - environment.ContainerRegistryEndpointEnvVarName, - ) - } - log.Printf("logging into registry %s\n", loginServer) progress <- "Logging into container registry" if err := t.az.LoginAcr(ctx, t.docker, t.env.GetSubscriptionId(), loginServer); err != nil { - return ServiceDeploymentResult{}, fmt.Errorf("logging into registry '%s': %w", loginServer, err) + return ServiceDeploymentResult{}, fmt.Errorf("failed logging into registry '%s': %w", loginServer, err) } imageTag, err := t.generateImageTag() if err != nil { - return ServiceDeploymentResult{}, fmt.Errorf("generating image tag: %w", err) + return ServiceDeploymentResult{}, fmt.Errorf("failed generating image tag: %w", err) } fullTag := fmt.Sprintf( @@ -162,7 +162,7 @@ func (t *aksTarget) Deploy( log.Printf("tagging image %s as %s", path, fullTag) progress <- "Tagging image" if err := t.docker.Tag(ctx, t.config.Path(), path, fullTag); err != nil { - return ServiceDeploymentResult{}, fmt.Errorf("tagging image: %w", err) + return ServiceDeploymentResult{}, fmt.Errorf("failed tagging image: %w", err) } log.Printf("pushing %s to registry", fullTag) @@ -170,7 +170,7 @@ func (t *aksTarget) Deploy( // Push image. progress <- "Pushing container image" if err := t.docker.Push(ctx, t.config.Path(), fullTag); err != nil { - return ServiceDeploymentResult{}, fmt.Errorf("pushing image: %w", err) + return ServiceDeploymentResult{}, fmt.Errorf("failed pushing image: %w", err) } // Save the name of the image we pushed into the environment with a well known key. diff --git a/cli/azd/pkg/project/service_target_aks_test.go b/cli/azd/pkg/project/service_target_aks_test.go index 3f7e2c3791f..579ccb242f1 100644 --- a/cli/azd/pkg/project/service_target_aks_test.go +++ b/cli/azd/pkg/project/service_target_aks_test.go @@ -30,7 +30,10 @@ import ( func Test_NewAksTarget(t *testing.T) { mockContext := mocks.NewMockContext(context.Background()) - serviceTarget, serviceConfig, err := createServiceTarget(mockContext, "") + serviceConfig := createServiceConfig("") + env := createEnv() + + serviceTarget, err := createServiceTarget(mockContext, serviceConfig, env) require.NoError(t, err) require.NotNil(t, serviceTarget) @@ -45,7 +48,10 @@ func Test_Deploy_HappyPath(t *testing.T) { err := setupMocks(mockContext) require.NoError(t, err) - serviceTarget, serviceConfig, err := createServiceTarget(mockContext, tempDir) + serviceConfig := createServiceConfig(tempDir) + env := createEnv() + + serviceTarget, err := createServiceTarget(mockContext, serviceConfig, env) require.NoError(t, err) err = setupK8sManifests(t, serviceConfig) @@ -64,9 +70,101 @@ func Test_Deploy_HappyPath(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) require.Equal(t, AksTarget, result.Kind) + require.NotNil(t, env.Values["SERVICE_SVC_IMAGE_NAME"]) + require.IsType(t, new(kubectl.Deployment), result.Details) + require.Greater(t, len(result.Endpoints), 0) +} + +func Test_Deploy_No_Cluster_Name(t *testing.T) { + tempDir := t.TempDir() + ostest.Chdir(t, tempDir) + + mockContext := mocks.NewMockContext(context.Background()) + err := setupMocks(mockContext) + require.NoError(t, err) + + serviceConfig := createServiceConfig(tempDir) + env := createEnv() + + // Simulate AKS cluster name not found in env file + delete(env.Values, environment.AksClusterEnvVarName) + + serviceTarget, err := createServiceTarget(mockContext, serviceConfig, env) + require.NoError(t, err) + + azdContext := azdcontext.NewAzdContextWithDirectory(tempDir) + progressChan := make(chan (string)) + + go func() { + for value := range progressChan { + log.Println(value) + } + }() - // TODO: - // 1. env var for container is set. + result, err := serviceTarget.Deploy(*mockContext.Context, azdContext, "", progressChan) + require.Error(t, err) + require.ErrorContains(t, err, "could not determine AKS cluster") + require.Equal(t, ServiceDeploymentResult{}, result) +} + +func Test_Deploy_No_Container_Registry(t *testing.T) { + tempDir := t.TempDir() + ostest.Chdir(t, tempDir) + + mockContext := mocks.NewMockContext(context.Background()) + err := setupMocks(mockContext) + require.NoError(t, err) + + serviceConfig := createServiceConfig(tempDir) + env := createEnv() + + // Simulate container registry endpoint not found in env file + delete(env.Values, environment.ContainerRegistryEndpointEnvVarName) + + serviceTarget, err := createServiceTarget(mockContext, serviceConfig, env) + require.NoError(t, err) + + azdContext := azdcontext.NewAzdContextWithDirectory(tempDir) + progressChan := make(chan (string)) + + result, err := serviceTarget.Deploy(*mockContext.Context, azdContext, "", progressChan) + require.Error(t, err) + require.ErrorContains(t, err, "could not determine container registry endpoint") + require.Equal(t, ServiceDeploymentResult{}, result) +} + +func Test_Deploy_No_Admin_Credentials(t *testing.T) { + tempDir := t.TempDir() + ostest.Chdir(t, tempDir) + + mockContext := mocks.NewMockContext(context.Background()) + err := setupMocks(mockContext) + require.NoError(t, err) + + // Simulate list credentials fail. + // For more secure clusters getting admin credentials can fail + err = setupListClusterAdminCredentialsMock(mockContext, http.StatusUnauthorized) + require.NoError(t, err) + + serviceConfig := createServiceConfig(tempDir) + env := createEnv() + + serviceTarget, err := createServiceTarget(mockContext, serviceConfig, env) + require.NoError(t, err) + + azdContext := azdcontext.NewAzdContextWithDirectory(tempDir) + progressChan := make(chan (string)) + + go func() { + for value := range progressChan { + log.Println(value) + } + }() + + result, err := serviceTarget.Deploy(*mockContext.Context, azdContext, "", progressChan) + require.Error(t, err) + require.ErrorContains(t, err, "failed retrieving cluster admin credentials") + require.Equal(t, ServiceDeploymentResult{}, result) } func setupK8sManifests(t *testing.T, serviceConfig *ServiceConfig) error { @@ -84,7 +182,7 @@ func setupK8sManifests(t *testing.T, serviceConfig *ServiceConfig) error { return nil } -func setupMocks(mockContext *mocks.MockContext) error { +func setupListClusterAdminCredentialsMock(mockContext *mocks.MockContext, statusCode int) error { kubeConfig := createTestCluster("cluster1", "user1") kubeConfigBytes, err := yaml.Marshal(kubeConfig) if err != nil { @@ -104,9 +202,22 @@ func setupMocks(mockContext *mocks.MockContext) error { }, } - return mocks.CreateHttpResponseWithBody(request, http.StatusOK, creds) + if statusCode == http.StatusOK { + return mocks.CreateHttpResponseWithBody(request, statusCode, creds) + } else { + return mocks.CreateEmptyHttpResponse(request, statusCode) + } }) + return nil +} + +func setupMocks(mockContext *mocks.MockContext) error { + err := setupListClusterAdminCredentialsMock(mockContext, http.StatusOK) + if err != nil { + return err + } + // Config view mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return strings.Contains(command, "kubectl config view") @@ -184,7 +295,7 @@ func setupMocks(mockContext *mocks.MockContext) error { return mocks.CreateHttpResponseWithBody(request, http.StatusOK, result) }) - // Docker Tag + // Docker login mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return strings.Contains(command, "docker login") }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { @@ -330,8 +441,8 @@ func createK8sResourceList[T any](resource T) *kubectl.List[T] { } } -func createServiceTarget(mockContext *mocks.MockContext, projectDirectory string) (ServiceTarget, *ServiceConfig, error) { - serviceConfig := ServiceConfig{ +func createServiceConfig(projectDirectory string) *ServiceConfig { + return &ServiceConfig{ Project: &ProjectConfig{ Name: "project", Path: projectDirectory, @@ -341,8 +452,10 @@ func createServiceTarget(mockContext *mocks.MockContext, projectDirectory string Host: string(AksTarget), Language: "js", } +} - env := environment.EphemeralWithValues("test", map[string]string{ +func createEnv() *environment.Environment { + return environment.EphemeralWithValues("test", map[string]string{ environment.TenantIdEnvVarName: "TENANT_ID", environment.SubscriptionIdEnvVarName: "SUBSCRIPTION_ID", environment.LocationEnvVarName: "LOCATION", @@ -350,18 +463,25 @@ func createServiceTarget(mockContext *mocks.MockContext, projectDirectory string environment.AksClusterEnvVarName: "AKS_CLUSTER", environment.ContainerRegistryEndpointEnvVarName: "REGISTRY.azcurecr.io", }) +} + +func createServiceTarget( + mockContext *mocks.MockContext, + serviceConfig *ServiceConfig, + env *environment.Environment, +) (ServiceTarget, error) { scope := environment.NewTargetResource("SUB_ID", "RG_ID", "CLUSTER_NAME", string(infra.AzureResourceTypeManagedCluster)) azCli := azcli.NewAzCli(mockContext.Credentials, azcli.NewAzCliArgs{}) containerServiceClient, err := azCli.ContainerService(*mockContext.Context, env.GetSubscriptionId()) if err != nil { - return nil, nil, err + return nil, err } kubeCtl := kubectl.NewKubectl(mockContext.CommandRunner) docker := docker.NewDocker(mockContext.CommandRunner) - return NewAksTarget(&serviceConfig, env, scope, azCli, containerServiceClient, kubeCtl, docker), &serviceConfig, nil + return NewAksTarget(serviceConfig, env, scope, azCli, containerServiceClient, kubeCtl, docker), nil } func createTestCluster(clusterName, username string) *kubectl.KubeConfig { diff --git a/cli/azd/pkg/tools/kubectl/kube_config_test.go b/cli/azd/pkg/tools/kubectl/kube_config_test.go new file mode 100644 index 00000000000..9642a7e8e1e --- /dev/null +++ b/cli/azd/pkg/tools/kubectl/kube_config_test.go @@ -0,0 +1,74 @@ +package kubectl + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/test/mocks" + "github.com/stretchr/testify/require" +) + +func Test_MergeKubeConfig(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + commandRunner := exec.NewCommandRunner(os.Stdin, os.Stdout, os.Stderr) + cli := NewKubectl(commandRunner) + kubeConfigManager, err := NewKubeConfigManager(cli) + require.NoError(t, err) + + config1 := createTestCluster("cluster1", "user1") + config2 := createTestCluster("cluster2", "user2") + config3 := createTestCluster("cluster3", "user3") + + defer func() { + err := kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config1") + require.NoError(t, err) + err = kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config2") + require.NoError(t, err) + err = kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config3") + require.NoError(t, err) + }() + + err = kubeConfigManager.SaveKubeConfig(*mockContext.Context, "config1", config1) + require.NoError(t, err) + err = kubeConfigManager.SaveKubeConfig(*mockContext.Context, "config2", config2) + require.NoError(t, err) + err = kubeConfigManager.SaveKubeConfig(*mockContext.Context, "config3", config3) + require.NoError(t, err) + + err = kubeConfigManager.MergeConfigs(*mockContext.Context, "config", "config1", "config2", "config3") + require.NoError(t, err) +} + +func createTestCluster(clusterName, username string) *KubeConfig { + return &KubeConfig{ + ApiVersion: "v1", + Kind: "Config", + CurrentContext: clusterName, + Preferences: KubePreferences{}, + Clusters: []*KubeCluster{ + { + Name: clusterName, + Cluster: KubeClusterData{ + Server: fmt.Sprintf("https://%s.eastus2.azmk8s.io:443", clusterName), + }, + }, + }, + Users: []*KubeUser{ + { + Name: fmt.Sprintf("%s_%s", clusterName, username), + }, + }, + Contexts: []*KubeContext{ + { + Name: clusterName, + Context: KubeContextData{ + Cluster: clusterName, + User: fmt.Sprintf("%s_%s", clusterName, username), + }, + }, + }, + } +} diff --git a/cli/azd/pkg/tools/kubectl/kubectl.go b/cli/azd/pkg/tools/kubectl/kubectl.go index e87064e7b18..8b26cd123fb 100644 --- a/cli/azd/pkg/tools/kubectl/kubectl.go +++ b/cli/azd/pkg/tools/kubectl/kubectl.go @@ -12,24 +12,44 @@ import ( "github.com/drone/envsubst" ) +// Executes commands against the Kubernetes CLI type KubectlCli interface { tools.ExternalTool + // Sets the current working directory Cwd(cwd string) + // Sets the env vars available to the CLI SetEnv(env map[string]string) + // Applies one or more files from the specified path ApplyFiles(ctx context.Context, path string, flags *KubeCliFlags) error + // Applies manifests from the specified input ApplyPipe(ctx context.Context, input string, flags *KubeCliFlags) (*exec.RunResult, error) + // Views the current k8s configuration including available clusters, contexts & users ConfigView(ctx context.Context, merge bool, flatten bool, flags *KubeCliFlags) (*exec.RunResult, error) + // Sets the k8s context to use for future CLI commands ConfigUseContext(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) + // Creates a new k8s namespace with the specified name CreateNamespace(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) + // Creates a new generic secret from the specified secret pairs CreateSecretGenericFromLiterals( ctx context.Context, name string, secrets []string, flags *KubeCliFlags, ) (*exec.RunResult, error) + // Executes a k8s CLI command from the specified arguments and flags Exec(ctx context.Context, flags *KubeCliFlags, args ...string) (exec.RunResult, error) } +// K8s CLI Fags +type KubeCliFlags struct { + // The namespace to filter the command or create resources + Namespace string + // The dry-run type, defaults to empty + DryRun string + // The expected output, typically JSON or YAML + Output string +} + type kubectlCli struct { tools.ExternalTool commandRunner exec.CommandRunner @@ -37,26 +57,39 @@ type kubectlCli struct { cwd string } +// Creates a new K8s CLI instance +func NewKubectl(commandRunner exec.CommandRunner) KubectlCli { + return &kubectlCli{ + commandRunner: commandRunner, + } +} + +// Checks whether or not the K8s CLI is installed and available within the PATH func (cli *kubectlCli) CheckInstalled(ctx context.Context) (bool, error) { - return true, nil + return tools.ToolInPath("kubectl") } +// Returns the installation URL to install the K8s CLI func (cli *kubectlCli) InstallUrl() string { return "https://aka.ms/azure-dev/kubectl-install" } +// Gets the name of the Tool func (cli *kubectlCli) Name() string { return "kubectl" } +// Sets the env vars available to the CLI func (cli *kubectlCli) SetEnv(envValues map[string]string) { cli.env = envValues } +// Sets the current working directory func (cli *kubectlCli) Cwd(cwd string) { cli.cwd = cwd } +// Sets the k8s context to use for future CLI commands func (cli *kubectlCli) ConfigUseContext(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) { res, err := cli.Exec(ctx, flags, "config", "use-context", name) if err != nil { @@ -66,6 +99,7 @@ func (cli *kubectlCli) ConfigUseContext(ctx context.Context, name string, flags return &res, nil } +// Views the current k8s configuration including available clusters, contexts & users func (cli *kubectlCli) ConfigView( ctx context.Context, merge bool, @@ -111,6 +145,7 @@ func (cli *kubectlCli) ApplyPipe(ctx context.Context, input string, flags *KubeC return &res, nil } +// Applies manifests from the specified input func (cli *kubectlCli) ApplyFiles(ctx context.Context, path string, flags *KubeCliFlags) error { entries, err := os.ReadDir(path) if err != nil { @@ -154,6 +189,7 @@ func (cli *kubectlCli) ApplyFiles(ctx context.Context, path string, flags *KubeC return nil } +// Creates a new generic secret from the specified secret pairs func (cli *kubectlCli) CreateSecretGenericFromLiterals( ctx context.Context, name string, @@ -173,12 +209,7 @@ func (cli *kubectlCli) CreateSecretGenericFromLiterals( return &res, nil } -type KubeCliFlags struct { - Namespace string - DryRun string - Output string -} - +// Creates a new k8s namespace with the specified name func (cli *kubectlCli) CreateNamespace(ctx context.Context, name string, flags *KubeCliFlags) (*exec.RunResult, error) { args := []string{"create", "namespace", name} @@ -190,6 +221,7 @@ func (cli *kubectlCli) CreateNamespace(ctx context.Context, name string, flags * return &res, nil } +// Executes a k8s CLI command from the specified arguments and flags func (cli *kubectlCli) Exec(ctx context.Context, flags *KubeCliFlags, args ...string) (exec.RunResult, error) { runArgs := exec. NewRunArgs("kubectl"). @@ -223,12 +255,6 @@ func (cli *kubectlCli) executeCommandWithArgs( return cli.commandRunner.Run(ctx, args) } -func NewKubectl(commandRunner exec.CommandRunner) KubectlCli { - return &kubectlCli{ - commandRunner: commandRunner, - } -} - func environ(values map[string]string) []string { env := []string{} for key, value := range values { diff --git a/cli/azd/pkg/tools/kubectl/kubectl_test.go b/cli/azd/pkg/tools/kubectl/kubectl_test.go index 9642a7e8e1e..18dc2717ff0 100644 --- a/cli/azd/pkg/tools/kubectl/kubectl_test.go +++ b/cli/azd/pkg/tools/kubectl/kubectl_test.go @@ -1,74 +1,176 @@ package kubectl import ( + "bytes" "context" - "fmt" "os" + "strings" "testing" "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/test/mocks" + "github.com/azure/azure-dev/cli/azd/test/ostest" "github.com/stretchr/testify/require" ) -func Test_MergeKubeConfig(t *testing.T) { +func Test_ApplyFiles(t *testing.T) { + tempDir := t.TempDir() + ostest.Chdir(t, tempDir) + + ran := false + var runArgs exec.RunArgs + mockContext := mocks.NewMockContext(context.Background()) - commandRunner := exec.NewCommandRunner(os.Stdin, os.Stdout, os.Stderr) - cli := NewKubectl(commandRunner) - kubeConfigManager, err := NewKubeConfigManager(cli) - require.NoError(t, err) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "kubectl apply -f") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + runArgs = args + ran = true - config1 := createTestCluster("cluster1", "user1") - config2 := createTestCluster("cluster2", "user2") - config3 := createTestCluster("cluster3", "user3") + return exec.NewRunResult(0, "", ""), nil + }) - defer func() { - err := kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config1") - require.NoError(t, err) - err = kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config2") - require.NoError(t, err) - err = kubeConfigManager.DeleteKubeConfig(*mockContext.Context, "config3") - require.NoError(t, err) - }() + cli := NewKubectl(mockContext.CommandRunner) - err = kubeConfigManager.SaveKubeConfig(*mockContext.Context, "config1", config1) + err := os.WriteFile("test.yaml", []byte("yaml"), osutil.PermissionFile) require.NoError(t, err) - err = kubeConfigManager.SaveKubeConfig(*mockContext.Context, "config2", config2) + + err = cli.ApplyFiles(*mockContext.Context, tempDir, &KubeCliFlags{ + Namespace: "test-namespace", + }) require.NoError(t, err) - err = kubeConfigManager.SaveKubeConfig(*mockContext.Context, "config3", config3) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(runArgs.StdIn) require.NoError(t, err) - err = kubeConfigManager.MergeConfigs(*mockContext.Context, "config", "config1", "config2", "config3") require.NoError(t, err) + require.True(t, ran) + require.Equal(t, "kubectl", runArgs.Cmd) + require.Equal(t, "yaml", buf.String()) + require.Equal(t, []string{"apply", "-f", "-", "-n", "test-namespace"}, runArgs.Args) } -func createTestCluster(clusterName, username string) *KubeConfig { - return &KubeConfig{ - ApiVersion: "v1", - Kind: "Config", - CurrentContext: clusterName, - Preferences: KubePreferences{}, - Clusters: []*KubeCluster{ - { - Name: clusterName, - Cluster: KubeClusterData{ - Server: fmt.Sprintf("https://%s.eastus2.azmk8s.io:443", clusterName), - }, +func Test_Command_Args(t *testing.T) { + tempDir := t.TempDir() + ostest.Chdir(t, tempDir) + + mockContext := mocks.NewMockContext(context.Background()) + cli := NewKubectl(mockContext.CommandRunner) + + tests := map[string]*kubeCliTestConfig{ + "apply-pipe": { + mockCommandPredicate: "kubectl apply -f -", + expectedCmd: "kubectl", + expectedArgs: []string{"apply", "-f", "-", "-n", "test-namespace"}, + testFn: func() error { + _, err := cli.ApplyPipe(*mockContext.Context, "input", &KubeCliFlags{ + Namespace: "test-namespace", + }) + + return err }, }, - Users: []*KubeUser{ - { - Name: fmt.Sprintf("%s_%s", clusterName, username), + "config-view": { + mockCommandPredicate: "kubectl config view", + expectedCmd: "kubectl", + expectedArgs: []string{"config", "view", "--merge", "--flatten"}, + testFn: func() error { + _, err := cli.ConfigView(*mockContext.Context, true, true, nil) + + return err }, }, - Contexts: []*KubeContext{ - { - Name: clusterName, - Context: KubeContextData{ - Cluster: clusterName, - User: fmt.Sprintf("%s_%s", clusterName, username), - }, + "config-use-context": { + mockCommandPredicate: "kubectl config use-context", + expectedCmd: "kubectl", + expectedArgs: []string{"config", "use-context", "context-name"}, + testFn: func() error { + _, err := cli.ConfigUseContext(*mockContext.Context, "context-name", nil) + + return err + }, + }, + "create-namespace": { + mockCommandPredicate: "kubectl create namespace", + expectedCmd: "kubectl", + expectedArgs: []string{"create", "namespace", "namespace-name", "--dry-run=client", "-o", "yaml"}, + testFn: func() error { + _, err := cli.CreateNamespace(*mockContext.Context, "namespace-name", &KubeCliFlags{ + DryRun: "client", + Output: "yaml", + }) + + return err + }, + }, + "create-secret": { + mockCommandPredicate: "kubectl create secret generic", + expectedCmd: "kubectl", + expectedArgs: []string{ + "create", + "secret", + "generic", + "secret-name", + "--from-literal=foo=bar", + "-n", + "test-namespace", + }, + testFn: func() error { + _, err := cli.CreateSecretGenericFromLiterals( + *mockContext.Context, + "secret-name", + []string{"foo=bar"}, + &KubeCliFlags{ + Namespace: "test-namespace", + }, + ) + + return err + }, + }, + "exec": { + mockCommandPredicate: "kubectl get deployment", + expectedCmd: "kubectl", + expectedArgs: []string{"get", "deployment", "-n", "test-namespace", "-o", "json"}, + testFn: func() error { + _, err := cli.Exec(*mockContext.Context, &KubeCliFlags{ + Namespace: "test-namespace", + Output: "json", + }, "get", "deployment") + + return err }, }, } + + for testName, config := range tests { + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, config.mockCommandPredicate) + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + config.ran = true + config.actualArgs = &args + + return exec.NewRunResult(0, config.mockCommandResult, ""), nil + }) + + t.Run(testName, func(t *testing.T) { + err := config.testFn() + require.NoError(t, err) + require.True(t, config.ran) + require.Equal(t, config.expectedCmd, config.actualArgs.Cmd) + require.Equal(t, config.expectedArgs, config.actualArgs.Args) + }) + } +} + +type kubeCliTestConfig struct { + mockCommandPredicate string + mockCommandResult string + expectedCmd string + expectedArgs []string + actualArgs *exec.RunArgs + ran bool + testFn func() error } diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml index 4ab49f0c44f..304c57c88a0 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/repo.yaml @@ -8,10 +8,10 @@ repo: includeProjectAssets: false remotes: - - name: wbreza-main - url: git@github.com:wbreza/todo-nodejs-mongo-aks.git - - name: wbreza-staging - url: git@github.com:wbreza/todo-nodejs-mongo-aks.git + - name: azure-samples-main + url: git@github.com:Azure-Samples/todo-nodejs-mongo-aks.git + - name: azure-samples-staging + url: git@github.com:Azure-Samples/todo-nodejs-mongo-aks.git branch: staging rewrite: From 8bc072eba3ad0766ae6f6882a59bb2e151ac85f8 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 28 Feb 2023 13:45:20 -0800 Subject: [PATCH 23/25] Fixes cspell lint issues --- .vscode/cspell.global.yaml | 1 + cli/azd/pkg/project/service_target_aks_test.go | 6 +++--- .../todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.vscode/cspell.global.yaml b/.vscode/cspell.global.yaml index 52ae4526794..b8f51ef084b 100644 --- a/.vscode/cspell.global.yaml +++ b/.vscode/cspell.global.yaml @@ -35,6 +35,7 @@ ignoreWords: - Azdo - aztfmod - azurecaf + - azurecr - azuredevops - azurerm - armcontainerregistry diff --git a/cli/azd/pkg/project/service_target_aks_test.go b/cli/azd/pkg/project/service_target_aks_test.go index 579ccb242f1..0d9e1820f0f 100644 --- a/cli/azd/pkg/project/service_target_aks_test.go +++ b/cli/azd/pkg/project/service_target_aks_test.go @@ -269,7 +269,7 @@ func setupMocks(mockContext *mocks.MockContext) error { Location: convert.RefOf("eastus2"), Name: convert.RefOf("REGISTRY"), Properties: &armcontainerregistry.RegistryProperties{ - LoginServer: convert.RefOf("REGISTRY.azcurecr.io"), + LoginServer: convert.RefOf("REGISTRY.azurecr.io"), }, }, }, @@ -392,7 +392,7 @@ func setupMocks(mockContext *mocks.MockContext) error { }, }, Spec: kubectl.IngressSpec{ - IngressClassName: "ingressclass", + IngressClassName: "webapprouting.kubernetes.azure.com", Rules: []kubectl.IngressRule{ { Http: kubectl.IngressRuleHttp{ @@ -461,7 +461,7 @@ func createEnv() *environment.Environment { environment.LocationEnvVarName: "LOCATION", environment.ResourceGroupEnvVarName: "RESOURCE_GROUP", environment.AksClusterEnvVarName: "AKS_CLUSTER", - environment.ContainerRegistryEndpointEnvVarName: "REGISTRY.azcurecr.io", + environment.ContainerRegistryEndpointEnvVarName: "REGISTRY.azurecr.io", }) } diff --git a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml index 7e5fefc6976..c80261db3b4 100644 --- a/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml +++ b/templates/todo/projects/nodejs-mongo-aks/.repo/bicep/azure.yaml @@ -5,12 +5,12 @@ metadata: template: todo-nodejs-mongo-aks@0.0.1-beta services: web: - project: ./src/web + project: ../../web/react-fluent dist: build language: js host: aks api: - project: ./src/api + project: ../../api/js language: js host: aks k8s: From 6769bb6a99c9569b4b08e9d2add7984cebc9a34a Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 28 Feb 2023 14:03:11 -0800 Subject: [PATCH 24/25] More spelling updates --- .vscode/cspell.global.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/cspell.global.yaml b/.vscode/cspell.global.yaml index b8f51ef084b..e39b7b1f7ba 100644 --- a/.vscode/cspell.global.yaml +++ b/.vscode/cspell.global.yaml @@ -162,6 +162,7 @@ ignoreWords: - menuid - PLACEHOLDERIACTOOLS - LOGANALYTICS + - webapprouting - zipdeploy - appinsights useGitignore: true From 8cd86aac9d2d3f751e5e05c99d10b01d9f898dd3 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 28 Feb 2023 15:11:21 -0800 Subject: [PATCH 25/25] Adds aks template to templates.json --- cli/azd/resources/templates.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/azd/resources/templates.json b/cli/azd/resources/templates.json index ab08012ee2d..84ddca4bfd1 100644 --- a/cli/azd/resources/templates.json +++ b/cli/azd/resources/templates.json @@ -63,5 +63,10 @@ "name": "Azure-Samples/todo-python-mongo-terraform", "description": "ToDo Application with a Python API and Azure Cosmos DB API for MongoDB on Azure App Service", "repositoryPath": "Azure-Samples/todo-python-mongo-terraform" + }, + { + "name": "Azure-Samples/todo-nodejs-mongo-aks", + "description": "ToDo Application with a Node.js API and Azure Cosmos DB API for MongoDB on Azure Kubernetes Service", + "repositoryPath": "Azure-Samples/todo-nodejs-mongo-aks" } ] \ No newline at end of file