SRE兼スクラムマスターのブログ

チーム開発が好きなエンジニアブログです。検証した技術やチームの取り組みを書いていきます。

Azure 上に GitOps 対応の AKS クラスターを自動構築する

背景

GitOpsというキーワードが出てきており、職場でもGitOpsを使った検証環境の運用が始まっている。

しかし、GitOps環境を構築するにあたってIaCは進めているものの、まだ人手によるOperationがなくなっていないのも事実です。

そこで、Azure上にkubernetes clusterの構築からGitOpsの仕組みを導入するまでを自動化する仕組みとして

Bedrockを検証したので手順や気づきについてまとめていきます。

GitOpsとは

Weaveworks社が提唱した,Kubernetesの運用ベストプラクティス

詳しくはGitOpsを参照

記事はボリュームがあるので 要点をまとめると下記のような感じです。

  • システムの状態がGit管理されている
  • コードとして宣言的なインフラストラクチャの実現 ⇔ Kubernetesも宣言的な仕組み
  • Git上で承認された場合は、環境にも適用される
  • Git = 環境を保証し、差異があったらアラートする

GitOpsを実現するために、 現在はFluxというOSSを使ってkubernetes clusterの状態とgit管理しているmanifestの間で 同期を取る仕組みを実現している。

fluxについて過去にチュートリアル記事を書いたので参考になればと思います。 cabi99.hatenablog.com

現在はFluxを使って下図の環境を構築しています。 この環境をpipeline以外、すべて自動で構築します。

f:id:JunichiMitsunaga:20200127125902p:plain
Azure DevOps PipelinesとFlux連携

Bedrockとは

Microsoftが提供するGitOpsワークフローを使用してKubernetesクラスターを運用可能にするterraformテンプレートです。 GitOpsワークフローは、Fabrikateの定義を中心に展開されており、構成から構成を分離する、より高い抽象化レベルで展開を指定できます。 今回の検証ではBedrockを使ってAKSクラスターを自動構築することを優先したのでFabrikateについては検証しません。

github.com

手順

前提条件

helm: v2.13.1 → 3系だとまだ動かないっぽい

terraform: v0.12.20 → テンプレートは古いバージョン対応だったのでupgradeコマンドでスクリプトを更新しました

Azure CLI

$ az version
{
  "azure-cli": "2.0.80",
  "azure-cli-command-modules-nspkg": "2.0.3",
  "azure-cli-core": "2.0.80",
  "azure-cli-nspkg": "3.0.4",
  "azure-cli-telemetry": "1.0.4",
  "extensions": {}
}

おおまかな流れ

bedrockのテンプレートで実行している内容を簡単に書くと以下になります。

  1. azure cli でresource groupを作成する
  2. terraformでvnet,subnetを作成する
  3. terraformでを作成する
  4. terraformでaks clusterを作成する
  5. terraformのnull_resourceを使ってshellでflux agentをインストール/起動する

詳細

1.azure cli でresource groupを作成する

az group create -n gitOpsCluster -l  japaneast

f:id:JunichiMitsunaga:20200215170328p:plain

2.vnetとsubnetをterraformで作成する ※利用するfileはbedrockの以下path

/bedrock/cluster/azure/vnet

参考までにmain.tf variables.tfはresource_group_name以外、ほぼデフォルト値

resource "azurerm_virtual_network" "vnet" {
  name                = var.vnet_name
  location            = var.location
  address_space       = [var.address_space]
  resource_group_name = var.resource_group_name
  dns_servers         = var.dns_servers
  tags                = var.tags
}

resource "azurerm_subnet" "subnet" {
  count                = length(var.subnet_names)
  name                 = var.subnet_names[count.index]
  virtual_network_name = azurerm_virtual_network.vnet.name
  resource_group_name  = azurerm_virtual_network.vnet.resource_group_name

  address_prefix    = var.subnet_prefixes[count.index]
  service_endpoints = var.subnet_service_endpoints[count.index]
}

f:id:JunichiMitsunaga:20200215184059p:plain

3.aksクラスターを作成し、fluxをinstallする ※利用するfileはbedrockの以下path

/bedrock/cluster/azure/aks-gitops

参考までにmain.tf variables.tfはfluxの設定値やkey情報以外はほぼデフォルト値

module "aks" {
  source = "../../azure/aks"

  resource_group_name      = var.resource_group_name
  cluster_name             = var.cluster_name
  agent_vm_count           = var.agent_vm_count
  agent_vm_size            = var.agent_vm_size
  dns_prefix               = var.dns_prefix
  vnet_subnet_id           = "/subscriptions/${var.azure_subscription_id}/resourceGroups/${var.resource_group_name}/providers/Microsoft.Network/virtualNetworks/${var.virtual_network_name}/subnets/${var.agent_subnet_name}"
  ssh_public_key           = var.ssh_public_key
  service_principal_id     = var.service_principal_id
  service_principal_secret = var.service_principal_secret
  service_cidr             = var.service_cidr
  dns_ip                   = var.dns_ip
  docker_cidr              = var.docker_cidr
  kubeconfig_filename      = var.kubeconfig_filename
  network_policy           = var.network_policy
  network_plugin           = var.network_plugin
  oms_agent_enabled        = var.oms_agent_enabled
}

module "flux" {
  source = "../../common/flux"

  gitops_ssh_url       = var.gitops_ssh_url  #Azure DevOpsのgitで管理しているリポジトリ
  gitops_ssh_key       = var.gitops_ssh_key #fluxがAzure DevOpsにアクセスするためのkey
  gitops_path          = var.gitops_path #fluxが同期したいマニフェストのpath
  gitops_poll_interval = var.gitops_poll_interval
  gitops_label         = var.gitops_label
  gitops_url_branch    = var.gitops_url_branch #fluxが同期したいbranch
  enable_flux          = var.enable_flux
  flux_recreate        = var.flux_recreate
  kubeconfig_complete  = module.aks.kubeconfig_done
  kubeconfig_filename  = var.kubeconfig_filename
  flux_clone_dir       = "${var.cluster_name}-flux"
  acr_enabled          = var.acr_enabled
  gc_enabled           = var.gc_enabled
}

module "kubediff" {
  source = "../../common/kubediff"

  kubeconfig_complete = module.aks.kubeconfig_done
  gitops_ssh_url      = var.gitops_ssh_url
}

Azure DevOpsでssh_keyを発行する手順はこちらを参照

Connect to your Git repos with SSH - Azure Repos | Microsoft Docs

注意点

fluxで同期を行うmanifestをkustomizeを使って管理している場合はfluxのoptionに注意する必要があります。 bedrockでfluxをインストールするのはhelmを利用しているのですが、「manifestGeneration」optionが変数として受け渡す設計がなされていないので自分たちで修正する必要があります。

/cluster/common/flux/deploy_flux.sh
#!/bin/sh
while getopts :b:f:g:k:d:e:c:l:s:r:t:z: option
do
 case "${option}" in
 b) GITOPS_URL_BRANCH=${OPTARG};;
 f) FLUX_REPO_URL=${OPTARG};;
 g) GITOPS_SSH_URL=${OPTARG};;
 k) GITOPS_SSH_KEY=${OPTARG};;
 d) REPO_ROOT_DIR=${OPTARG};;
 e) GITOPS_PATH=${OPTARG};;
 c) GITOPS_POLL_INTERVAL=${OPTARG};;
 l) GITOPS_LABEL=${OPTARG};;
 s) ACR_ENABLED=${OPTARG};;
 r) FLUX_IMAGE_REPOSITORY=${OPTARG};;
 t) FLUX_IMAGE_TAG=${OPTARG};;
 z) GC_ENABLED=${OPTARG};;
 *) echo "Please refer to usage guide on GitHub" >&2
    exit 1 ;;
 esac
done

KUBE_SECRET_NAME="flux-ssh"
RELEASE_NAME="flux"
KUBE_NAMESPACE="flux"
CLONE_DIR="flux"
REPO_DIR="$REPO_ROOT_DIR/$CLONE_DIR"
FLUX_CHART_DIR="chart/flux"
FLUX_MANIFESTS="manifests"

echo "flux repo root directory: $REPO_ROOT_DIR"

rm -rf "$REPO_ROOT_DIR"

echo "creating $REPO_ROOT_DIR directory"
if ! mkdir "$REPO_ROOT_DIR"; then
    echo "ERROR: failed to create directory $REPO_ROOT_DIR"
    exit 1
fi

cd "$REPO_ROOT_DIR" || exit 1

echo "cloning $FLUX_REPO_URL"

if ! git clone -b "$FLUX_IMAGE_TAG" "$FLUX_REPO_URL"; then
    echo "ERROR: failed to clone $FLUX_REPO_URL"
    exit 1
fi

cd "$CLONE_DIR/$FLUX_CHART_DIR" || exit 1

echo "creating $FLUX_MANIFESTS directory"
if ! mkdir "$FLUX_MANIFESTS"; then
    echo "ERROR: failed to create directory $FLUX_MANIFESTS"
    exit 1
fi

# call helm template with
#   release name: flux
#   git url: where flux monitors for manifests
#   git ssh secret: kubernetes secret object for flux to read/write access to manifests repo
echo "generating flux manifests with helm template"
#   ここにmanifestGenerationがないため、kustomizeでmanifestをgenerateする場合はoptionを追加する必要があります。
#   こんな感じ「--set manifestGeneration=true」
if ! helm template . --name "$RELEASE_NAME" --namespace "$KUBE_NAMESPACE" --values values.yaml --set image.repository="$FLUX_IMAGE_REPOSITORY" --set image.tag="$FLUX_IMAGE_TAG" --output-dir "./$FLUX_MANIFESTS" --set git.url="$GITOPS_SSH_URL" --set git.branch="$GITOPS_URL_BRANCH" --set git.secretName="$KUBE_SECRET_NAME" --set git.path="$GITOPS_PATH" --set git.pollInterval="$GITOPS_POLL_INTERVAL" --set git.label="$GITOPS_LABEL" --set registry.acr.enabled="$ACR_ENABLED" --set syncGarbageCollection.enabled="$GC_ENABLED"; then
    echo "ERROR: failed to helm template"
    exit 1
fi

# back to the root dir
cd ../../../../ || exit 1


echo "creating kubernetes namespace $KUBE_NAMESPACE if needed"
if ! kubectl describe namespace $KUBE_NAMESPACE > /dev/null 2>&1; then
    if ! kubectl create namespace $KUBE_NAMESPACE; then
        echo "ERROR: failed to create kubernetes namespace $KUBE_NAMESPACE"
        exit 1
    fi
fi

echo "creating kubernetes secret $KUBE_SECRET_NAME from key file path $GITOPS_SSH_KEY"

if kubectl get secret $KUBE_SECRET_NAME -n $KUBE_NAMESPACE > /dev/null 2>&1; then
    # kubectl doesn't provide a native way to patch a secret using --from-file.
    # The update path requires loading the secret, base64 encoding it, and then
    # making a call to the 'kubectl patch secret' command.
    if [ ! -f "$GITOPS_SSH_KEY" ]; then
        echo "ERROR: unable to load GITOPS_SSH_KEY: $GITOPS_SSH_KEY"
        exit 1
    fi

    secret=$(< "$GITOPS_SSH_KEY" base64 -w 0)
    if ! kubectl patch secret $KUBE_SECRET_NAME -n $KUBE_NAMESPACE -p="{\"data\":{\"identity\": \"$secret\"}}"; then
        echo "ERROR: failed to patch existing flux secret: $KUBE_SECRET_NAME "
        exit 1
    fi
else
    if ! kubectl create secret generic $KUBE_SECRET_NAME --from-file=identity="$GITOPS_SSH_KEY" -n $KUBE_NAMESPACE; then
        echo "ERROR: failed to create secret: $KUBE_SECRET_NAME"
        exit 1
    fi
fi

echo "Applying flux deployment"
if ! kubectl apply -f  "$REPO_DIR/$FLUX_CHART_DIR/$FLUX_MANIFESTS/flux/templates" -n $KUBE_NAMESPACE; then
    echo "ERROR: failed to apply flux deployment"
    exit 1
fi

aksのデプロイが完了するとfluxの同期によりapplicationがデプロイされます。

今回はaksチュートリアルにあるvoteアプリを使いました。

クイック スタート:Azure Kubernetes Service クラスターをデプロイする | Microsoft Docs

デプロイされたらexternal-ipを取得してアクセス出来ます。

$ kubectl get svc -o wide
NAME               TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE   SELECTOR
azure-vote-back    ClusterIP      10.0.111.235   <none>          6379/TCP       10m   app=azure-vote-back
azure-vote-front   LoadBalancer   10.0.55.117    23.102.68.224   80:31559/TCP   10m   app=azure-vote-front
kubernetes         ClusterIP      10.0.0.1       <none>          443/TCP        22m   <none>

アクセスすると無事表示されました。 f:id:JunichiMitsunaga:20200215193713p:plain

デフォルトですと「default」namespaceにfluxのpodがdeployされるので別で管理したい場合はoptionを変更したほうが良いかもしれません。

$ kubectl get po --all-namespaces
NAMESPACE     NAME                                        READY   STATUS    RESTARTS   AGE
default       azure-vote-back-bc5b86f5f-dpskl             1/1     Running   0          14m
default       azure-vote-front-ccfd8667f-rt8rc            1/1     Running   0          14m
flux          flux-6455b87976-7thxx                       1/1     Running   0          15m
flux          flux-memcached-5ff94b674c-t9qcm             1/1     Running   0          15m
kube-system   azure-cni-networkmonitor-bjxvq              1/1     Running   0          22m
kube-system   azure-ip-masq-agent-pltrz                   1/1     Running   0          22m
kube-system   azure-npm-79bxx                             2/2     Running   0          17m
kube-system   coredns-544d979687-dzrxw                    1/1     Running   0          26m
kube-system   coredns-autoscaler-546d886ffc-s8ssf         1/1     Running   0          26m
kube-system   dashboard-metrics-scraper-867cf6588-9llkt   1/1     Running   0          26m
kube-system   kube-proxy-nvxvm                            1/1     Running   0          22m
kube-system   kubernetes-dashboard-7f7676f7b5-dnrcc       1/1     Running   0          26m
kube-system   metrics-server-75b8b88d6b-spllf             1/1     Running   1          26m
kube-system   tunnelfront-6dcfb8d9d9-vrtvr                1/1     Running   0          26m

気になったこと

FluxのDocker Image

検証していて気になったこととして、FluxのDocker imageのリポジトリです。 もともとfluxはFlux projectの「fluxcd」リポジトリからpullしていると想定していましたが 実際はweaveworksのリポジトリからでした。

リポジトリを比較するとfluxcdの方がversionは新しいようです。

weaveworks
weaveworks/flux:1.14.2

fluxcd
fluxcd/flux:1.17.1

この違いに関しては

weaveworksの「Flagger」というプロダクトがFluxを利用したcanary デプロイを実現する仕組みがあり対応しているFluxをweaveworks自身が管理しているのではないかと推測しています。

所感

今回はbedrockを使ってAzure上にGitOps対応のAKSクラスターを手動で作成しました。

これらの工程はAzure DevOpsのpipelinesのようなCIツールで自動化することが可能なので自動化できればエンジニアは 特に意識せずにテスト環境を構築できるようになります。

fluxを使ってclusterのgitOpsは実現できていますが、terraformのコードと環境が同期を取っているわけではないので、 実際に環境に関してもgitOpsを行いたい場合はFabrikateを導入してCI pipelineを構築することが必要になります。

検証してみた感想としてあまり抽象化しすぎると、管理や保守の面で結果的に属人化してしまう恐れもあるので テスト環境のような使い捨ての環境を作る場合は、今回のようなterraformスクリプトを自動化するpipelineのみでも十分だと感じました。