Azure: Create AKS Cluster by ARM and ACI (using CNAB, porter.sh)

Bạn muốn deploy Azure AKS Cluster bằng ARM và Azure Container Instance, sau đó deploy helm chart lên AKS cluster đó.

Table of Contents

Bạn muốn deploy Azure AKS Cluster bằng ARM và Azure Container Instance, sau đó deploy helm chart lên AKS cluster đó.

1. Giới Thiệu

CNAB là gì?

https://cnab.io/

CNAB là Cloud Native Application Bundle. Nó được thiết kế để bundling, installing, managing các distributed app.
Nó được design bởi MS, Docker, Bitami, Hashicorp, Pivotal, codefresh.

1 CNAB bao gồm 3 thành phần: Application Image, Invocation Image, Bundle descriptor.

Tác dụng mà CNAB đem lại: Package toàn bộ app của bạn, ko cần cấu trúc phức tạp,…

Porter là gì?

https://porter.sh/

Trong các CNAB tools, để tạo ra CNAB thì có 1 số tool được sử dụng: Porter, Duffle, Docker App

-> Như vậy Porter là 1 tools để tạo ra CNAB

2. Yêu Cầu

2.1. Service Principal credential

2.1.a. Create Service principal

Hãy chắc chắn bạn đã có 1 Azure Service Principal Credential, và add permission cho nó vào Subscription mà bạn định deploy. Nếu chưa có thì làm theo các step sau:

Cách 1 là run command, mình tạo 1 credential tên là CNAB_PORTER_APP:

az ad sp create-for-rbac -n "CNAB_PORTER_APP" --query "{ client_id: appId, client_secret: password, tenant_id: tenant }"

bạn nhận dc kết quả trả về là 1 đoạn json kiểu sau, hãy giữ nó bí mật:

{
  "client_id": "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
  "client_secret": "yyyyyyyy_yyyyyyyyyy",
  "tenant_id": "zzzz-zzzz-zzzz-zzzz-zzzzzzzzzzz"
}

Cách 2 nếu bạn ko muốn run command thì vào giao Azure Portal để tạo, từ Home, chọn Azure Active Directory AAD:

màn hình sau đó nhập tên App, chọn Accounts in this organizational directory only (hoặc tùy ý bạn nếu hiểu) -> Register

Chọn cái App mà bạn vừa tạo, chọn tab Certificates & secrets, tạo 1 secrets:

Hãy để ý tab Overview, Client ID của App ở đây:

Tất cả những gì bạn cần ghi lại, save lại là Client ID và Client secret mà bạn vừa tạo ra.
Chúng sẽ có ích về sau.

2.1.b. Grant permission Service principal on Subscription

Tiếp theo hãy gán quyền cho cái App bạn vừa tạo, chúng cần quyền trên toàn Subscription mà bạn sẽ sử dụng. Như hình sau, hãy chắc chắn App của bạn nằm trong List Role assignments với role Contributors:

Nếu ko phải, hoặc chưa đúng, hãy chọn nút khoanh đỏ Add để add role assignment cho App của bạn.

2.2. ACR

Sau này bạn sẽ muốn các images chứa code của mình được giữ ở Private Registry của riêng mình.
Hãy tạo cho mình 1 Azure Container Registry, nơi sẽ lưu giữ các Porter Images của bạn. Chú ý enable Admin User vì sau này bạn sẽ cần.

3. Cách làm

3.1. Cài đặt

Theo official docs: https://porter.sh/install/

Mình dùng linux nên chỉ cần run command sau:

curl -L https://cdn.porter.sh/latest/install-linux.sh | bash

3.2. porter.yaml

Tại workspace của bạn, tạo 1 file tên là porter.yaml:
chú ý thay YOUR_ACR_NAME bằng tên ACR của riêng bạn.

name: aks
version: 0.1.6
description: "Azure Kubernetes Service (AKS)"
registry: YOUR_ACR_NAME.azurecr.io/porter

credentials:
- name: azure_client_id
  env: AZURE_CLIENT_ID
  description: AAD Client ID for Azure account authentication - used for AKS Cluster SPN details and for authentication to azure to get KubeConfig
- name: azure_tenant_id
  env: AZURE_TENANT_ID
  description: Azure AAD Tenant Id for Azure account authentication - used to authenticate to Azure to get KubeConfig 
- name: azure_client_secret
  env: AZURE_CLIENT_SECRET
  description: AAD Client Secret for Azure account authentication - used for AKS Cluster SPN details and for authentication to azure to get KubeConfig
- name: azure_subscription_id
  env: AZURE_SUBSCRIPTION_ID
  description: Azure Subscription Id used to set the subscription where the account has access to multiple subscriptions

parameters:
- name: resource_group
  env: CNAB_PARAM_resource_group
  type: string
  description: The name of the resource group to create the AKS Cluster in
- name: cluster_name
  env: CNAB_PARAM_cluster_name
  type: string
  description: The name to use for the AKS Cluster
- name: azure_location
  env: CNAB_PARAM_azure_location
  type: string
  description: The Azure location to create the resources in
  applyTo:
    - "install"
- name: kubernetes_version
  env: CNAB_PARAM_kubernetes_version
  type: string
  description: The Kubernetes version to use
  default: "1.21.2"
  applyTo:
    - "install"
- name: node_vm_size
  env: CNAB_PARAM_node_vm_size
  type: string
  description: The VM size to use for the cluster
  default: "Standard_DS2_v2"
  applyTo:
    - "install"
- name: node_count
  env: CNAB_PARAM_node_count
  type: integer
  minimum: 1
  description: The VM size to use for the cluster
  default: 1
  applyTo:
    - "install"
- name: vm_set_type
  env: CNAB_PARAM_vm_set_type
  type: string
  enum: 
  - VirtualMachineScaleSets
  - AvailabilitySet
  description: Agent pool VM set type
  default: VirtualMachineScaleSets
  applyTo:
    - "install"
- name: installation_name
  env: CNAB_PARAM_installation_name
  type: string
  description: Installation name for Helm deployment
  default: wordpress
  applyTo:
    - "install"
- name: helm_chart_version
  env: CNAB_PARAM_helm_chart_version
  type: string
  description: Version number for the Helm chart
  default: 7.6.5
  applyTo:
    - "install"
    - "upgrade"
- name: namespace
  env: CNAB_PARAM_namespace
  type: string
  description: Kubernetes namespace for installation
  default: wordpress
  applyTo:
    - "install"

mixins:
  - exec
  - az
  - helm

install:
  - az: 
      description: "Azure CLI login"
      arguments: 
        - "login" 
      flags:
        service-principal:
        username: "{{ bundle.credentials.azure_client_id}}"
        password: "{{ bundle.credentials.azure_client_secret}}"
        tenant: "{{ bundle.credentials.azure_tenant_id}}"

  - az: 
      description: "Azure set subscription Id"
      arguments: 
        - "account" 
        - "set" 
      flags:
        subscription: "{{ bundle.credentials.azure_subscription_id}}"

  - az: 
      description: "Create resource group if not exists"
      arguments: 
        - "group" 
        - "create" 
      flags:
        name: "{{ bundle.parameters.resource_group }}"
        location: "{{ bundle.parameters.azure_location }}"
  
  - exec: 
      description: "Create AKS if not exists"
      command: "bash"
      arguments:
        - "aks.sh"
        - "create-aks"
        - "{{ bundle.parameters.cluster_name }}"
        - "{{ bundle.parameters.resource_group }}"
        - "{{ bundle.parameters.kubernetes_version }}"
        - "{{ bundle.parameters.node_vm_size }}"
        - "{{ bundle.parameters.node_count }}"
        - "{{ bundle.credentials.azure_client_id}}"
        - "{{ bundle.credentials.azure_client_secret}}"
        - "{{ bundle.parameters.azure_location }}"
        - "{{ bundle.parameters.vm_set_type }}"

  - az: 
      description: "Azure CLI AKS get-credentials"
      arguments: 
        - "aks" 
        - "get-credentials" 
      flags:
        resource-group: "{{ bundle.parameters.resource_group }}"
        name: "{{ bundle.parameters.cluster_name }}"

  
  - helm:
      description: Install wordpress
      name: '{{ bundle.parameters.installation_name }}'
      chart: stable/wordpress
      version: '{{ bundle.parameters.helm_chart_version }}'
      namespace: '{{ bundle.parameters.namespace }}'
      replace: true

 
uninstall:
  - az: 
      description: "Azure CLI login"
      arguments: 
        - "login" 
      flags:
        service-principal: 
        username: "{{ bundle.credentials.azure_client_id }}"
        password: "{{ bundle.credentials.azure_client_secret }}"
        tenant: "{{ bundle.credentials.azure_tenant_id }}"

  - az: 
      description: "Azure set subscription Id"
      arguments: 
        - "account" 
        - "set" 
      flags:
        subscription: "{{ bundle.credentials.azure_subscription_id }}"

  - exec: 
      description: "Delete AKS"
      command: bash
      arguments: 
        - "aks.sh" 
        - "delete-aks" 
        - "{{ bundle.parameters.cluster_name }}"
        - "{{ bundle.parameters.resource_group }}"

File porter.yaml này mô tả tất cả các step sẽ được chạy khi ACI của bạn start.
Nó giống như 1 kiểu wrapper lên az-cli, run các command từ az login, rồi az create aks, helm…etc

File bên trên với action install, nó sẽ login vào az bằng service principal mà bạn cung cấp, tạo AKS cluster nếu chưa có, deploy Helm chart của Wordpress. End! Rất đơn giản thế thôi.

File tiếp theo bạn cần chuẩn bị là file shell aks.sh:

function create-aks {
    if [[ -z $(az aks show --name $1 --resource-group $2 2> /dev/null) ]]
    then
        az aks create \
        --name $1 \
        --resource-group $2 \
        --kubernetes-version $3 \
        --node-vm-size $4 \
        --node-count $5 \
        --service-principal $6 \
        --client-secret $7 \
        --location $8 \
        --vm-set-type $9 \
        --generate-ssh-keys
    else
        echo "AKS cluster already exists in specified resource group with specified name"
    fi
}

function delete-aks {
    az aks delete --name $1 --resource-group $2 --yes
}

"$@"

Giờ làm sau để đóng gói hết các step đó vào 1 Docker image, rồi publish nó lên ACR?

3.3. Build images

Run command sau để build Docker image:
chú ý trỏ đến nơi bạn đang chứa file porter.yamlaks.sh

porter build

Nó sẽ tạo ra 1 Docker image gọi là invocation image ở local máy bạn.
bạn thử run command docker images sẽ thấy nó:

3.4. Publish images & bundle

Giờ làm sao để sử dụng được nó?
Chắc bạn nghĩ rằng chỉ cần docker push ... là images sẽ lên ACR của mình rồi đúng ko? - Không đơn giản thế

Để sử dụng được nó, ACR của bạn cần chứa images và 1 thứ gọi là bundle. Nếu bạn chỉ push mỗi image lên thì vô dụng. Vì image này do Porter tạo ra, nên hãy push nó lên ACR bằng command của porter.

porter publish

Cả image và bundle sẽ được push lên ACR mà bạn đã chỉ định trong file porter.yaml

như hình trên bạn đã thấy images là /porter/aks-installer:v0.1.6
và bundle là /porter/aks:v0.1.6. Đều nằm trên ACR của bạn.

3.5. Write ARM

Làm sao để sử dụng images chứa các step trên trong ACI ?

Tiếp theo sẽ cần viết 1 ARM template để deploy ra 1 ACI, nơi sẽ run các step mình đã define trong porter.yaml

tạo 1 file ARM template tên tùy ý: deploy-aks-by-aci-cnab.json

{
	"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
	"contentVersion": "1.0.0.0",
	"parameters": {
		"cluster_name": {
			"type": "string",
			"defaultValue": "akstest001",
			"metadata": {
				"description": "The name to use for the AKS Cluster"
			}
		},
		"cnab_action": {
			"type": "string",
			"defaultValue": "install",
			"metadata": {
				"description": "The name of the action to be performed on the application instance."
			}
		},
		"cnab_azure_client_id": {
			"type": "string",
			"metadata": {
				"description": "AAD Client ID for Azure account authentication - used to authenticate to Azure using Service Principal for ACI creation."
			}
		},
		"cnab_azure_client_secret": {
			"type": "securestring",
			"metadata": {
				"description": "AAD Client Secret for Azure account authentication - used to authenticate to Azure using Service Principal for ACI creation."
			}
		},
		"kubernetes_version": {
			"type": "string",
			"defaultValue": "1.21.2",
			"metadata": {
				"description": "The Kubernetes version to use"
			}
		},
		"node_count": {
			"type": "int",
			"defaultValue": 1,
			"metadata": {
				"description": "The VM size to use for the cluster"
			},
			"minValue": 1
		},
		"node_vm_size": {
			"type": "string",
			"defaultValue": "Standard_DS2_v2",
			"metadata": {
				"description": "The VM size to use for the cluster"
			}
		},
		"resource_group": {
			"type": "string",
			"defaultValue": "[concat(parameters('cluster_name'),'-RG')]",
			"metadata": {
				"description": "The name of the resource group to create the AKS Cluster in"
			}
		},
		"vm_set_type": {
			"type": "string",
			"defaultValue": "VirtualMachineScaleSets",
			"allowedValues": [
				"VirtualMachineScaleSets",
				"AvailabilitySet"
			],
			"metadata": {
				"description": "Agent pool VM set type"
			}
		},
		"registry_server": {
			"type": "string",
			"defaultValue": "",
			"metadata": {
				"description": "REGISTRY server"
			}
		},
		"registry_username": {
			"type": "string",
			"defaultValue": "",
			"metadata": {
				"description": "REGISTRY username"
			}
		},
		"registry_password": {
			"type": "securestring",
			"metadata": {
				"description": "REGISTRY password"
			}
		},
		"installation_name": {
			"type": "string",
			"defaultValue": "wordpress",
			"metadata": {
				"description": "REGISTRY username"
			}
		},
		"helm_chart_version": {
			"type": "string",
			"defaultValue": "7.6.5",
			"metadata": {
				"description": "helm chart version"
			}
		},
		"namespace": {
			"type": "string",
			"defaultValue": "wordpress",
			"metadata": {
				"description": "namespace"
			}
		}
	},
	"variables": {
		"aci_location": "[resourceGroup().Location]",
		"cnab_action": "[parameters('cnab_action')]",
		"cnab_azure_client_id": "[parameters('cnab_azure_client_id')]",
		"cnab_azure_client_secret": "[parameters('cnab_azure_client_secret')]",
		"cnab_azure_location": "[resourceGroup().Location]",
		"cnab_azure_subscription_id": "[subscription().subscriptionId]",
		"cnab_azure_tenant_id": "[subscription().tenantId]",
		"cnab_installation_name": "aks",
		"containerGroupName": "[concat('cg-',uniqueString(resourceGroup().id, 'aks'))]",
		"containerName": "[concat('cn-',uniqueString(resourceGroup().id, 'aks'))]"
	},
	"resources": [
		{
			"type": "Microsoft.ContainerInstance/containerGroups",
			"name": "[variables('containerGroupName')]",
			"apiVersion": "2018-10-01",
			"location": "[variables('aci_location')]",
			"properties": {
				"imageRegistryCredentials": [
					{
						"server": "[parameters('registry_server')]",
						"username": "[parameters('registry_username')]",
						"password": "[parameters('registry_password')]"
					}
				],
				"containers": [
					{
						"name": "[variables('containerName')]",
						"properties": {
							"image": "YOUR_ACR_NAME.azurecr.io/porter/aks-installer:v0.1.6",
							"resources": {
								"requests": {
									"cpu": 1.0,
									"memoryInGb": 1.5
								}
							},
							"environmentVariables": [
								{
									"name": "CNAB_ACTION",
									"value": "[variables('cnab_action')]"
								},
								{
									"name": "CNAB_INSTALLATION_NAME",
									"value": "[variables('cnab_installation_name')]"
								},
								{
									"name": "AZURE_LOCATION",
									"value": "[variables('cnab_azure_location')]"
								},
								{
									"name": "AZURE_CLIENT_ID",
									"value": "[variables('cnab_azure_client_id')]"
								},
								{
									"name": "AZURE_CLIENT_SECRET",
									"secureValue": "[variables('cnab_azure_client_secret')]"
								},
								{
									"name": "AZURE_SUBSCRIPTION_ID",
									"value": "[variables('cnab_azure_subscription_id')]"
								},
								{
									"name": "AZURE_TENANT_ID",
									"value": "[variables('cnab_azure_tenant_id')]"
								},
								{
									"name": "VERBOSE",
									"value": "false"
								},
								{
									"name": "CNAB_BUNDLE_NAME",
									"value": "aks"
								},
								{
									"name": "CNAB_BUNDLE_TAG",
									"value": "YOUR_ACR_NAME.azurecr.io/porter/aks:v0.1.6"
								},
								{
									"name": "CNAB_PARAM_azure_location",
									"value": "[variables('cnab_azure_location')]"
								},
								{
									"name": "CNAB_PARAM_cluster_name",
									"value": "[parameters('cluster_name')]"
								},
								{
									"name": "CNAB_PARAM_kubernetes_version",
									"value": "[parameters('kubernetes_version')]"
								},
								{
									"name": "CNAB_PARAM_node_count",
									"value": "[parameters('node_count')]"
								},
								{
									"name": "CNAB_PARAM_node_vm_size",
									"value": "[parameters('node_vm_size')]"
								},
								{
									"name": "CNAB_PARAM_resource_group",
									"value": "[parameters('resource_group')]"
								},
								{
									"name": "CNAB_PARAM_vm_set_type",
									"value": "[parameters('vm_set_type')]"
								},
								{
									"name": "CNAB_PARAM_installation_name",
									"value": "[parameters('installation_name')]"
								},
								{
									"name": "CNAB_PARAM_helm_chart_version",
									"value": "[parameters('helm_chart_version')]"
								},
								{
									"name": "CNAB_PARAM_namespace",
									"value": "[parameters('namespace')]"
								}
							]
						}
					}
				],
				"osType": "Linux",
				"restartPolicy": "Never"
			}
		}
	],
	"outputs": {
		"CNAB Package Action Logs Command": {
			"type": "string",
			"value": "[concat('az container logs -g ',resourceGroup().name,' -n ',variables('containerGroupName'),'  --container-name ',variables('containerName'), ' --follow')]"
		}
	}
}

tìm 2 chỗ YOUR_ACR_NAME và sửa bằng ACR name của bạn

bạn sẽ thấy Bundle cần đặt ở đâu và Images cần đặt ở đâu.

Mình sẽ ko đi sâu vào logic code, các bạn làm theo đúng và chạy được OK đã, sau đó hãy quay lại để tìm hiểu logic thì sẽ dễ hiểu hơn. Trong quá trình fix cho code chạy được, các bạn cũng sẽ hiểu logic hơn là ngồi đọc chay.

3.6. Deploy ARM template

Vào link sau để deploy ARM template mình vừa viết:
https://portal.azure.com/#create/Microsoft.Template

Chọn hình bút chì Build your own template in the editor

Copy toàn bộ nội dung ARM vừa viết paste đè lên -> ấn nút Save

Tại màn hình tiếp theo, Hãy nhập Client ID, Client secret, Registry server/username/password (chú ý bôi vàng):

ấn Review and create

Chờ đến khi màn hình này hiện ra, nghĩa là ACI của bạn đã được tạo ra:

Tuy nhiên đến đây thì các command/step của bạn vẫn đang chạy trong ACI, hãy xem log của nó là success hay fail: Vào Azure Container Instance, chọn Instance mới do bạn tạo ra, -> vào Container -> Log,

Nếu màn hình log như sau, có nghĩa là nó đã qua hết các step từ AZ login, AZ create AKS, Helm chart để cuối cùng có 1 trang Wordpress cho bạn:

Tất nhiên bạn có thể login vào AKS cluster xem các pod, các namespace, services được rồi

Done!

Chúc bạn thành công 😜

4. Troubleshooting

4.1. ERROR: (AuthorizationFailed)

Nếu bạn gặp lỗi này khi xem Logs của Container:

Azure set subscription Id
Create resource group if not exists
ERROR: (AuthorizationFailed) The client 'xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx' with object id 'xxxxxxx-xxxx-xxxx-yyyy-yyyyy' does not have authorization to perform action 'Microsoft.Resources/subscriptions/resourcegroups/write' over scope '/subscriptions/*******/resourcegroups/akstest001-RG' or the scope is invalid. If access was recently granted, please refresh your credentials.
err: error running command /cnab/app az group create --location westeurope --name akstest001-RG --output json: exit status 1

-> Có nghĩa là App Client Id/Secret của bạn đang ko có quyền tạo Resource Group. Bạn hãy chú ý App Registration của bạn cần dc có quyền COntributor trên scope là Subscription, ko phải chỉ 1 Resource group nào đó. (Xem kỹ lại step 2.1.b.)

5. Tìm hiểu thêm về Porter

Trong quá trình phát triển, bạn sẽ không muốn mỗi lần sửa code muốn test thì lại phải porter publish rồi lại deploy ARM template. Như vậy sẽ rất tốn thời gian (Mỗi lần porter publish phải mất khoảng 10-15 phút để nó upload được images xấp xỉ 2Gb lên Registry của bạn)

Thế nên sau bước porter build, bạn có thể test luôn image trước khi publish nó bằng cách dùng các command porter install

Nhưng để dùng thì bạn cần truyền các credential vào trước đã:

porter credentials generate [YOUR_CRED_NAME]

1 list các lựa chọn hiện ra, sau đó bạn insert các giá trị credential (Service Principal Client ID, secret, registry user name, password, …etc)

(Optional) Bạn cũng có thể tạo các Parameter set:

porter parameters generate [YOUR_PARAM_SET_NAME]

Cuối cùng thì có thể test Images:

porter install --cred [YOUR_CRED_NAME] --parameter-set [YOUR_PARAM_SET_NAME]

Câu lệnh trên sẽ tạo ra container ngay trên máy local của bạn, giúp bạn nhanh chóng debug nếu có vấn đề.

List các command của Porter ở đây: https://porter.sh/cli/porter/#see-also

CREDITS

https://porter.sh/
https://github.com/Azure/azure-cnab-quickstarts
https://www.youtube.com/watch?v=z1lnQfaAVeg&ab_channel=endjin

Thank You!

Your comment has been submitted. It will appear on this page shortly! OK

Yikes, Sorry!

Error occured. Couldn't submit your comment. Please try again. Thank You! OK

Leave a comment