Oracle Cloud Always Freeで作る 0円Kubernetesクラスタ

これは何?

Oracle社が発表したAlways FreeサービスでKubernetesクラスタを作成する方法を紹介します。

f:id:inajob:20191006162003p:plain

Oracle Cloud Free Tier | Oracle 日本

Oracle Cloud Free Tierはクレジットカードの登録さえすれば、無料でいくつかのリソースを利用することができます。

特に今回利用するFree Tierは

  • 2つのVM(1/8 CPU, 1GB Memory)
  • 10TB/monthの外部通信

です。

 

以前こういう記事を書きました。

inajob.hatenablog.jp

この記事でははVultrというVPSサービスを使い$10でKubernetesクラスタを作る方法を紹介したのですが、そのとき利用したVPSのスペックは 1CPU,1GB Memory のVM2台でした。

 

CPUこそ劣るものの、これでできるならOracle CloudののFreeTierでもKubernetesクラスタが作れるのでは?と考え挑戦してみました。

注意

この手順を使って作るクラスタはインターネットからアクセスできる状態になります。この手順通り実施したとしても様々な攻撃のリスクが存在しています。この手順を実行し起きたいかなる問題も筆者は責任を負えませんので、個人の責任で作業を行ってください。

また、これらの手順を実行した結果によってOracle Cloudへの課金が生じる可能性があります、こちらに関しても筆者は責任を負いません

FreeTierへの登録

ここは割愛します。クレジットカードの登録などが必要です。

また、ここで登録したリージョンが「ホームリージョン」となり、Free Tierで利用できるのはそのリージョンだけになるようです(お金を払えばそれとは関係なくどのリージョンでも利用できます。)

ここFreeTierが発表されてしばらくはJapanリージョンではVMが払い出せない状況にありましたが、ここ数日で払い出せるように回復しているように見えます。

 

追記:この記事を書いている2019/10/06現在、再び払い出せなくなりました。(この症状の時 VM作成時に Out of host capacity というエラーが出ます。)

VMの作成

f:id:inajob:20191006131954p:plain

インスタンス命名は何でもいいです、イメージソースはここではUbuntuの18.0.4にします。また、パブリックIPを割り当てるために「シェイプ、ネットワーク、ストレージオプションの表示」をクリックします。

f:id:inajob:20191006132111p:plain

そして”Assign in public IP address”を選択します。

さらにSSHキーを適切に設定して「作成」をクリックします。

VMが作成できると「パブリックIPアドレスが表示されます。このIPにsshすることで、作成したVMにログインできます。

 

同じ要領でWorkerのVMも作成しておきます。

このようにして作ったVM2台はちょうどAlways Free Tierの要件を満たしており、無料で利用することができます。

これから作るKubernetesクラスタ

f:id:inajob:20191006161646p:plain

master, workerのVMを作り、その2台でKubernetesクラスタを構築します。ingress-controllerをデプロイし、インターネットからそこを経由してクラスタ内のWebアプリケーションにアクセスできるようにします。(0円で)

Kubernetes Nodeの構築

次にKubernetesのNodeとして必要なパッケージのインストールです。これをざざっと実行することで

  • 最新のDockerパッケージのインストール
  • 最新のKubernetesパッケージのインストール
  • Kubernetesとしては非推奨ですが)Swapの有効化とKubeletの設定

が行えます。

apt-get update && apt-get install -y apt-transport-https ca-certificates curl software-properties-common
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb http://apt.kubernetes.io/ kubernetes-xenial main
EOF

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

apt-get update
apt-get install -y docker-ce
apt-get install -y kubelet kubeadm kubectl

dd if=/dev/zero of=/swapfile count=2048 bs=1M
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo "/swapfile   none    swap    sw    0   0" >> /etc/fstab

sed -i 's/\/usr\/bin\/kubelet/\/usr\/bin\/kubelet --fail-swap-on=false --system-reserved=memory=100Mi /' /etc/systemd/system/kubelet.service.d/10-kubeadm.conf

systemctl daemon-reload
systemctl restart kubelet

ネットワークの設定(Oracle Cloud)

Kubernetesクラスタを構築する前にネットワークの設定をする必要があります。

必要なポートの設定はInstalling kubeadm - Kubernetesが参考になります。

要するにMasterはWorkerからの6443、MasterからすべてのNodeへの10250が疎通できれば良さそうです。

またこれから利用するオーバーレイネットワークのWeaveNetで利用する6783ポートも開ける必要があります。

 

「仮想クラウドネットワーク」から(おそらくmasterという名前(初めに作ったVMと同じ名前になる?)目的のネットワークを選択し、さらにその中にあるサブネットを選択します(おそらく1つしかないです)。さらに、その中のセキュリティリストを選択し、下記のように設定します。

 

上のほうのルールはもとからあるものです。

f:id:inajob:20191006133749p:plain

ネットワークの設定(iptables

Oracle Cloudの設定とは別に各ホストのiptablesも設定する必要があります。

master

iptablesの設定は既存の設定に依存します。ここまでセットアップしたVMでは例えば下記のようになっているはずです。

# iptables -L INPUT --line-numbers
Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination
1    ACCEPT     all  --  anywhere             anywhere             state RELATED,ESTABLISHED
2    ACCEPT     icmp --  anywhere             anywhere
3    ACCEPT     all  --  anywhere             anywhere
4    ACCEPT     udp  --  anywhere             anywhere             udp spt:ntp
5    ACCEPT     tcp  --  anywhere             anywhere             state NEW tcp dpt:ssh
6   REJECT     all  --  anywhere             anywhere             reject-with icmp-host-prohibited

 行番号6のルールより前に通信を許可しないと、通信が捨てられてしまいます。

5行目のような(ここではSSHを許可している)ルールを追加していく必要があります。

iptables -I INPUT 6 -p tcp -m state --state NEW -m tcp --dport 6443 -j ACCEPT
iptables -I INPUT 6 -p tcp -m state --state NEW -m tcp --dport 6783 -j ACCEPT
iptables -I INPUT 6 -p tcp -m state --state NEW -m tcp --dport 10250 -j ACCEPT

 こんな感じで設定します。

Masterの構築

# kubeadm init --ignore-preflight-errors=swap

swapが有効だとうまくセットアップできないので、あえて無視するように設定します。

コマンドがうまく実行できるとkubeadm joinのスニペットが表示されます。

Workerの構築

# kubeadm join <前手順で生成されるものをコピペ> --ignore-preflight-errors=swap

 Workerでは基本的にはスニペットを実行するだけですが、ここでもswapを無視して進めるようにします。

kubectlでのアクセス

kubeconfigをコピーしてきます。(セキュリティ上はあまりよくないですが・・)

# cp /etc/kubernetes/admin.conf /home/ubuntu/
# chmod 644 admin.conf

 以降は一般ユーザで作業できます

 

$ export KUBECONFIG=/home/ubuntu/admin.conf
$ kubectl get nodes

 node一覧が表示されれば成功です。

ネットワークアドオンのデプロイ

 Pod間の通信を可能にするためにネットワークアドオンであるWeave Netをデプロイします。

$ kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"

 

Ingress Controllerのデプロイ

Webサービスを公開するためにIngress Controllerをデプロイします。

ここでは GitHub - zlabjp/nghttpx-ingress-lb: nghttpx ingress controller for Kubernetes を使うことにします。

 

まず該当するingressリソースがない場合に表示する404画面用のPodをデプロイします。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: default-http-backend
  namespace: kube-system
spec:
  selector:
    matchLabels:
      k8s-app: default-http-backend
  replicas: 1
  template:
    metadata:
      labels:
        k8s-app: default-http-backend
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: default-http-backend
        # Any image is permissible as long as:
        # 1. It serves a 404 page at /
        # 2. It serves 200 on a /healthz endpoint
        image: k8s.gcr.io/defaultbackend-amd64:1.5
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 30
          timeoutSeconds: 5
        ports:
        - containerPort: 8080
        resources:
          limits:
            cpu: 10m
            memory: 20Mi
          requests:
            cpu: 10m
            memory: 20Mi

 

apiVersion: v1
kind: Service
metadata:
  name: default-http-backend
  namespace: kube-system
  labels:
    k8s-app: default-http-backend
spec:
  selector:
    k8s-app: default-http-backend
  ports:
  - name: 8080-8080
    port: 8080
    targetPort: 8080
    protocol: TCP

次にingress-controllerが利用するServiceAccountとその権限を設定します。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nghttpx-ingress-serviceaccount
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: nghttpx-ingress-clusterrole
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
      - endpoints
      - nodes
      - pods
      - secrets
    verbs:
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - nodes
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - services
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - "networking.k8s.io"
    resources:
      - ingresses
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - ""
    resources:
        - events
    verbs:
        - create
        - patch
  - apiGroups:
      - "networking.k8s.io"
    resources:
      - ingresses/status
    verbs:
      - update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: nghttpx-ingress-role
  namespace: kube-system
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
      - pods
      - secrets
      - namespaces
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - configmaps
    resourceNames:
      # Defaults to "<election-id>-<ingress-class>"
      # Here: "<ingress-controller-leader>-<nginx>"
      # This has to be adapted if you change either parameter
      # when launching the nginx-ingress-controller.
      - "ingress-controller-leader-nginx"
    verbs:
      - get
      - update
  - apiGroups:
      - ""
    resources:
      - configmaps
    verbs:
      - create
  - apiGroups:
      - ""
    resources:
      - endpoints
    verbs:
      - get
      - create
      - update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: nghttpx-ingress-role-nisa-binding
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: nghttpx-ingress-role
subjects:
  - kind: ServiceAccount
    name: nghttpx-ingress-serviceaccount
    namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: nghttpx-ingress-clusterrole-nisa-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: nghttpx-ingress-clusterrole
subjects:
  - kind: ServiceAccount
    name: nghttpx-ingress-serviceaccount
    namespace: kube-system

 最後にingress-controllerをデプロイします。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nghttpx-ingress-controller
  namespace: kube-system
  labels:
    k8s-app: nghttpx-ingress-lb
spec:
  selector:
    matchLabels:
      k8s-app: nghttpx-ingress-lb
  template:
    metadata:
      labels:
        k8s-app: nghttpx-ingress-lb
        name: nghttpx-ingress-lb
    spec:
      serviceAccountName: nghttpx-ingress-serviceaccount
      terminationGracePeriodSeconds: 60
      hostNetwork: true
      containers:
      - image: zlabjp/nghttpx-ingress-controller:latest
        name: nghttpx-ingress-lb
        imagePullPolicy: Always
        livenessProbe:
          httpGet:
            path: /healthz
            # when changing this port, also specify it using --healthz-port in nghttpx-ingress-controller args.
            port: 11249
            scheme: HTTP
          initialDelaySeconds: 30
          timeoutSeconds: 5
        # use downward API
        env:
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: POD_NAMESPACE
            valueFrom:
              fieldRef:
                fieldPath: metadata.namespace
        ports:
        - containerPort: 80
          hostPort: 80
        - containerPort: 443
          hostPort: 443
        args:
        - /nghttpx-ingress-controller
        - --default-backend-service=$(POD_NAMESPACE)/default-http-backend
        - --healthz-port=11249
        - --logtostderr

 これで、このクラスタWebサービスをデプロイする準備が整いました。

追加のネットワーク設定

インターネットからの80番アクセスを許可するために追加でネットワークの設定が必要です。

 

f:id:inajob:20191006141934p:plain一番下にある80番ポートについてのルールを追加しました。

 

また、workerのiptablesも合わせて追加の設定が必要です。


iptables -I INPUT 6 -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT

 (INPUT 6の”6”はルールを見て適切な行番号を指定する必要があります。)

 

ここで、手元のPCからworkerのパブリックIPにアクセスをしてみると

f:id:inajob:20191006142222p:plain

このようにそっけない404ページが表示されるはずです。これはKubernetesクラスタ内のPodが返したものです。

Webアプリケーションのデプロイ

最後に、nginxをクラスタ上にデプロイして動作確認をしてみます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: test-service
spec:
  ports:
    - port: 80
  selector:
    app: nginx
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
spec:
  rules:
  - http:
      paths:
      - backend:
          serviceName: test-service
          servicePort: 80

 これをデプロイしたのちに、先ほどと同様にworkerのパブリックIPにアクセスすると

f:id:inajob:20191006142525p:plain

おめでとう!無事Kubernetesクラスタ上のアプリケーションにインターネットからアクセスできるようになりました。

まとめ

Oracle CloudのAlways Free Tierを使って、Master1台、Worker1台のKubernetesクラスタを作ることができました。

貧弱な構成ですが、無料の範囲内でインターネットからアクセスできるKubernetesクラスタを作れるのは面白いおもちゃになりそうと感じました。

また、Oracle Cloudは初めて使いましたが、普段使っているVPSと遜色なく利用することができ、特段詰まることもありませんでした。