みなさん、おはこんばんちわ〜
ドコモサービスイノベーション部ビックデータ担当1年目の平山です。
いきなりですが、Kubernetesって好きですか?僕は大好きです。
この記事では、そんな愛の深い僕だからこそ思いついたんじゃないか(使いたくてたまらなかった)なものを紹介しています。
アイデア的には結構面白いと自分では思っていて一応自慢の子です。
ぱっと思いついてそのままのノリでやったので明日見直したらそうじゃなくなってるかも知れませんが・・・
技術的にはEKSについてが中心の記事となります。お楽しみくだされば幸いです。
1. 概要
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に魅せられたエンジニアの末路なのかなと若輩者ながら思う今日この頃です。
GoogleChromeでアクセスした時の様子です。${DomainName}:${Port}
によりアクセスします。
HTTP GETリクエストで動作する仕様にしているので、curlコマンドなどでも結果が表示されます(HTMLが整形されずそのまま返ってきますが・・・)。
4パターンのどれかが表示されます。
大吉(10%)中吉(20%)小吉(60%)凶(10%)の排出率にしてあります。
これで今日の運勢調べられるぞヤッター!
2. はじめに(はじめじゃない)
上記アーキテクチャ図を見てみなさんどう思いましたか?ちょっと不可解なところははないでしょうか?そうですそうです。ELB(Elactic Load Balancer)から伸びる矢印に**"おみくじを引く"**って書いているところです。コンテナ大好きインフラエンジニアの方ならこの時点で何してるかわかるんですかね?
はじめに言っておきますが、少々トリッキーなEKSの使い方をしていると思います。アイデアとして個人的面白いかもと思ったのを趣味で実装してしまいました。
3. 環境構築
3.1 ネットワークの構築
CFnによりネットワークを構築します。
テンプレートに入力するパラメータは
- Stack name:omikuji-network
- Project Prefix:omikuji
- CIDR(VPC/Subnet * 2)
です。
この時CIDR1はEKSの都合上多く取っておいてください。理由は後述します。
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環境の構築
先ほど作成したネットワーク上にEKSを操作する環境を構築します。
読者のみなさんと環境揃えるためにAWS Cloud9という統合開発環境のサービスを利用します。
構築時の設定は
- Create a new EC2 instance for environment (direct access)
- t3.small (2 GiB RAM + 2 vCPU)
- Amazon Linux 2 (recommended)
です。
こうしてできた環境で、
- EKSを操作するためのツールのインストール
- AWS Clous9のクレデンシャルの設定
をおこないます。
3.2.1 ツールのインストール
AWS公式のEKSユーザーガイドに沿っておこないます。
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
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に食わせます。
PROFILE_NAME=cluster-admin
aws configure --profile ${PROFILE_NAME}
export AWS_DEFAULT_PROFILE=${PROFILE_NAME} # defaultからcluster-adminのユーザーへのprofileの切り替え
aws configure
をしたときに、AWSからせっかくAMTCあげてるのになんで自分でクレデンシャル塗り替えようとするの?って言われちゃいますが、EKSをコマンドで構築するためには仕方ありません。ゴリ押しましょう。以下は、怒られている様子です。
今回は認証部分にこだわるのは本質的でない(僕は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(凶)
です。以下のアセット類をビルドしたものになります。
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ので、コレはひどい書き方ですね。
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.');
{
"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において、我に安定運用への光を示せ!!クーヴァネティス!!
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が使えなかったんですよね。
Cloud9のターミナルのログを見るとなにしてるかわかりますね。ここ見ながら、マネコンで実際何作られるかとか細かく見ていくと20分かかっても許せます。こんなに作ってくれてるのかと・・・
これで、いよいよコンテナをバシバシ走らせて管理していけますね!やっとです。現状は以下のような状態になっています。
5. ぼくがかんがえたさいきょうのおみくじアプリの実装
ここまではAWSのプラットフォームでの構築でした。ここからは、AWSリソースで構築されてはいるもののKubernetesというプラットフォームでの構築になります。
Kubernetesではyaml形式で書かれたマニフェストファイルにより宣言的にAPIを呼ぶことで、あとはシステムがよしなにその望む状態のシステムを構築してくれます。そのメタ的に管理してくれる部分をコントロールプレーンノード(マスターノードともいう)と言い、対して実際にコンテナが動き仕事をする部分をワーカーノードと言います。なのでこれ以降の操作は非常に簡単で、マニフェストファイルを作成し、それをkubectl apply
するだけです。
とても楽だなぁ。コンテナのオーケストレーションっていうのも納得です。
さて、ようやく**"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 各コンテナの展開(おみくじの補充)
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 ロードバランサの展開(おみくじを販売開始)
今のままでは、展開したコンテナたちにはアクセスすることができません。エンドポイントがないからです。
それを実現するのがService APIの中のLoadBalacerオブジェクトです(コレはKubernetes側のオブジェクトなので注意)
今回EKSを用いているので、KubernetesのService.LoadBalancerオブジェクトを作成すると、実態としてはAWSのロードバランサであるELB(ClassicLoadBalancerの方)が構築されます。
細かい説明は置いておいて、とりあえず以下のマニフェストファイルをkubectl apply
しましょう。
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の話と合わせて、頭がごっちゃになりかねないので、ここでトラフィックの通るルートをおさらいしておきましょう。
- ユーザーからのGETリクエストがELBに
- ELBからService.NordPortオブジェクト
- Service.NodePortオブジェクトからランダムに選ばれたPod
といった具合です。詳しくはKubernetesの公式ドキュメントなどをみると内部実装などがわかると思います。
それではロードバランサのエンドポイントを確認したいので、実際にAWSのマネコンで見てみましょう。
マニフェストファイル通りKubernetesがよしなにやってくれていることがわかりますね。それではここのDNS nameを使ってアクセスして動きを確認しましょう。Chromeのアクセスバーに${Domain name}:${Port}
でポート番号を指定しながらURIにGETリクエストを投げることができます。今回ロードバランサのポートは8080
を開けている(上記マニフェストファイルで指定)ので、XXXX・・・.elb.amazonaws.com:8080
と入力します。
おみくじが無事引けました。何度か試行する場合、ブラウザからリクエストを送る方法だとどこかに何かがキャッシュ4されているみたいでそのままの結果が返って来てしまいます(F5連打はうまくいかない)。そのためランダム性をすぐに確認したい場合は、curlリクエストですると毎回結果が変わって、バランシングのランダム性が体感できると思います。
for itr in `seq 100`; do curl a9809XXXXXXXXXXXXXXXXXXXXXXXXXX.ap-northeast-1.elb.amazonaws.com:8080 --silent | grep -G "<p\sclass=\"fortune\">"; done
6 まとめ
いかがだったでしょうか。記事を開く前にこの実装の仕方思いついた人いますか?
実用性や有用性はともかくタイトル詐欺にならず、ちょっと面白いなと思っていただけたら幸いです。ぱっと思いついてそのまま実装して、急いで執筆したのであまり色々は深く考えていないので、ツッコミどころもあるかと思いますが、共有していただけたら嬉しいです。
この記事で、EKS(というよりKubernetes)の基本は勉強できるかと思います。
ローカルPCの環境に依存しないように作ったので、興味が湧いた人はぜひ試してみてください。
それでは、終わりm...えっ?こんなのアプリのロジックはPythonやC++で乱数使って一瞬で実装できるし、AWS使うならAPIゲートウェイやLambda使えば簡単に同じようなもの実装できるじゃないかって??
うるさいです。なんでもapply
でこういうようにしといてねって言ったらその通りにしてくれるEKSちゃん可愛いじゃないですか。結構愛で実装してしまいましたが、いずれ面白いもの作って見せますよ!!
ここまでみてくださった方、誠にありがとうございました。それではっ!!
-
Classless Inter-Domain Routing。クラスを使わずにIPアドレスの仕組みです。細かいことは今はどうでもいいので、ここではそのネットワークで使えるIPアドレスの範囲を決めていると思えばいいです。 ↩
-
レイヤー的に変更されにくい
npm install
などは前の方に持ってくるべきです。逆にCOPY
とかは後ろの方が良いですね。今のままだと例えば、COPY元のindex.jsに少し変更を加えると、それ以降のレイヤーが全て更新されます。index.jsに手を加える度にnpm install
が走ってしまうというわけですね・・・ぼっ僕はコレを伝えるために、嫌だなぁと思いながら敢えてこんな実装にしたんですよ、ええまぁ・・・(目は合わせられない) ↩ -
この世の中、予測できない不都合なことは起こりますよね。突如トラフィックの謎スパイクが生じるとか良く聞きます。僕自身は若輩ゆえ、まだ経験したことないですが・・・ ↩
-
GoogleChromeの検証ツールでブラウザのキャッシュを切ってもF5連打でおみくじはすぐには変わらなかったです。ブラウザでも少し時間おけば違う結果返ってくるんですが、ちょっとこの辺よくわかってないです・・・有識者のどなたか、怪しい箇所思いついたらどうか教えてくださいませ・・・ ↩