みなさん、おはこんばんちわ〜
ドコモサービスイノベーション部ビックデータ担当1年目の平山です。
いきなりですが、Kubernetesって好きですか?僕は大好きです。
この記事では、そんな愛の深い僕だからこそ思いついたんじゃないか(使いたくてたまらなかった)なものを紹介しています。
アイデア的には結構面白いと自分では思っていて一応自慢の子です。
ぱっと思いついてそのままのノリでやったので明日見直したらそうじゃなくなってるかも知れませんが・・・
技術的にはEKSについてが中心の記事となります。お楽しみくだされば幸いです。

1. 概要

1.アプリ動作図3.png
EKSLoverによるEKSLoverのためのEKSLoverが興奮する(?)Webおみくじアプリシステムの構築記事です。
手順は以下の通りとなります。
(1) CFn(CloudFormation)によりネットワークを展開。
(2) Operator環境としてAWS Cloud9を準備・環境構築。
(3) コンテナイメージの管理先としてECR(Elactic Contianer Registory)を展開。
(4) EKS(Elactic Kubernetes Service)クラスタを展開。
(5) EKS上で僕の考えた最強のおみくじアプリを展開。
このYAMLエンジニアめと言われそうなAWSサービスの羅列ですね。
コンテナ・IaC・CI/CDに魅せられたエンジニアの末路なのかなと若輩者ながら思う今日この頃です。

1.アプリ動作図.png

GoogleChromeでアクセスした時の様子です。${DomainName}:${Port}によりアクセスします。
HTTP GETリクエストで動作する仕様にしているので、curlコマンドなどでも結果が表示されます(HTMLが整形されずそのまま返ってきますが・・・)。
1.アプリ動作図2.png
4パターンのどれかが表示されます。
大吉(10%)中吉(20%)小吉(60%)凶(10%)の排出率にしてあります。
これで今日の運勢調べられるぞヤッター!

2. はじめに(はじめじゃない)

上記アーキテクチャ図を見てみなさんどう思いましたか?ちょっと不可解なところははないでしょうか?そうですそうです。ELB(Elactic Load Balancer)から伸びる矢印に"おみくじを引く"って書いているところです。コンテナ大好きインフラエンジニアの方ならこの時点で何してるかわかるんですかね?
はじめに言っておきますが、少々トリッキーなEKSの使い方をしていると思います。アイデアとして個人的面白いかもと思ったのを趣味で実装してしまいました。

3. 環境構築

3.1 ネットワークの構築

3.1.1ネットワーク図.png
CFnによりネットワークを構築します。
テンプレートに入力するパラメータは

  • Stack name:omikuji-network
  • Project Prefix:omikuji
  • CIDR(VPC/Subnet * 2)

です。
この時CIDR1はEKSの都合上多く取っておいてください。理由は後述します。

cfn-base-resources.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: omikuji-base-environment

Metadata:
  AWS::CloudFormation::Interface :
    ParameterGroups:
      - 
        Label:
          default: "Project Name Prefix"
        Parameters:
          - PJPrefix
      - 
        Label:
          default: "Data Center"
        Parameters:
          - AvailabilityZone1
          - AvailabilityZone2
          - VPCCIDR
          - PublicSubnet1CIDR
          - PublicSubnet2CIDR

    ParameterLabels:
      PJPrefix:
        default: "Project Prefix"
      AvailabilityZone1:
        default: "Availability Zone 1"
      AvailabilityZone2:
        default: "Availability Zone 2"
      VPCCIDR:
        default: "VPC CIDR"
      PublicSubnet1CIDR:
        default: "Public Subnet 1 CIDR"
      PublicSubnet2CIDR:
        default: "Public Subnet 2 CIDR"

Parameters:
  PJPrefix:
    Type: String
    Default: omikuji
  AvailabilityZone1:
    Type: String
    Default: ap-northeast-1a
  AvailabilityZone2:
    Type: String
    Default: ap-northeast-1c
  VPCCIDR:
    Type: String
    Default: 10.10.0.0/16
    Description: X:0~255
  PublicSubnet1CIDR:
    Type: String
    Default: 10.10.1.0/24
    Description: "X:same as above, Y:0~255"
  PublicSubnet2CIDR:
    Type: String
    Default: 10.10.2.0/24
    Description: "X:same as above, Y:0~255"

Resources:

  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
      - Key: Name
        Value: !Sub ${PJPrefix}-vpc

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub ${PJPrefix}-igw

  AttachIGW:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PublicSubnet1CIDR
      AvailabilityZone: !Ref AvailabilityZone1
      MapPublicIpOnLaunch: 'true'
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${PJPrefix}-public-subnet-1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PublicSubnet2CIDR
      AvailabilityZone: !Ref AvailabilityZone2
      MapPublicIpOnLaunch: 'true'
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${PJPrefix}-public-subnet-2

  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${PJPrefix}-rt

  AssociateIGWwithRT:
    Type: AWS::EC2::Route
    DependsOn: AttachIGW
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  AssociateSubnet1withRT:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref RouteTable

  AssociateSubnet2withRT:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref RouteTable

Outputs:
  WorkerSubnets:
    Value: !Join
      - ","
      - [!Ref PublicSubnet1, !Ref PublicSubnet2]


3.2 Operator環境の構築

3.2Operator環境図.png
先ほど作成したネットワーク上にEKSを操作する環境を構築します。
読者のみなさんと環境揃えるためにAWS Cloud9という統合開発環境のサービスを利用します。
構築時の設定は

  • Create a new EC2 instance for environment (direct access)
  • t3.small (2 GiB RAM + 2 vCPU)
  • Amazon Linux 2 (recommended)

です。
こうしてできた環境で、

  1. EKSを操作するためのツールのインストール
  2. AWS Clous9のクレデンシャルの設定

をおこないます。

3.2.1 ツールのインストール

AWS公式のEKSユーザーガイドに沿っておこないます。

eksctlのインストール
curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
sudo mv /tmp/eksctl /usr/local/bin
eksctl version
kubectlのインストール
curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.18.9/2020-11-02/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin
kubectl version --short --client

3.2.2 クレデンシャルの設定

AWS Cloud9では、起動したユーザーの権限と同じ範囲のAMTC(Amazon Managed Temporary Credentials)という一時的な認証情報が払い出されるため、普段使いではこのようなことはしません。
しかし、EKS構築時に時間がかかるコマンド(20分くらい?)を打つ関係上、15分で有効期限が切れ新しいものに切り替わるという仕様のAMTCでの認証では、途中でToken expiredと怒られてしまいます。そのため、AWSマネジメントコンソールでIAMユーザーのアクセスキーを払い出し、それをCloud9に食わせます。

aws_credentialの設定
PROFILE_NAME=cluster-admin
aws configure --profile ${PROFILE_NAME}
export AWS_DEFAULT_PROFILE=${PROFILE_NAME} # defaultからcluster-adminのユーザーへのprofileの切り替え

aws configureをしたときに、AWSからせっかくAMTCあげてるのになんで自分でクレデンシャル塗り替えようとするの?って言われちゃいますが、EKSをコマンドで構築するためには仕方ありません。ゴリ押しましょう。以下は、怒られている様子です。
3.2.2AMTC.png

今回は認証部分にこだわるのは本質的でない(僕はEKSをごにょごにょしたいだけなんだ!!)のでやりませんが、本来、IAMのアクセスキーを食わしたまま運用するのはセキュリティ的に非常によろしくないです。なので、商用サービスであったりするなら、必要な時だけ使ってそのフェーズが過ぎれば、他のクレデンシャルにかえます。本案件も構築が終わってからはAMTCに戻すべきです(操作するのも記事を書くのもやりたくないので今回はしないですが)。ボタンぽちぽちで戻せます。

また、Kubernetes(EKSの中で実際に動いているのはこれ)の設定でRoleの情報をもとに認証を行うRBAC(Role-Based Access Control)を扱うことでも認証は可能です。実際、私が業務においてEKSで運用しているシステムは、構築終了後はEKSを操作するリソースが持つIAM Roleに対して、このRBACの設定をして運用し、アクセスキーは消し去るようにしています。少なくとも私が所属するチームでは、商用システムのユーザーでアクセスキーが1日以上残り続けたことはきっとないはずです・・・知らないですが・・・

ここはインシデントに関わる重要なことなので少し丁寧めに書いてしまいました・・・良く分かんなかったら飛ばして結構です。僕がこの記事で共有したいのはここじゃないので!

3.3 コンテナイメージの準備

ECRを構築し、そこに本システムで使うコンテナを格納します。
ここから、釣りタイトルの"僕にしか思いつけない"とした部分の実装の仕方がだんだん見えてきます。
今回用意したコンテナイメージは全部で4つです。

  • great-fortune(大吉)
  • middle-fortune(中吉)
  • small-fortune(小吉)
  • misfortune(凶)

です。以下のアセット類をビルドしたものになります。

Dockerfile
FROM node:15.1.0-stretch-slim
WORKDIR /app
COPY index.js /app/index.js
COPY package.json /app/package.json
RUN npm install
RUN useradd -m -u 1009 app
USER app
EXPOSE 8081
CMD ["npm", "start"]

ちなみにDockerfileの書き方がテキトーすぎてすみません。あまりにも急いでいたのでレイヤー構造など全く意識せずにテキトー実装してしまった2ので、コレはひどい書き方ですね。

index.js
const express = require('express');
const os = require('os');
const app = express();

app.get('/', (req, res) => {
    const hostname = os.hostname();
    res.send(`<!DOCTYPE html>
<html lang=“ja”>
<head>
<meta charset=“UTF-8”>
<title>
おみくじ
</title>
<style>
p {font-size:20px;}
.fortune {
color:red;
font-size: 30px;
font-weight: bold;
}
</style>
</head>
<body>
<h1></h1>
<p>あなたの運勢は</p>
<p class="fortune">大吉</p>
<p>です</p>
</body>
</html>`);
});

app.listen(8081);
console.log('Example app listening on port 8081.');
package.json
{
"name": "app",
"version": "1.0.0",
"description": "Container App",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1"
}
}

index.jsに主にコンテナの挙動が書いてあります。
HTTP GETリクエストをポート:8081に受けると、HTML形式のメッセージを返す仕様です。
4つのコンテナの違いは、メッセージの表す内容が大吉中吉小吉になっていることです。

コンテナ大好きインフラエンジニアの皆さん、今回の実装方法だんだんわかって来ましたかね?

4. EKSクラスタの作成

ここは非常に爽快です。たった一つのコマンド+パラメータでコンテナを管理する素晴らしいシステムが出来上がります。魔法で巨大建造物を作っているかのよう。顕現せよ、混沌に満ちた世界3において、我に安定運用への光を示せ!!クーヴァネティス!!

create_cluster.sh
subnet=subnet-00xxxxxxxxxxxxx,subnet-00xxxxxxxxxxxxx

eksctl create cluster \
--name eks-handson-cluster \
--version 1.18 \
--region ap-northeast-1 \
--vpc-public-subnets ${subnets} \
--nodegroup-name omikuji-nodegroup \
--node-type t2.small \
--nodes 1 \
--nodes-min 1 \
--nodes-max 1000 \
--asg-access

このeksctl create clusterコマンドを実行するだけで、内部で2つのCFnが動き、必要なものを全て作ってくれます。ここで20分ほどかかるので、Cloud9でAMTCが使えなかったんですよね。
4EKScluster.png
Cloud9のターミナルのログを見るとなにしてるかわかりますね。ここ見ながら、マネコンで実際何作られるかとか細かく見ていくと20分かかっても許せます。こんなに作ってくれてるのかと・・・
これで、いよいよコンテナをバシバシ走らせて管理していけますね!やっとです。現状は以下のような状態になっています。
4EKSC.png

5. ぼくがかんがえたさいきょうのおみくじアプリの実装

ここまではAWSのプラットフォームでの構築でした。ここからは、AWSリソースで構築されてはいるもののKubernetesというプラットフォームでの構築になります。

Kubernetesではyaml形式で書かれたマニフェストファイルにより宣言的にAPIを呼ぶことで、あとはシステムがよしなにその望む状態のシステムを構築してくれます。そのメタ的に管理してくれる部分をコントロールプレーンノード(マスターノードともいう)と言い、対して実際にコンテナが動き仕事をする部分をワーカーノードと言います。なのでこれ以降の操作は非常に簡単で、マニフェストファイルを作成し、それをkubectl applyするだけです。
とても楽だなぁ。コンテナのオーケストレーションっていうのも納得です。
5eksope.png
さて、ようやく"EKS Loverな僕にしか思いつけない最強の"部分にきました(笑)。なんかここまできて、いや案外そういう実装あるのかもなとか思い始めて来たなぁ。でももう時間ないしコレでいくしかないんだ・・・
ここで、ネタバラシです。実装方法としては、おみくじにそのものに値するようなコンテナ(つまり大吉とか内容を表示するもの)を複数作り、その前にロードバランサのようなもの(正確にはKubernetesのService.LoadBalancerオブジェクト)をかませて、投げたGETリクエストのバランシングによりランダム性を持たせるというものです。システムの設計がオブジェクト指向に則ってます。ロードバランサがおみくじコンテナを引いている感じですね。

ランダム性は、今回はService.NordPortオブジェクトに依存しています。あれ?さっきService.LoadBalancerオブジェクトって言ってたやんって思った人、鋭いですね・・・(数行前の話ではありますが)。実はService.NordPortオブジェクトはService.LoadBalancerオプジェクト利用時に自動的に割り当てられるんです。なので、Service.LoadBalancerオプジェクトを使う時にService.NordPortオブジェクトが自動的に割り当てられ、その内部仕様はデフォルトではiptablesでのルーティングを採用していて、その設定はランダムに割り振るようになっているが故に、ランダムにおみくじを引いてくれるというわけです、ちょっとややこしいですかね。まぁとりあえずランダムなんです。

Kubernetesでの手順は以下の通りとなります。
(1)オートスケールできるようにするための設定
(2)各コンテナの展開(Deployment)
(3)ロードバランサの展開(Service:LoadBalancer)

5.1 オートスケールできるようにするための設定

これは、もしはじめから展開するコンテナ(正確にはPod)の数がわかっているなら、それに合わせてノードの数を指定して展開すればいいのですが、EKS使うからには、ノードの数の管理もKubernetesに任せたいが私の信条なので実装しました。オートスケールしてるのみると「おぉ、いいなぁ」ってなりますもんね!
eksctl create clusterで構築した時の設定では、ワーカーノードはASG(AutoScaling)で作られています。つまりAWSのプラットフォーム上ではノードはスケールすることができるようリソースが展開されています。しかし、EKSを使う場合、Kubernetesというプラットフォーム上でコンテナに関するリソースを管理していく事になります。そのため、KubernetesがASGに働きかけられるようにするために、Cluster Autoscalerというものを展開する必要があります。

AWS公式がそのためのマニフェストファイルを用意してくれているので、それに従って、設定しましょう。
cluster-autoscaler-autodiscover.yamlがダウンロードできるので、それにクラスタ名を自分の環境のものにして、kube applyするだけです!

5.2 各コンテナの展開(おみくじの補充)

deployment_great_fortune.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: great-fortune
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 2
      maxSurge: 4
  replicas: 1
  selector:
    matchLabels:
      sample-label: omikuji-app
  template:
    metadata:
      labels:
        sample-label: omikuji-app
    spec:
      containers:
        - name: omikuji-great-fortune
          image: 262403382529.dkr.ecr.ap-northeast-1.amazonaws.com/omikuji/fortune:great-fortune
          ports:
          - containerPort: 8081

上記のようなマニフェストファイルをkubectl applyします。
こちらはgreat_fortuneなので大吉のおみくじコンテナなのですが、replicas: 1となっているので、1つ展開されています。
今回、大吉(10%)中吉(20%)小吉(60%)凶(10%)の排出率するので

  • 大吉 replicas: 1
  • 中吉 replicas: 2
  • 小吉 replicas: 6
  • replicas: 1

で展開して、おみくじ(コンテナ)を箱(ワーカーノード)に入れましょう!

そしてここで、CIDRを多めに切っておいてくださいといった先程のフラグを回収します。
ではなぜ多めにと言ったかというと、Pod1つ構築するたびににIPアドレスが1つ消費されるからです。従って、IPアドレスを絞りすぎた場合、IPアドレスの数の制限のせいでPodを起動できなくなることもあるので、ここは実運用でCIDRを切るときはその辺も考慮して設定をしなければいけません。今回は雑に多めに切りました。

5.3 ロードバランサの展開(おみくじを販売開始)

ロードバランサ.png

今のままでは、展開したコンテナたちにはアクセスすることができません。エンドポイントがないからです。
それを実現するのがService APIの中のLoadBalacerオブジェクトです(コレはKubernetes側のオブジェクトなので注意)
今回EKSを用いているので、KubernetesのService.LoadBalancerオブジェクトを作成すると、実態としてはAWSのロードバランサであるELB(ClassicLoadBalancerの方)が構築されます。

細かい説明は置いておいて、とりあえず以下のマニフェストファイルをkubectl applyしましょう。

service_loadbalancer.yaml
apiVersion: v1
kind: Service
metadata:
  name: loadbalancer
spec:
  type: LoadBalancer
  ports:
  - name: "http-port"
    protocol: "TCP"
    port: 8080
    nodePort: 30081
    targetPort: 8081
  selector:
    sample-label: omikuji-app

このマニフェストファイルでは、selectorとしてsample-labelを指定しています。すると、コントロールプレーンにより、同じsample-labelを持ち、その値が一致する(今回はomikuji-app)するPodがロードバランシングする先として登録されます。

上のELBの話と合わせて、頭がごっちゃになりかねないので、ここでトラフィックの通るルートをおさらいしておきましょう。
1. ユーザーからのGETリクエストがELBに
2. ELBからService.NordPortオブジェクト
3. Service.NodePortオブジェクトからランダムに選ばれたPod
といった具合です。詳しくはKubernetesの公式ドキュメントなどをみると内部実装などがわかると思います。

それではロードバランサのエンドポイントを確認したいので、実際にAWSのマネコンで見てみましょう。
5_load.png
マニフェストファイル通りKubernetesがよしなにやってくれていることがわかりますね。それではここのDNS nameを使ってアクセスして動きを確認しましょう。Chromeのアクセスバーに${Domain name}:${Port}でポート番号を指定しながらURIにGETリクエストを投げることができます。今回ロードバランサのポートは8080を開けている(上記マニフェストファイルで指定)ので、XXXX・・・.elb.amazonaws.com:8080と入力します。

アプリ動作4.png

おみくじが無事引けました。何度か試行する場合、ブラウザからリクエストを送る方法だとどこかに何かがキャッシュ4されているみたいでそのままの結果が返って来てしまいます(F5連打はうまくいかない)。そのためランダム性をすぐに確認したい場合は、curlリクエストですると毎回結果が変わって、バランシングのランダム性が体感できると思います。

curlにより、100回おみくじを引くコマンド
for itr in `seq 100`; do curl a9809XXXXXXXXXXXXXXXXXXXXXXXXXX.ap-northeast-1.elb.amazonaws.com:8080 --silent | grep -G "<p\sclass=\"fortune\">"; done

くじ結果.png

最後にKubernetesのリソースたちを載せておきます。
resources.png

6 まとめ

いかがだったでしょうか。記事を開く前にこの実装の仕方思いついた人いますか?
実用性や有用性はともかくタイトル詐欺にならず、ちょっと面白いなと思っていただけたら幸いです。ぱっと思いついてそのまま実装して、急いで執筆したのであまり色々は深く考えていないので、ツッコミどころもあるかと思いますが、共有していただけたら嬉しいです。

この記事で、EKS(というよりKubernetes)の基本は勉強できるかと思います。
ローカルPCの環境に依存しないように作ったので、興味が湧いた人はぜひ試してみてください。

それでは、終わりm...えっ?こんなのアプリのロジックはPythonやC++で乱数使って一瞬で実装できるし、AWS使うならAPIゲートウェイやLambda使えば簡単に同じようなもの実装できるじゃないかって??
うるさいです。なんでもapplyでこういうようにしといてねって言ったらその通りにしてくれるEKSちゃん可愛いじゃないですか。結構愛で実装してしまいましたが、いずれ面白いもの作って見せますよ!!

ここまでみてくださった方、誠にありがとうございました。それではっ!!


  1. Classless Inter-Domain Routing。クラスを使わずにIPアドレスの仕組みです。細かいことは今はどうでもいいので、ここではそのネットワークで使えるIPアドレスの範囲を決めていると思えばいいです。 

  2. レイヤー的に変更されにくいnpm installなどは前の方に持ってくるべきです。逆にCOPYとかは後ろの方が良いですね。今のままだと例えば、COPY元のindex.jsに少し変更を加えると、それ以降のレイヤーが全て更新されます。index.jsに手を加える度にnpm installが走ってしまうというわけですね・・・ぼっ僕はコレを伝えるために、嫌だなぁと思いながら敢えてこんな実装にしたんですよ、ええまぁ・・・(目は合わせられない) 

  3. この世の中、予測できない不都合なことは起こりますよね。突如トラフィックの謎スパイクが生じるとか良く聞きます。僕自身は若輩ゆえ、まだ経験したことないですが・・・ 

  4. GoogleChromeの検証ツールでブラウザのキャッシュを切ってもF5連打でおみくじはすぐには変わらなかったです。ブラウザでも少し時間おけば違う結果返ってくるんですが、ちょっとこの辺よくわかってないです・・・有識者のどなたか、怪しい箇所思いついたらどうか教えてくださいませ・・・