Josh Kaufman氏によると、正しく学習を行えば20時間で新たなスキルを身につけることができるそうです。
今回は氏の学習方法を参考に、「Kubernetes・Docker」について20時間学習を行ってみたので、その結果をまとめたいと思います。なおこの記事は学習ノートとしての側面の方が強いので注意してください。
はじめに
学習前の知識や技術
Dockerfileはほとんど書いたことがない。どのタイミングで利用するべきなのか理解していない。可能な限りdocker-composeのymlファイルだけで良いのでは?と考えている。
docker-compose.ymlには共通した内容を書いて、-f
オプションで環境ごとにymlファイルを使い分けると良いらしい。
dockerignoreファイルには何を指定するのが良いのかを理解していない。
Kubernetesはコンテナ運用をサポートしてくれるため、大規模な環境では使うことが多くなってきているらしい。開発環境でも利用することで、本番環境との差異を減らすことができる。
クラスターやPodという用語の存在は知っているが詳細な意味までは分からない。
オーバースペックすぎて大半のケースでは必要なさそうなイメージがある。
kindで環境のみ構築したが、実際に動かしたことは一度もない。
学習目標
Dockerに関してはDockerfileとdocker-composeファイルに留めて、Kubernetesに集中しました。
とはいえKubernetesはかなり学習コストが高そうに見えたため目標は控えめです。
- Dockerfileの適切な利用法を理解する
- docker-composeファイルの適切な利用法を理解する
- Kubernetesの簡単な概要を理解する
- DockerとKubernetesの比較ができるようにする
- Kubernetesで環境を構築し動作させてみる
- Developer Roadmapを参考に知識を深めていく
Dockerfileについて
DockerfileはDockerイメージを作成する手順が記述されたテキストファイルです。
docker build
コマンドを実行するとファイルやディレクトリのコピーや依存関係の追加など記述されている命令を実行し、Dockerイメージをビルドできます。
Docker Composeを利用している場合、公式イメージで十分の場合はわざわざDockerfileを用意する必要はないのではと考えていたため、逆にどのような場面で用意するべきなのかを調べてみました。
結果として、公式イメージでは不十分、例えばwgetやcurl, vimなど追加コマンドをインストールしたい場合や、Node.jsでnpm install
コマンドを実行してアプリケーションの依存関係をインストールしたい場合に用意する。という結論に至りました。
実際に、依存関係のインストールとビルドイメージサイズの削減を目的としたDockerfileを作成してみました。
なおDockerfileの内容はNext.jsのGithubリポジトリを参考にしました。
FROM node:current-alpine AS base
WORKDIR /app
FROM base AS dependencies
COPY package*.json ./
RUN npm install --omit=dev
FROM base AS builder
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS production
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
USER node
create-next-app
で環境構築したアプリケーションを例にマルチステージビルドを行っています。
このDockerfileでビルドしたイメージは後々Docker Composeで利用しています。
dependenciesステージでは依存関係のインストールを行い、builderステージではビルドプロセスを実行しています。そしてproductionステージではアプリケーションに必要なコードだけをコピーしています。
docker images
コマンドでDockerイメージのサイズを確認してみると、node_modules
をコピーしていないproductionステージのイメージサイズがかなり小さいことがわかります。
$ docker images next*
REPOSITORY TAG IMAGE ID CREATED SIZE
next-production latest 3469bb3b0015 12 seconds ago 190MB
next-builder latest acfe359c32a5 13 seconds ago 573MB
next-dependencies latest 662eb4a07791 35 seconds ago 648MB
docker-composeファイルについて
docker-composeファイルは複数のコンテナを定義し、それらを連携させてアプリケーションを実行するためのYAML形式の設定ファイルです。docker-composeファイルでは、コンテナ間のネットワーク設定やマウント設定などを定義することができます。
docker-composeファイルは環境ごとに分けるという考え方のほか、override.ymlだけ作成して.gitignoreに追加するという考え方もあるようです。
今回は分けたい環境の数が少ない(ない)ため、overrideする方法を試してみました。
version: '3.4'
services:
web:
image: next-production:latest
ports:
- 3000:3000
command: [sh, -c, node server.js]
上記は先の章でビルドしたイメージを利用しているdocker-composeファイルです。
そして開発環境用に以下のようなdocker-compose.override.yml
ファイルを作成してみました。
version: '3.4'
services:
web:
image: node:current-alpine
working_dir: /usr/src/app
volumes:
- ./services/web:/usr/src/app
ports:
- 3000:3000
command: [sh, -c, npm run dev]
tty: true
docker-compose up
コマンドを実行するとベースとdocker-compose.override.yml
の定義をもとに環境が構築されます。
overrideファイルだけの方法を試してみましたが、やはり環境ごとにファイルを分けた方が便利なような気がしました。
KubernetesでNginx × Node環境を構築する
ここではKubernetesを使ってNginxとNode環境を構築してみようと思います。Nginx経由でNode.jsアプリケーションにアクセスするという簡単な構成です。
なおKindを使ってKubernetes環境を構築したため、クラスター作成部分など一部ほかとは異なっている可能性があります。
クラスターの作成
まずはクラスターの作成を行っていきます。
クラスターを作成する前に、Node.jsアプリケーションでExpressのコードを動かすためのマウントの設定を記述した設定ファイルを用意します。(Dockerfileの章でビルドしたイメージを使えば設定ファイルを用意する必要はなかったのにと後悔しています...)
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
# extraPortMappings:
# - containerPort: 30080
# hostPort: 8080
- role: worker
extraMounts:
- hostPath: <絶対パス>/services
containerPath: /services
readOnly: true
設定ファイルの内容としては、コントロールプレーンとワーカーノードがあり、そのワーカーノード(Dockerコンテナで実現されています)にホスト側のソースコードをマウントするようにしています。コメントアウトされたnodes.extraPortMappings
という設定がありますが、これはコントロールプレーン(というよりDockerコンテナ)とホスト側のポートマッピング設定です。しかしわたしの環境では設定しなくても動作したため、一旦コメントアウトにしました。
この設定ファイルをもとにkind
コマンドでクラスターを作成します。
$ kind create cluster --config kind-config.yaml
$ kind get clusters
kind
$ kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* kind-kind kind-kind kind-kind
$ kubectl get node # ノード一覧の表示
kindで作成したクラスターはkind-
というプレフィックスが付くようです。kind側では○○
という名前でクラスター名を指定しますが、Kubernetes側ではkind-○○
という名前を使うことになります。
また、--name
オプションを使うことでクラスター名を自由に変えることができます。このオプションを利用したとしても、kind-
というプレフィックスが付くため注意してください。デフォルトの名前はkind
です。
NginxのDeployment・Service・ConfigMap作成
クラスター(とノード)の用意ができたため、続いてNginx環境を構築します。
まずはConfigMapの作成を行います。
ConfigMapは機密性の低いデータを保存するためのオブジェクトです。ConfigMapを使用することで環境固有の設定をコンテナイメージから分離できるため、アプリケーションを簡単に移植できるようになります。
また、機密性の高い情報を扱うためのSecretというオブジェクトも用意されています。
今回はNginxの設定ファイルからConfigMapを作成します。
ConfigMapはマニフェストファイルから作成する方法と作成時にファイルそのものを渡す方法とがありますが、設定ファイルは少し複雑なため、作成時にファイルそのものを渡す方法を取ることにしました。
設定ファイルの中身としては一般的な内容ですが、わたしの環境下では名前解決部分が特殊だったため、その部分を抜粋します。
# 省略
location /nodejs/ {
resolver kube-dns.kube-system.svc.cluster.local;
set $upstream nodejs-service.default.svc.cluster.local:3000/;
proxy_pass http://$upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# 省略
$ kubectl create configmap nginx-config --from-file=nginx.conf
$ kubectl get configmap nginx-config -o yaml # マニフェストファイル形式でConfigMapの内容を表示
続いてDeploymentとServiceの作成を行います。オブジェクトごとにマニフェストファイルを用意するのは手間がかかるため、---
で区切ることで1つのファイルにまとめました。
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 2 # Pod数
selector:
matchLabels:
app: nginx-deployment
template:
metadata:
labels:
app: nginx-deployment
spec:
containers:
- name: nginx
image: nginx:stable-alpine
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/nginx.conf
name: config-volume
subPath: nginx.conf # 単一のファイルをマウントする場合は必須
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
volumes:
- name: config-volume
configMap:
name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx-deployment
ports:
- port: 80
targetPort: 80
nodePort: 30080
type: NodePort # 外部からアクセスできるようにする
ファイルの用意ができたため、kubectl apply
コマンドでリソースの作成を行います。
$ kubectl apply -f nginx-manifest.yaml
$ kubectl get deployment
$ kubectl get pod
$ kubectl get service
kubectl get pod
コマンドで表示されたSTATUSがRunningとなっていれば正常に動作しています。初回のリソース作成時はDockerイメージをプルするのに時間がかかる可能性があります。
また、上手く動作していない場合は以下のコマンドで詳細情報を確認することができます。
$ kubectl descirbe pod
$ kubectl logs <リソース名>
ダッシュボードをデプロイすることでKubernetesリソースの情報を視覚的に確認することもできます。詳細はこちらを参照してください。
リソースの作成後にConfigMapでマウントした内容を変更したい場合は、ConfigMapの再作成後にPodの再起動を行うことで変更を反映させることができます。
# ConfigMapを再作成する
$ kubectl rollout restart deployment <デプロイメント名> # 今回の場合はnginx-deployment
NodeのDeployment・Service作成
続いてNode環境の構築も行います。アプリケーションのソースコードに関しては省略します。
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nodejs-deployment
spec:
selector:
matchLabels:
app: nodejs-deployment
template:
metadata:
labels:
app: nodejs-deployment
spec:
containers:
- name: nodejs
image: node:current-alpine
command: ["npm", "run", "start"]
workingDir: /usr/src/app
ports:
- containerPort: 3000
volumeMounts:
- name: source-code
mountPath: /usr/src/app
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
volumes:
- name: source-code
hostPath:
path: /services/node-sample
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: nodejs-service
spec:
selector:
app: nodejs-deployment
ports:
- port: 3000
targetPort: 3000
type: ClusterIP # クラスター内部にのみ公開
Node.jsアプリケーションはNginx経由でアクセスできれば十分なため、Serviceオブジェクトのspec.type
フィールドにはClusterIPを指定しました。
その部分以外ではNginxのマニフェストファイルと大きな違いはありません。
$ kubectl apply -f node-manifest.yaml
# Nginxと同様にkubectl get 〇〇 コマンドでリソースの状態を確認することができる
動作確認をする
ここまでの章で構築したNginx × Node環境にアクセスしてみたいと思います。
今回Kubernetesの環境構築に利用したkindはクラスターをDockerコンテナとして作成するため、まずコンテナに割り振られているIPアドレスを取得します。
$ kubectl get node
$ kubectl describe node <node名> | grep InternalIP
InternalIP: <コンテナに割り振られているIPアドレス>
IPアドレスを取得できたため、IPアドレス:30080
とIPアドレス:30080/nodejs/
にアクセスしてみたいと思います。
$ curl <IPアドレス>:30080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
...
$ curl <IPアドレス>:30080/nodejs/
{"message":"Hello World"}
無事に動作していることを確認することができました。
まとめ
今回は「Kubernetes・Docker」についての学習を20時間行いその結果をまとめてみました。
20時間フルにKubernetesの学習に使った訳ではありませんでしたが、それでも「触れる」というレベルになることができたと思います。
反省点として、学習コストが高すぎたため途中でより良い方法を知ったとしても試す時間がなかったことです。
例えばKustomizeというマニフェストファイル管理ツールがあるそうですが、時間が足りなかったため試すことができませんでした。
またnodejs-deploymentにMySQLコンテナを追加してSecretオブジェクトと永続化を試してみたかったのですが時間が足りませんでした。
とはいえこれは仕方のないことなのかなと。今後も学習を続けて上記の内容に挑戦してみたいと思います。
次回はデータベース選定・データベース設計やデータ構造とアルゴリズムなど苦手な分野に挑戦してみたいなと考えています。