ansible の command module を使用して複数ホストのログを確認する

複数ホストへコマンドを一括実行したい時

複数ホストに一括してコマンドを実行したい時がたまにあります。繰り返す作業ではなく、ざっとログにエラーメッセージが出ていないかなどをアドホックに確認したい時など。これは ansible の command module を利用することで実現できます。

$ pyenv exec ansible -i inventory webservers -u ec2-user --become -K --become-user root -a 'bash -c "grep slow_flush_log_threshold /var/log/td-agent/td-agent.log"'

$ cat inventory
[webservers]
web001
web002
web003

ansible.builtin.command の他にも ansible.builtin.shell モジュールもあります。パイプやリダイレクトはじめちょっと凝ったことをやりたい場合はこちらのほうがいいかもしれません。

So far all our examples have used the default ‘command’ module. To use a different module, 
pass -m for module name. For example, to use the ansible.builtin.shell module:

$ ansible raleigh -m ansible.builtin.shell -a 'echo $TERM'

https://docs.ansible.com/ansible/latest/user_guide/intro_adhoc.html

同様の作業(run commands on multiple hosts via ssh) を昔は pdshcapistranocap shell を使用して実行していました。対象ホストが3桁を超えるとさすがに煩雑になりますが、問題解決にあたってログの標本を抽出して確認したい場合なのには有用だと思います。

参考情報

ひとつの入力語に対して複数カラムを対象にして検索を行い結果をまとめる

「空気を読む」検索

Rails の検索には Ransack が重用されます。Ransack では基本的にモデルの単一カラムに対して cont(contain) や eq(equal) といった条件で検索が可能です。この場合、ユーザーがひとつの用語で複数のカラムを検索したい場合、別々の入力フォームで複数回の入力を求めることになります。ここで、ひとつの検索語で複数カラムを一気に実行できたらどうでしょうか。これはいわゆる「空気を読む」検索と言えるかもしれません。

ゲームでの例を挙げます。例えば「龍飛」という入力をして検索をするユーザーはアイテム名「龍飛剣」を探しているのかもしれませんし、スキル名「龍飛の心得」を探しているのかもしれません。もしくは両方かもしれません。

Ransack でこの「空気を読む」検索を実現するためには入力語に対してモデルの複数カラムで検索を行い、結果の集約(と重複排除やソート)を行う必要があります。ここで Ruby の Runnable Object(Proc) を使用すると便利です。この実装で The Well-Grounded Rubyist, Third Edition の Chapter 14 で紹介されている Callable and runnable objects をはじめて有効活用できた気がします。

class EndpointsController < ApplicationController
  def search
    @input = params[:input]

    if (@input.empty?) then
      @input = "nothing"
    end

    @items = Item.search(@input)
    @skills = Skill.search(@input)

    @outcome = empty?([@items, @skills])
  end

  def empty?(arg)
    (... snip ...)
  end
class Item < ApplicationRecord

  class << self

    def search(arg)
      result = {}
      result[:by_name] = search_by_name(arg)
      result[:by_description] = search_by_description(arg)
      result[:by_parameter] = search_by_parameter(arg)
      result
    end

    def search_by_name(arg)
      p = Proc.new do |x|
        { name: x.name, level: x.level, description: x.description,
          rarity: x.rarity.name, skills: x.skills.map(&:name) }
      end
      Item.ransack(name_cont: arg).result.map(&p) end
    end

    def search_by_description(arg)
      (... snip ...)
    end

(... snip ...)

参考情報

www.manning.com

AWS CDK で EC2 を構築する時の地雷処理

古い AWS アカウントの呪い(?)

私の AWS アカウントでは Tokyo リージョン(ap-northeast-1) の Availability Zone の a が使用できません。使用できるのは b と c だけです。これはアカウントを作成した時期(2011年)に関係しているようです。比較的新しい時期にアカウントを作成した人は Availability Zone の a が利用可能なようです。

この初期勢にまつわるアカウント呪い(?)によって、公開されている CFn や CDK のサンプルで「他の人のアカウント(新しめ)では動く」のに「自分のアカウント(古め)では動かない(古めのアカウント)」ことが稀によくあります。

解決方法として AWS CDK のドキュメント Control over availability zones を参照したところ、コンストラクタが使用可能な Availability Zone の getter を設定してこのアカウントの呪いを教えてあげる必要があることが判明しました。

$ basename $PWD
aws-cdk-examples

$ git diff
diff --git a/typescript/ec2-instance/lib/ec2-cdk-stack.ts b/typescript/ec2-instance/lib/ec2-cdk-stack.ts
index 4cfdec0..bc5b5f9 100644
--- a/typescript/ec2-instance/lib/ec2-cdk-stack.ts
+++ b/typescript/ec2-instance/lib/ec2-cdk-stack.ts
@@ -45,13 +45,13 @@ export class Ec2CdkStack extends cdk.Stack {
     // Use Latest Amazon Linux Image - CPU Type ARM64
     const ami = new ec2.AmazonLinuxImage({
       generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
-      cpuType: ec2.AmazonLinuxCpuType.ARM_64
+      cpuType: ec2.AmazonLinuxCpuType.X86_64
     });

     // Create the instance using the Security Group, AMI, and KeyPair defined in the VPC created
     const ec2Instance = new ec2.Instance(this, 'Instance', {
       vpc,
-      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MICRO),
+      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.NANO),
       machineImage: ami,
       securityGroup: securityGroup,
       // keyName: key.keyPairName,
@@ -77,4 +77,9 @@ export class Ec2CdkStack extends cdk.Stack {
     new cdk.CfnOutput(this, 'Download Key Command', { value: 'aws secretsmanager get-secret-value --secret-id ec2-ssh-key/cdk-keypair/private --query SecretString --output text > cdk-key.pem && chmod 400 cdk-key.pem' })
     new cdk.CfnOutput(this, 'ssh command', { value: 'ssh -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + ec2Instance.instancePublicIp })
   }
+
+  get availabilityZones(): string[] {
+    return ['ap-northeast-1b', 'ap-northeast-1c'];
+  }
+
 }

Getting started with Amazon EKS の地雷処理 もそうですが、Availability Zone 関係は地雷が多い印象です。

AWS CDK の印象

編集 > 実行のサイクルが自動化される watch 機能 や try&error のサイクルを早める --hotswap オプション があります。API の動作確認と実装作業を並行して進められ 各リソースのテスト も可能です。

Imperative approach な IaC の急先鋒として

個人的に Declarative approach(what) な CloudFormationImperative approach(how) な CDK を比べると後者は YAML から解放され、「書いていても鬱にならない」 IaC だなと思いました(高度に抽象化されていてやりたいことの書き方が見つからないなど短所もありますが...)

参考情報

Tenets of SRE を Github Issues/Pull Requests のラベルにする

Tenets of SRE を Github Issues/Pull Requests のラベルにする

Github にデフォルトで用意されているラベル(bug, duplicate, enhancement, ...) はアプリ開発向けです。インフラ開発向けの課題管理には SRE Book で定義されている Tenets of SRE(availability, latency, performance, efficiency, change management, monitoring, emergency response, and capacity planning) をラベルにすると管理しやすいと思います。どのラベルでの課題が最も多いのかも、意思決定に活用できます。

SRE(= Site Reliability Engineering) という言葉を生み出した Benjamin Treynor Sloss 自身が こちら で SRE Book を引用しつつ各 tenets を分解してわかりやすく説明しています。例えば、provisioning を change management と capacity planning と結びつけて説明するだけでなく、管理コスト/調達時間がかかる capacity (expensive と表現) と対比して「必要な時に素早く行えること」と定義しています。 その他 demand forecasting を organic growth と inorganic growth に分類して予測するなど各 tenet を実際にどうやって実装していくかについてヒントとなるものが多いのではないでしょうか。

参考情報

バッチ処理を cron から k8s cronjob に移行する

Shamshir のスタンドアローン版を k8s(minikube) の cronjob に移植した記録です。

cron でバッチを動かす時の問題点

node, ruby, python 他インタプリンタ系の言語でプログラムを作ると nodenv + npm/yarn, rbenv + bundler, pyenv + pip などのエコシステムを使って環境構築することになります。

これを cron で動かそうとすると ~/.nodenv/shims/node などエコシステム上の実行ファイルを crond に教えてあげる必要が出てきます。このため起動方法を bash -l cron.sh など(-l: Make bash act as if it had been invoked as a login shell (see INVOCATION below) 個人の動作環境をシミュレートするような特別な「忖度」が必要になってくる場合も。

このため 自分の環境では動くのに crontab に設定すると動かない! どうして!? となってしまうことが稀によくあります。(^_^; エコシステムを切り出したコンテナを作ることでこういった問題は解決でき、デプロイ環境が安定します。

もともとコンテナの定義として Content-agnostic, Infrastructure-agnostic という agnostic(= someone who believes that people cannot know whether God exists or not 神様がいるかどうかなんかわかりっこない = 神がいようといまいと関係ない: X に依存しない?) という概念があります。これが、コンテナが Content-agnostic (中身がスクリプトであろうがバイナリであろうが何のプログラミング言語で書かれていようが適切なイメージであれば動く)、Infrastructure-agnostic(物理でも仮想でもLinuxでもMacでも runc や gVisor といった OCI ランタイムがあればそこで差分を吸収して関係なく動く) と呼ばれる所以だと思います。

k8s cronjob はシンプルなバッチ実行環境ですが、プロダクション環境では依存関係などを設定可能な argo などのワークフローに載せることで処理の分割、依存関係の設定、部分実行/再実行がしやすくなるはずです。

k8s cronjob に移植する時の注意点

  • minikube でレジストリからイメージを DL せずローカルのイメージを使う場合( imagePullPolicy: Never )、eval $(minikube docker-env) で minikube の仮想 worker の docker context に切り替えて docker build する必要がある(下記 docker context の切り替えを参照)

docker context の切り替え

1. もともとの docker context は DOCKER ENDPOINT に unix socket 通信を使う

$ docker context ls
NAME        DESCRIPTION                               DOCKER ENDPOINT               KUBERNETES ENDPOINT                   ORCHESTRATOR
default *   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock   https://192.168.49.2:8443 (default)   swarm

$ docker images
REPOSITORY                    TAG         IMAGE ID       CREATED          SIZE
thetitle                      latest      203fc3c16979   25 minutes ago   125MB
thetitle                      v1.0        203fc3c16979   25 minutes ago   125MB
kyagi/thetitle                1.0         22d1a9e10b4a   42 minutes ago   125MB
node                          16-alpine   0e1547c0f4a4   3 weeks ago      110MB
gcr.io/k8s-minikube/kicbase   v0.0.29     64d09634c60d   2 months ago     1.14GB

2. eval $(minikube docker-env) で切り替えると

$ eval $(minikube docker-env)

3. minikube の仮想 worker の docker context になり DOCKER ENDPOINT が tcp://192.168.x.x になる。minikube にイメージを渡すためにはこちらのコンテキストで docker build する必要がある

$ docker context ls
NAME        DESCRIPTION                               DOCKER ENDPOINT           KUBERNETES ENDPOINT                   ORCHESTRATOR
default *   Current DOCKER_HOST based configuration   tcp://192.168.49.2:2376   https://192.168.49.2:8443 (default)   swarm
Warning: DOCKER_HOST environment variable overrides the active context. To use a context, either set the global --context flag, or unset DOCKER_HOST environment variable.

$ docker images
REPOSITORY                                TAG         IMAGE ID       CREATED             SIZE
shamshir                                  v1.0        145688e57875   About an hour ago   165MB
thetitle                                  v1.0        39a4338f4a4f   About an hour ago   125MB
<none>                                    <none>      da981f50f1eb   2 hours ago         125MB
<none>                                    <none>      f062c5aecb69   2 hours ago         125MB
kyagi/kubia                               latest      d5e0f5c0c6f0   13 days ago         906MB
node                                      16-alpine   0e1547c0f4a4   3 weeks ago         110MB
busybox                                   latest      ec3f0931a6e6   3 weeks ago         1.24MB
node                                      14-alpine   755b96824e40   4 weeks ago         119MB
k8s.gcr.io/kube-apiserver                 v1.23.1     b6d7abedde39   2 months ago        135MB
k8s.gcr.io/kube-proxy                     v1.23.1     b46c42588d51   2 months ago        112MB
k8s.gcr.io/kube-scheduler                 v1.23.1     71d575efe628   2 months ago        53.5MB
k8s.gcr.io/kube-controller-manager        v1.23.1     f51846a4fd28   2 months ago        125MB
k8s.gcr.io/etcd                           3.5.1-0     25f8c7f3da61   4 months ago        293MB
k8s.gcr.io/coredns/coredns                v1.8.6      a4ca41631cc7   4 months ago        46.8MB
k8s.gcr.io/pause                          3.6         6270bb605e12   6 months ago        683kB
kubernetesui/dashboard                    v2.3.1      e1482a24335a   8 months ago        220MB
kubernetesui/metrics-scraper              v1.0.7      7801cfc6d5c0   8 months ago        34.4MB
gcr.io/k8s-minikube/storage-provisioner   v5          6e38f40d628d   11 months ago       31.5MB

4. ログアウトするともとの context にもどる

いままでとこれから

いままで)

$ cat cron.sh
#!/usr/bin/bash

set -m
shamshir_pat=<HERE_IS_TOKEN> node shamshir-stand-alone.js --owner kyagi --repo awesome-project --label "releasable" --quorum 2

$ crontab -l
*/15 10-19 * * 1-5 cd /home/ec2-user/git/shamshir/src;  bash -l -c /home/ec2-user/git/shamshir/src/cron.sh

これから)

$ kubectl config current-context
minikube

$ kubectl get cronjobs
NAME       SCHEDULE           SUSPEND   ACTIVE   LAST SCHEDULE   AGE
shamshir   */15 10-19 * * *   False     0        16s             9m49s

$ cat cronjob5.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: shamshir
spec:
  schedule: "*/15 10-19 * * *"
  jobTemplate:
    spec:
      backoffLimit: 5
      ttlSecondsAfterFinished: 100
      template:
        spec:
          containers:
          - name: shamshir
            image: shamshir:v1.0
            env:
              - name: TZ
                value: Asia/Tokyo
              - name: shamshir_pat
                valueFrom:
                  secretKeyRef:
                    name: shamshir
                    key: pat
            imagePullPolicy: Never
          restartPolicy: OnFailure
      parallelism: 1
      completions: 1
  concurrencyPolicy: "Forbid"
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5

$ cat secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: shamshir
type: Opaque
stringData:
    pat: HERE_IS_TOKEN

$ cat Dockerfile
FROM node:16-alpine

# Create app directory
WORKDIR /app

COPY package*.json ./

RUN npm install

# Bundle app source
COPY . .

WORKDIR /app/src

CMD [ "node", "shamshir-stand-alone.js", "--owner", "kyagi", "--repo", "awesome-project", "--label", "releasable", "--quorum", "2" ]

$ kubectl logs shamshir-27439718-hl7jf
{"level":"info","message":"Shamshir started.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:22"}
{"level":"info","message":"Shamshir got pulls: 2602,2598,2596,2575,2573,2557,2553,2551,2540,2539,2481,2478,2295,2281,1981,1951,1685","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:22"}
{"level":"info","message":"Shamshir added releasable label to pull/2598.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:24"}
{"level":"info","message":"Shamshir added releasable label to pull/2596.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:24"}
{"level":"info","message":"Shamshir added releasable label to pull/2575.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:25"}
{"level":"info","message":"Shamshir removed releasable label from pull/2573.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:26"}
{"level":"info","message":"Shamshir added releasable label to pull/2551.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:28"}
{"level":"info","message":"Shamshir added releasable label to pull/2540.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:29"}
{"level":"info","message":"Shamshir added releasable label to pull/2539.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:30"}
{"level":"info","message":"Shamshir added releasable label to pull/2478.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:31"}
{"level":"info","message":"Shamshir added releasable label to pull/2295.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:32"}
{"level":"info","message":"Shamshir added releasable label to pull/1951.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:34"}
{"level":"info","message":"Shamshir finished.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:34"}

参考情報

minikube.sigs.k8s.io

Chrome で検索対象の言語を英語のみにして検索する

設定方法

  1. URL バーを右クリックして「検索エンジンを管理」のメニューを表示します。 f:id:kyagi:20220308054556p:plain

  2. メニューを以下のように修正します。検索エンジン名とキーワード(トリガーとなるキー)をお好みで指定します。

https://www.google.com/search?gl=us&hl=en&gws_rd=cr&pws=0&q=%s

f:id:kyagi:20220308054649p:plain

使用方法

URL バーで「e」+「スペース」をタイプすると検索エンジンの設定が上記で設定した「Google(e)」に切り替わり英語のみ検索対象になります。

f:id:kyagi:20220308054612p:plain

上記の設定で検索した場合(左)と普通に検索した場合(右) の比較

f:id:kyagi:20220308054630p:plain

aws-minikube: ELB とも連携できてフットワークが軽い AWS k8s 環境

k8s 環境として minikube, eks, aws-minikube を比較してみました(k3s は未経験)。個人的には aws-minikube がコスト面/機能面でよさそうに思えたので紹介です。

aws-minikube とは?

EC2 1 台で k8s 環境が整うツールです。Red Hat 社の Jakub Scholz 氏が 2017 年から開発しています。最新の k8s への対応も早いです。 terraform ベースの aws-minikube と kubeadm ベースの aws-kubernetes の 2 段揃えの構成です。

github.com

(日本ではあまり知られていないようです... 私は minikube で ELB も使えるような拡張がないかな... とぐぐっていた時にたまたま見つけました)

使用する AWS リソース

terrafrom apply すると my-minikube という EC2 と f:id:kyagi:20220221201110p:plain

my-minikube.<your.domain.here> という Route53 レコードが追加されます。 f:id:kyagi:20220221201142p:plain

Security Group も作成されます。以下では自宅 IP に変更していますがデフォルトは 0.0.0.0/0。6443 は k8s の API をたたくポートで専用の .kube/config (認証情報を含む設定ファイル) が構築時に発行されます。 f:id:kyagi:20220221202550p:plain

何が嬉しいのか

  • 1 台で k8s 環境がそろう
  • 使う ec2 は t2.medium でコストが安い。1 ヶ月 4377 円(= 0.0608 USD * 100 * 24 * 30)。これはデフォルトで t3.medium 2 台を使って EKS 代も徴収する EKS の 1/3。環境構築のスピードも早いのでそのたびに作って壊してもストレスがない。
  • 環境構築のスピードがはやい。EKS だと cluster 作成に 20 分くらいかかるけどこちらは 5 分ほど。
  • minikube の制約だったサービスの外部公開が簡単に可能。service を type:LoadBalancer で create/delete すると応じて ELB が作られる/削除される
  • いじる aws リソースが最小限(EC2, Route53, SecurityGroup)
  • terraform apply/destroy でまるごと消せる

想定する使い方

開発者ごとに t2.medium の EC2 1 台を支給し、手軽に k8s を試してもらって組織の k8s 力をあげる、といった使い方がよさそうです。

minkube, aws-minikube, eks の比較

候補 お手軽さ 時間 できること コスト 後始末
minikube
aws-minikube
eks

コスト内訳

候補 コスト構成 コスト/日 コスト/月
minikube ローカルの Mac で動かせば無料、EC2 の場合は EC2 料金(*1) 146円 4,377 円
aws-minikube EC2 料金(*1) 146円 4,377円
EKS EC2 料金(*2) + EKS 料金 440円 13,200円
  • *1 t2.medium で計算(vCPU が 2 つある EC2)
  • *2 t3.medium * 2 で計算(EKS がデフォルトで選択する)

使い方

*ほぼ README 通りですが AWS MarketPlace の centos AMI を subscribe する必要があります。無料です)

1. centos の AMI を subscribe する(これをしないと terraform apply 時に止まります)

https://aws.amazon.com/marketplace/pp/prodview-qkzypm3vjr45g

f:id:kyagi:20220221201412p:plain

2. 設定ファイルを書き換える

$ git diff
diff --git a/example.tfvars b/example.tfvars
index 177a1fe..a16914c 100644
--- a/example.tfvars
+++ b/example.tfvars
@@ -1,5 +1,5 @@
 # AWS region where should the Minikube be deployed
-aws_region = "eu-central-1"
+aws_region = "ap-northeast-1"

 # Name for role, policy and cloud formation stack (without DBG-DEV- prefix)
 cluster_name = "my-minikube"
@@ -8,13 +8,13 @@ cluster_name = "my-minikube"
 aws_instance_type = "t2.medium"

 # SSH key for the machine
-ssh_public_key = "~/.ssh/id_rsa.pub"
+ssh_public_key = "~/.ssh/id_rsa.ubuntu.com.amazonaws.info.myservice.dev.all.pub"

 # Subnet ID where the minikube should run
-aws_subnet_id = "subnet-8a3517f8"
+aws_subnet_id = "subnet-0a102dc87cd20932e"

 # DNS zone where the domain is placed
-hosted_zone = "my-domain.com"
+hosted_zone = "my.service.com"
 hosted_zone_private = false

 # AMI image to use (if empty or not defined, latest CentOS 7 will be used)

3. terraform apply する

f:id:kyagi:20220221201437p:plain

(... snip ...)

f:id:kyagi:20220221201452p:plain

4. EC2 がたちあがるので SSH ログインする

5. config ファイル作成して完了

[centos@ip-10-0-0-87 ~]$ mkdir .kube
[centos@ip-10-0-0-87 ~]$ cp kubeconfig ~/.kube/config
[centos@ip-10-0-0-87 ~]$ kubectl get svc --all-namespaces
NAMESPACE     NAME                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                  AGE
default       kubernetes                  ClusterIP   10.96.0.1        <none>        443/TCP                  11h
kube-system   dashboard-metrics-scraper   ClusterIP   10.103.238.218   <none>        8000/TCP                 11h
kube-system   kube-dns                    ClusterIP   10.96.0.10       <none>        53/UDP,53/TCP,9153/TCP   11h
kube-system   kubernetes-dashboard        ClusterIP   10.96.254.133    <none>        443/TCP                  11h
kube-system   metrics-server              ClusterIP   10.103.193.240   <none>        443/TCP                  11h

6. サービスを作って ELB で外部公開

[centos@ip-10-0-0-87 ~]$ kubectl create deployment kubia --image=kyagi/kubia --replicas=3 --port=8080
deployment.apps/kubia created

[centos@ip-10-0-0-87 ~]$ kubectl expose deployment kubia --port=8080 --target-port=8080 --type=LoadBalancer --name=kubia
service/kubia exposed

[centos@ip-10-0-0-87 ~]$ kubectl get svc
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP                                                                    PORT(S)          AGE
kubernetes   ClusterIP      10.96.0.1       <none>                                                                         443/TCP          11h
kubia        LoadBalancer   10.97.188.164   a81e8fb4f947f444f9bae599cf38ca3a-2070947448.ap-northeast-1.elb.amazonaws.com   8080:31678/TCP   10s

[centos@ip-10-0-0-87 ~]$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
kubia-7f585c778c-k4t5h   1/1     Running   0          10m
kubia-7f585c778c-nkzbr   1/1     Running   0          10m
kubia-7f585c778c-p77hr   1/1     Running   0          10m

(外部公開を確認) f:id:kyagi:20220221201516p:plain

(ELB が作られている) f:id:kyagi:20220221201537p:plain

Getting started with Amazon EKS (2) とコスト計算

前回 に引き続き、EKS でサンプルの nginx pod を外部公開するまでを実施しました。 docs.aws.amazon.com

Service の外部公開

Getting Started with Amazon EKS で紹介されているデプロイだと Type:ClusterIP でクラスタ内部への公開なので、これを type: LoadBalancer にして外部へ公開する YAML を作成しました。チュートリアルで紹介されている YAML との差分は以下になります。

$ diff -Nurp eks-sample-service.yaml eks-sample-service-loadbalancer.yaml
--- eks-sample-service.yaml 2022-02-20 20:16:54.000000000 +0900
+++ eks-sample-service-loadbalancer.yaml    2022-02-20 22:54:35.000000000 +0900
@@ -1,11 +1,12 @@
 apiVersion: v1
 kind: Service
 metadata:
-  name: eks-sample-linux-service
+  name: eks-sample-linux-service-loadbalancer
   namespace: eks-sample-app
   labels:
     app: eks-sample-linux-app
 spec:
+  type: LoadBalancer
   selector:
     app: eks-sample-linux-app
   ports:

Amazon EKS クラスターで実行されている Kubernetes Services を公開する

こちらを適用すると LoadBalancer として AWS 内部で ELB が作成されて外部からアクセスすることが可能になります。

f:id:kyagi:20220220230353p:plain f:id:kyagi:20220220230557p:plain

ダッシュボード設定

ダッシュボードの設定は以下 2 つを実施することになります。

docs.aws.amazon.com

docs.aws.amazon.com

f:id:kyagi:20220220225811p:plain

eks 固有の pod

kube-system namespace に aws-node という pod が 2 つあがっています。これは eks 固有の pod のようです。

f:id:kyagi:20220220234720p:plain

コスト計算

N.Virginia で試算すると

EC2(t3.medium * 2): 199.68 円= 0.0416USD * 100 * 24 * 2 EKS: 240 円= 0.1USD * 100 * 24

で合計 440円/日になります。月換算だと 1.3万円。これは EC2(t3.nano) を 1 ヶ月立てっぱなしにした 374円/月より高いです(374.4 = 0.0052 * 100 * 24 * 30)。さらにここに ELB が載ってきます。EKS での個人利用はちょっときついですね。

Ansible における抽象/具体モデルと誰が/何をするの考え方

Ansible の group と host, role と task の関係が 公式ドキュメント からだと個人的にわかりにくかったのでまとめた図です。頭の中でなんとなく理解していたことを図や表に起こしてみることで理解が促進されました(公式ドキュメントにこういった説明があれば助かりましたが... たとえば以下の定義だけだと他のエンティティとどう結びついているのがわかりづらい)

抽象/具体の 2 軸に分類して考える

Ansible の主要なエンティティは group, host, role, task, inventory です。それぞれをまず抽象と具体の 2 軸で分類すると以下になります。

  • 抽象的なエンティティ: group, role
  • 具体的なエンティティ: host, task

抽象的なエンティティである group, role は実際に存在するわけではなく「集合」としての概念です。例えば redisservers という group は redisservers という具体的な host を指しているわけではありません。redis01, redis02, redis03 という実在する具体的な host を集合として抽象化したのが group です。

同様に roles も実際に存在するわけではなく「集合」としての概念です。例えば commonPackges という role は commopnPackages という 具体的な task を指しているわけではありません。install git, install gcc という実在する具体的な task を集合として抽象化したのが role です。

「誰が」「何をする」の 2 軸を追加して考える

もう一歩考えを進めて、抽象/具体モデルにもうひとうの軸「誰が」「何をする」を追加してみます。こうすると、それぞれ抽象的なレベルと具体的なレベルそれぞれで「誰が」「何をする」がわかりやすくなります。そしてこの抽象と具体を結びつけているのが inventory です。

誰が 何をする
抽象 group role
具体 host task
抽象/具体の関連付けファイル inventory [groupname].yml
  • group の例) redisservers, webservers
  • host の例) redis01, redis02, redis03, web01, web02, web03
  • role の例) commonPackges, tuningKernelParams
  • task の例) install git, install gcc, modify sysctl.conf, load sysctl
  • inventory の例) development, staging, production
  • [groupname].yml の例) redisservers.yml, webservers.yml

f:id:kyagi:20220218222605p:plain

抽象化することで「個」ではなく「集合」を扱える

抽象レベルで記述することの最大の利点は結果的に「個(host, task)」ではなく「集合(group, role)」を扱うことにより記述量が少なくなることです。同様の考え方はデータベースでもオブジェクト志向プログラミングでもクラウドのリソース管理/設計でも見つけることができるはずです。

f:id:kyagi:20220218222620p:plain

抽象レベルで記述したことを具体レベルまで落とし込んでいくと以下のようになります。

  • 誰が

    • host: redis01(具体) は group: redisservers(抽象) に属している
  • 何をする

    • task: yum install redis(具体) は role: redis(抽象) に属している
    • task: yum install git(具体) は role: commonPackages(抽象) に属している

よって「redis01」は「yum install redis」「yum install git」を行える「(... 省略 ...)」を行う(「誰が」「何をする」)

参考情報

docs.ansible.com

Node.js のテストフレームワークとデバッガ

テストフレームワーク Jest

shamshir に細かなバグを見つけたので少しの間、我流のプリントデバッグをしていたのですが、継続的なメンテナンスを考えてテストフレームワークを導入することにしました。参考にしている「ハンズオン Node.js」の「8 章 ユニットテストとデバッグ」には 2 種類のツールが紹介されています。ひとつは Mocha + Chai + Sinon.JS + Istanbul の組み合わせで、もう一つが Jest です。フレームワーク、アサーション、テストダブル、カバレッジ全てカバーしているフルスタックであることと、後発ゆえに設計の洗練されてきているのでは期待を込めて Jest を使用することにしました。

jestjs.io

ドキュメントを少し読んだだけですが、導入もしやすいし使い勝手もよさそうです。テストだけでなくカバレッジもこなせるのがいいですね。

f:id:kyagi:20220215230455p:plain

f:id:kyagi:20220215230551p:plain

今となってはスタンドアローンと Github Actions 両方で起動できるようにしておいてよかったと思います。スタンドアローンで環境変数で認証できるようにしておいたおかげでテストフレームワークの Setup にそのまま使えます(Jest での実装は beforeAll)。Github Actions だけにしていたら認証の部分を別途書かなくてはいけないところでした。

Setup and Teardown · Jest

デバッガ inspect

inspect を追加するだけで GDB ライクにデバッグが可能になります。ブレイクポイントを N 行目に設定するには sb(N) です。変数の値を見るには p ではなく debugger 内で repl を起動する必要があります。node --inspect-brk を使う場合はブラウザの deveoper tool でデバッグができます。

nodejs.org

f:id:kyagi:20220215231438p:plain