AWSハイブリッド構成にてKubernetesクラスターを構築する(kopsを利用)

# はじめに

AWSハイブリッド構成とは、Amazon VPCとオンプレミス環境をAWS Direct ConnectやインターネットVPNを利用して接続し、運用する形態のことです。この場合、Amazon VPCは企業内であれば社内ネットワークの一部として扱われる構成となります。また、特に企業内での利用においては、セキュリティ上の理由からVPCにはインターネットゲートウェイはアタッチされておらず、インターネットアクセスはオンプレミス側に用意されているゲートウェイ/プロキシサーバを通るような構成にされることが多いかと思います。

このような構成の場合、Amazon VPC上にKubernetesを自前で構築するのは一苦労です。そもそもKubernetesのマニュアル構築自体も面倒ですし、その上でインターネットに直接出れないといったネットワーク制約もあり、なかなか大変です。

そこで本記事では、このような環境でKubernetesクラスターを[kops](https://github.com/kubernetes/kops)を利用してVPC上にお手軽に構築する方法について説明します。

# kopsとは

Kubernetesクラスターを構築するためのデプロイツールのひとつです。Kubernetesオフィシャルなツールのひとつとして開発されています。利用環境としては現時点ではAWSのみが公式サポート環境であり、実質AWS向けのKubernetesクラスター構築ツールです。

本記事では、AWSハイブリッド構成においてkopsでKubernetesクラスターを構築する上でのポイントを中心に説明します。kopsそのものについてはQiitaでも既にいくつか記事がありますので、そちらを参照されるとよいでしょう。

* [awsでkubernetes環境構築〜kopsでクラスタ作成〜](https://qiita.com/uni-3/items/e3f3bdebe81f13ce53b5)
* [【メモ】 AWSにkopsを使ってkubernetesのクラスタを構築する](https://qiita.com/Kawata/items/e9d52dda2ff8c9da2f91)
* [既存のAWS環境にkopsでsubnetを指定してkubernetesを構築する(master冗長化)](https://qiita.com/kamekichi0527/items/831d768e5507a89075c9)

# AWSハイブリッド構成における課題

AWSハイブリッド構成でプライベートなVPCの構成の場合、下記点の考慮が必要です。

## HTTP(S)プロキシの設定が必要

kopsで構築・運用するにはインターネット経由でリソースの取得が必要になってきます(kubeletのインストールなど)。また、AWSの各種サービスを利用する上でもプロキシ設定が必要です(例えば、Dockerイメージのレジストリに[Amazon ECR](https://aws.amazon.com/jp/ecr/)を使うなど)。

## kopsのデフォルトネットワーク設定である`kubenet`はプライベートサブネットで使えない

VPCはインターネットから接続されていないプライベートサブネットとなる場合、デフォルトの`kubenet`を使うことができないという制約があります。以下、[kopsのネットワークのドキュメント](https://github.com/kubernetes/kops/blob/1.9.0/docs/networking.md#kops-default-networking)より引用です。

> Users running --topology private will not be able to choose kubenet networking because kubenet requires a single routing table. These advanced users are usually running in multiple availability zones and NAT gateways are single AZ, multiple route tables are needed to use each NAT gateway.

`kubenet`はルートテーブルが1つの環境でしか動かず、AZ単位にNATゲートウェイを立てて冗長化する一般的なプライベートネットワークを構成する場合は、複数のルートテーブルが必要になってくるので対応できない、とのことです(実際に試しに`kubenet`で構築してみたところ、AZをまたいでPOD間通信ができないという現象になりました)。

この話は下記のとおり、[kopsのネットワークトポロジーのドキュメント](https://github.com/kubernetes/kops/blob/1.9.0/docs/topology.md#defining-a-topology-on-create)の方にも書かれており、`kubenet`以外のネットワーク設定を利用する必要があります

> In the case of a private cluster you must also set a networking option other than kubenet.

## ロードバランサは内部ELBを使う必要あり

AWSを使う場合、KubernetesのServiceやIngressでELBを使うことができますが、プライベートネットワークのためパブリックなELBは使えず、内部ELBを使う必要があります。

# kopsによるクラスター構築のポイント

上記課題に対応方法について具体的に説明していきます。なお、kopsではCUIでクラスター構築時にオプションで設定項目を指定できますが、kopsのバージョンによっては対応していない設定項目があるため、本記事ではkopsのYAMLファイルによるクラスター定義での設定例を記載しています。とりあえず適当なオプションで`kops create cluster $CLUSTER_NAME`の`--yes`なしでクラスター定義だけをまず作成し、`kops edit cluster $CLUSTER_NAME`にてクラスター定義を編集しつつ、`kops update cluster $CLUSTER_NAME --yes`にて詳細設定した内容でクラスターを構築できます。

## HTTP(S)プロキシの設定

私がちょうどkopsを触り始めた時期(2017年の秋頃)に、kopsのHTTPプロキシ対応がコミットされました。当時は未リリースのため自分でビルドして使っていましたが、kops 1.8以降であれば簡単に設定できるようになっています!

[kopsのHTTPフォワードプロキシのドキュメント](https://github.com/kubernetes/kops/blob/1.9.0/docs/http_proxy.md)に書かれているように、下記のようにkopsのクラスター定義のYAMLに`egressProxy`を追加してクラスターを構築するだけで、必要な箇所にHTTPプロキシ設定がまとめて行われます。

```yaml

spec:
  egressProxy:
    httpProxy:
      host: proxy.corp.local
      port: 3128
    excludes: corp.local,internal.corp.com
```

## ネットワーク設定は`kubenet`以外を使う

`kubenet`は使えないため、それ以外のものを利用します。kopsのクラスター定義の`networking`に設定します。[kopsのネットワークのドキュメント](https://github.com/kubernetes/kops/blob/1.9.0/docs/networking.md#kops-default-networking)によるといくつか利用可能なタイプがあります。例えば`flannel-vxlan`を使う場合は以下のように設定します。

```yaml
spec:
  networking:
    flannel:
      backend: vxlan
```

また、プライベートネットワークのため、kopsのクラスター定義の`topology`設定を以下のようにします。

```yaml
spec:
  topology:
    dns:
      type: Private
    masters: private
    nodes: private
```

なお、既存のVPC/サブネットに対してKubernetesクラスターを構築する場合は、下記のように`networkCIDR`にVPCのネットワークアドレスを、`networkID`にVPCのIDを、`subnets`にサブネット情報を(既存のサブネットを流用する場合は、`id`でサブネットIDを)を設定します。既存のVPC/サブネットを利用する方法についての詳細は、[kopsにドキュメント](https://github.com/kubernetes/kops/blob/1.9.0/docs/run_in_existing_vpc.md)に記載があります。

```yaml
  networkCIDR: 172.16.0.0/24
  networkID: vpc-54baa4eb
  subnets:
  - cidr: 172.16.0.0/25
    id: subnet-11e5ee67
    name: ap-northeast-1a
    type: Private
    zone: ap-northeast-1a
  - cidr: 172.16.0.128/25
    id: subnet-a6113be0
    name: ap-northeast-1c
    type: Private
    zone: ap-northeast-1c
```

## ロードバランサーに内部ELBを利用する

### Kubernetes APIサーバのELB
kopsのクラスター定義にて、下記のように設定することで、KubernetesのAPIサーバのフロントに内部ELBを利用することができます。

```yaml
spec:
  api:
    loadBalancer:
      type: Internal
```

### Service/IngressのELB
こちらはクラスター構築後の話ですが、[Kubernetesのドキュメント](https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer)に記載のとおり、AWSの場合は`annotations`で`service.beta.kubernetes.io/aws-load-balancer-internal`を追加することで、Service/Ingressのロードバランサーに内部ELBを利用することができます。

```yaml
kind: Service
apiVersion: v1
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
  labels:
    app: ingress-nginx
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0
    # Enable PROXY protocol
    service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: '*'
    # Increase the ELB idle timeout to avoid issues with WebSockets or Server-Sent Events.
    service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: '3600'
spec:
  type: LoadBalancer
  selector:
    app: ingress-nginx
  ports:
  - name: http
    port: 80
    targetPort: http
  - name: https
    port: 443
    targetPort: https
```

# その他、AWSならではの設定

## DockerレジストリにECRを利用する

AWSを使うのであれば、Dockerイメージの管理にはECRを使いたくなります。しかし、kopsで構築したノードに対してデフォルトで設定されるIAMロールは、ECRへのアクセス権をもちません。別途アクセス権を付与する必要があります。これは、[kopsのIAMロールに関するドキュメント](https://github.com/kubernetes/kops/blob/1.9.0/docs/iam_roles.md)に記載のとおり、クラスター定義にて下記のように`allowContainerRegistry`を設定すればよいです。

```yaml
spec:
  iam:
    allowContainerRegistry: true
    legacy: false
```

上記設定でクラスターを構築すると、自動的に作成されるIAMロール(`nodes.クラスター名`)に下記のようにECRへの参照権限が追加されます。このIAMロールがKubernetesのノード(=EC2インスタンス)のIAMロールとして付与されることで、Kubernetesクラスター内からECRを利用できるようになります。

```json
        {
            "Sid": "kopsK8sECR",
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetRepositoryPolicy",
                "ecr:DescribeRepositories",
                "ecr:ListImages",
                "ecr:BatchGetImage"
            ],
            "Resource": [
                "*"
            ]
        }
```

なお、注意点として、IAMロールで下回りのEC2インスタンスにECRの参照権限付与してしまうため、セキュリティ上考慮が必要な場合があります。例えば、Kubernetesクラスターを共用サービスとして不特定多数の方に提供した場合は、ECRはだれからも参照できる状態になってしまいます(更新は不可です)。ユースケースによってはこの点注意が必要かと思います。
将来的には、[Amazon EKS](https://aws.amazon.com/eks/)が正式リリースされればIAMロールの適用をもっと細かく制御できるようになるのではないか、と期待しています。

## DNSサーバにRoute 53 Private Hosted Zoneを利用する

AWSを使うのであれば、DNSの管理にはRoute 53を使いたくなります。Kubernetes上にアプリケーションをデプロイした際に、自動的にアプリケーションのDNS登録もできるとユースケースによっては便利です。KubernetesのIncubatorプロジェクトのひとつである[ExternalDNS](https://github.com/kubernetes-incubator/external-dns)がこれを実現してくれるものですが、kopsを使うとこれを簡単に組み込むことができるようになっています。[kopsのドキュメント](https://github.com/kubernetes/kops/blob/1.9.0/docs/cluster_spec.md#externaldns)のとおり、クラスター定義に`externalDns`を下記のように設定してクラスターを構築すると有効化されます。

```yaml
spec:
  externalDns:
    watchIngress: true
```

これでIngressの設定にある`host`を自動的にDNSに登録してくれるようになります。例えば、下記のようなIngress設定をデプロイすると、`myapp.example.org`が自動的にRoute 53に登録されます。

```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: myapp-ingress 
  namespace: myapp
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
  - host: myapp.example.org
    http:
      paths:
      - backend:
          serviceName: myapp-svc
          servicePort: 80
```

なお、Route 53 Private Hosted ZoneをAWSハイブリッド環境で利用するには、オンプレミス側のDNS環境によっては色々やり方があります。クラスメソッドさんの記事、「[AWSハイブリッド構成のDNS設計レシピ](https://dev.classmethod.jp/cloud/aws-hybrid-cloud-dns-designs/)」が参考になりますが、例えば、この記事の「設計レシピ4 : Route 53 Private Hosted Zone + Amazon DNSを利用する」のような構成を取る必要があります。
もし、オンプレミス環境に既にDNS権威サーバがあり、Route 53 Private Hosted Zoneにはサブドメインを管理させたい場合は、サブドメインの委任が必要ですが残念ながらRoute 53 Private Hosted Zoneは対応していません。その場合、最近私が書いた「[AWSハイブリッド構成にてRoute 53 Private Hosted Zoneでサブドメインを管理する](https://qiita.com/wadahiro/items/3462bbff972318d91885)」のような方式をとることで、Route 53を活用することができます。


## ログ収集にAmazon CloudWatch Logsを利用する

AWSを使うのであれば、Kubernetesクラスター上で動くアプリケーションのログ管理に、CloudWatch Logsを使いたくなります。アプリケーションはDockerコンテナとして動作しますが、DockerがそもそもCloudWatch Logsに対応しているので、Dockerの設定変更のみで対応することができます。kopsを利用する場合は、クラスター定義にて下記のように`docker`配下でログ周りの設定ができます。

```yaml
spec:
  docker:
    version: 17.09.0
    logDriver: awslogs
    logOpt:
    - awslogs-region=ap-northeast-1
    - awslogs-group=/k8s-cluster
    - awslogs-create-group=false
    - tag={{.Name}}/{{.ID}}
```

また、CloudWatch Logsへの登録のための権限をIAMロールに付与する必要があります。kopsのクラスター定義にて、下記のように`additionalPolicies`設定しておくことで、クラスター構築時にIAMロールを追加設定することができます。

```yaml
spec:
  additionalPolicies:
    master: |
      [
        {
          "Effect": "Allow",
          "Action": [
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:PutLogEvents"
          ],
          "Resource": ["*"]
        }
      ]
    node: |
      [
        {
          "Effect": "Allow",
          "Action": [
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:PutLogEvents"
          ],
          "Resource": ["*"]
        }
      ]
```

注意点として、`logDriver`を`awslogs`にすると`kubectl logs`でのログ参照はできなくなってしまいます。両立させたいのであれば、`logDriver`でCloudWatch Logsに転送するのではなく、Fluentdなどを使用してCloudWatch Logsに転送する必要があります。


# その他、問題と対策

## インターネットゲートウェイ問題
プライベートネットワークのためVPCにインターネットゲートウェイのアタッチは不要なのですが、この状態でkopsで既存のVPCに対してクラスターを構築すると、インターネットゲートウェイがアタッチされておらず正常終了しません。一応、[#3031 Skip the creation of a IGW](https://github.com/kubernetes/kops/pull/3031) にてプルリクエストが出ていますがマージされていません。現状、クラスター構築時は残念ながら一時的にインターネットゲートウェイをVPCにアタッチさせる必要があります(アタッチされていればよく、ルートテーブル設定でインターネットゲートウェイに通信を流す必要はありません)。

# まとめ

(あまりニーズはなさそうですが)AWSハイブリッド環境にてKubernetesクラスターを構築する際のノウハウをいくつか紹介しました。何かの助けになれば幸いです。