ブログのバックエンドをKubernetesにしてみた

 去年のいつだったか、会社のエンジニアではない人に「なんでうちのエンジニアたちはKubernetesを使いたがっているの?」と質問された。エンジニアではない人もPaaSなどがわかるように教育されていたおかげでわりと簡単に答えられた。「PaaSはサーバ管理を任せられるし、スケーリングやローリングアップデートなんかもワンクリックで済むぐらいの簡単さがある。だけどそれは各クラウドベンダがそれぞれのやり方で実装しているものだから、ベンダロックインを受けてしまう。k8sは前に挙げた便利機能を持っている。そしてどこで動かしたっていいからベンダロックインを回避できる」と答えた。

 去年の年末、このブログを動かしていたコンテナホスティングサービスからサービス終了の告知が来た。コストなどを少し考えた上でAzureのマネージドのk8s、つまりAKSを使うことにした。
 ぼくの説明は本当に正しかったのか試す機会を得た。この、Dockerイメージ化されてコンテナホスティングサービスで動いていたブログをAKSに移行するのに、どの程度Azure独自のことをしなければならず、どの程度k8sの設定で済むのか。そんなところをやってみた。Dockerイメージ化されたブログを、Let's Encryptで証明書を取得、自動更新しつつHTTPSで配信するという構成。


 k8sに配置するリソースは下記のものを用意した。YAMLの詳細は後に回す。
- Deployment: アプリのPodをコントロール
- Ingress: http & httpsアクセス
- Service: Ingress - Pod間のネットワーキング
- Certificate: 証明書情報(Let's Encryptで取得)
- Secret: 外部データベース、クラウドストレージなどの接続情報
これらはもちろんk8sの標準的なリソースなのでAzureだから特別な書き方をしなければというのはない。標準的なYAMLだ。

 AKSでk8sを立ち上げてCLIで接続後、唯一Azure上の独自のやり方でやらなければならなかったのはIPの発行。まあここはさすがに仕方がないことだし、コマンドですぐに発行できるし。
az network public-ip create --resource-group [resource name] --name [ip name] --allocation-method static


IPを取得したらもちろん、DNS設定は忘れずに行う。

 デフォルト構成にHelmを加え、CertManagerを入れたが、これはk8s側に用意されたもので、WindowsでもCLIが動くので簡単にAKSのk8sへ入れることができた。
Azure Kubernetes Service (AKS) での Helm を使用したアプリケーションのインストール
helm install stable/nginx-ingress --namespace kube-system --set controller.service.loadBalancerIP="[ip]" --set controller.replicaCount=2 --set rbac.create=false --set rbac.createRole=false --set rbac.createClusterRole=false

helm install stable/cert-manager --namespace kube-system --set ingressShim.defaultIssuerName=letsencrypt-prod --set ingressShim.defaultIssuerKind=ClusterIssuer --set rbac.create=false --set serviceAccount.create=false


 IP取得&それのDNS設定、Helmが準備できれば、あとはk8s上にリソースを配置していくだけだ、YAMLを読み込ませることによって。あとドメインとIPが結びついてなけりゃ証明書取得はできるはずもないので忘れないこと。
apiVersion: apps/v1

kind: Deployment
metadata:
name: app-deployment
namespace: tetsujin
labels:
app: tetsujin
spec:
replicas: 1
selector:
matchLabels:
app: tetsujin
template:
metadata:
labels:
app: tetsujin
spec:
containers:
- name: tetsujin
image: matoba/tetsujin:414
ports:
- containerPort: 80
env:
- name: HASHKEY
valueFrom:
secretKeyRef:
name: tetsujin-secret
key: HASHKEY
- name: MONGO_CONNECTION
valueFrom:
secretKeyRef:
name: tetsujin-secret
key: MONGO_CONNECTION
- name: STORAGE_ACCOUNT
valueFrom:
secretKeyRef:
name: tetsujin-secret
key: STORAGE_ACCOUNT
- name: STORAGE_KEY
valueFrom:
secretKeyRef:
name: tetsujin-secret
key: STORAGE_KEY
- name: STORAGE_URL
valueFrom:
secretKeyRef:
name: tetsujin-secret
key: STORAGE_URL

↑のYAMLはCPUやメモリのリソース制限をかけていないのでよろしくない。自分の環境に合わせて適当に適切な設定をすること

apiVersion: v1

kind: Service
metadata:
name: tetsujin-service
namespace: tetsujin
labels:
network: tetsujin
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: tetsujin


apiVersion: extensions/v1beta1

kind: Ingress
metadata:
name: ingress-tetsujin
namespace: tetsujin
annotations:
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/rewrite-target: /
ingress.appscode.com/hsts: "true"
ingress.appscode.com/hsts-preload: "true"
ingress.appscode.com/hsts-include-subdomains: "true"
ingress.appscode.com/hsts-max-age: "100000"
spec:
tls:
- hosts:
- blog.hmatoba.net
secretName: tls-secret
rules:
- host: blog.hmatoba.net
http:
paths:
- path: /
backend:
serviceName: tetsujin-service
servicePort: 80

↑もHSTS有効化はしたけどもう少しセキュアなものにしたい

apiVersion: certmanager.k8s.io/v1alpha1

kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: foo@example.com
privateKeySecretRef:
name: letsencrypt-prod
http01: {}


apiVersion: certmanager.k8s.io/v1alpha1

kind: Certificate
metadata:
name: tls-secret
spec:
secretName: tls-secret
dnsNames:
- blog.hmatoba.net
acme:
config:
- http01:
ingressClass: nginx
domains:
- blog.hmatoba.net
- http01:
ingress: ingress-tetsujin
domains:
- blog.hmatoba.net
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer




 上記のYAMLに加え、Secretを用意してしかるべき順でリソースをk8s上に加えていった結果このブログが動作した。Let's Encryptで取得した証明書でhttps化も問題ない。結局Azureのマネージドk8sサービスであるAKSを使ってWebアプリを公開するにあたってAzure独自でやらなければならないのは、Azureのコンパネに従ってk8sを用意することと、IPの発行だけであった。それらは簡単だったし一度で終わることなので、それ以外のことやこれからの運用はk8sのお作法に従うだけだろう。こりゃロックインは回避できてるなというところ。


 余談。AKSの料金は開始時に設定した仮想マシンリソースの設定に従う。仮想マシンリソース。CPUが何コアのメモリが何コアという、仮想マシンにありがちなアレ。アレはリザーブドインスタンスというものがあり、長期で先に金を払ってリソースを買っておくことで使用料が安くなる。一年分を先買いで40%OFF。断然お得。
comment: 0

Azure PipelinesでDocker Composeを使ってCIしてみた

 CIサービスを公私で三つ使っている。今回は私のほうでTravisCIのYAMLを書き換える必要が出てきたので、それを機にちとAzure Pipelinesへの乗り換え検討のための試行。

 まずPipelinesをGithub連携で使う準備、はリンクを置いて割愛する。
https://qiita.com/YuichiNukiyama/items/531fe3e83a7324faff23



 とりあえず動かしてみる。下記の二ファイル構成のプロジェクト。
azure-pipelines.docker.yml

pool:
vmImage: 'ubuntu-16.04'

steps:
- script: |
docker -v
docker version -f '{{.Server.Experimental}}'
docker-compose -v
docker login -u $(dockerId) -p $(dockerPw)
displayName: 'prepare'
- script: |
docker pull nginx:alpine
docker tag nginx:alpine matoba/nginx
displayName: 'build'
- script: |
docker-compose build
docker-compose up -d
displayName: 'run'
- script: |
curl --insecure https://127.0.0.1/
displayName: 'test'
- script: |
docker push matoba/nginx
displayName: 'push'


docker-compose.yml

version: '3.7'

services:
webapp:
image: matoba/nginx
proxy:
image: matoba/ssldevenv:cc
ports:
- "443:443"
environment:
- LINK_TO=webapp


CI中にDockerアカウントにログインしたいので、環境変数でそれを渡せるように設定しておく。


 Github連携をしておいてプッシュすると、CIが流れていく。


そしてDocker Hubへのプッシュが確認できた。




 とりあえず難なく動くのは確認できた。ここから検討をする。


 前提としてCI中にはDockerイメージのビルド、それとDocker Composeを使ってのアプリの起動とテスト、Dockerレジストリへのプッシュを行う。できれば一つのYAMLをいじるだけでシンプルに終えたい。あとOSSで、プルリクを受け付けている。
 プルリクを受け付けている状態でのCIの動作として、テストが済んだら自動デプロイというフローはまずい。プルリクなんてパブリックなリポジトリなら勝手に投げられるので、その結果物が自動デプロイされるようになっているのはまずい。今TravisCIでは環境変数を使ってプルリクに分岐をかけたり、リリースを行ってタグを判別してリリースを行ったりしている。まあこのあたりそこまで難しくなくて、TravisCIは便利。
 Azure Pipelinesではこれが劇的に便利に!となれば移行した。↑でTravisCIでやっていることは、Azure Pipelinesでは"script"の実行条件をいじらればということになる。
https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#script
 期待するオプションはなかったので、劇的に簡単になったりはしなさそう。ブランチとかプルリクのtrue or falseでのフィルタリングができればと思ったがなかった。でも"script"の粒度でそんな細かいことがやれてしまうと、ステージやジョブが分けられる意味が薄れてくるんだろうな。とりあえず次に新規でなにか作るときにまた使ってみるか。
comment: 0

GItLabのコンテナレジストリからタグのリストを取得する

 Dockerのイメージレジストリにあるイメージのタグをリストで取得したい。Dockerレジストリの公式に以下のAPIがある。
https://docs.docker.com/registry/spec/api/#listing-image-tags

 GitLabもこれ踏襲してるだろーと"/v2/***"にアクセスしたら404。調べてみたら別のパスで用意しているらしい。
https://gitlab.com/gitlab-org/gitlab-ce/issues/40826


 まず↓の形のパスにアクセスして"id"を控える。
https://gitlab.com/$PROJECT_PATH/container_registry.json


 あとはその"id"を↓に入れてアクセス。
https://gitlab.com/$PROJECT_PATH/registry/repository/$registry_id/tags?format=json
comment: 0

Docker,Alpine,Let's Encrypt,NginxでHTTPS化を行うリバースプロキシコンテナ

 Let's Encrypt、Docker、NginXを組み合わせて、自動で証明書更新をしてくれるリバースプロキシコンテナを用意している記事を見つけた。だがhttps-portalっていう、同じことをやるDockerイメージもすでにあったりする。
 https-portalとそのブログ記事のあいだに、ぼくも同様のものを作っている。このブログはそのDockerイメージを使ってHTTPS化して動いている。
 実装は自分に必要な機能に絞った。単一の証明書の取得、自動更新、リバースプロキシ。先に挙げた二つのDockerイメージは複数のサブドメインの証明書を取得できるようにしているが、個人的に必要なかったので証明書は一つにしておいた。今ならワイルドカード証明書取れるようになってるし。

https://github.com/hMatoba/cdcc
https://hub.docker.com/r/matoba/cdcc/

 ↑がそのソースとDockerイメージのリポジトリ。そもそもhttps-portalを使わなかったのは、そっちのソースを見てRuby入ってるのはなくてもできるだろうから軽量化できるなと思ったり、ちゃんとcertbotでの処理を理解しておきたかったから。
 機能を絞った代わりにちゃんと軽量化は果たしていて、https-portalは90MB超なところをぼくのは30MB未満。またSSLのセキュリティもちゃんと調整してSSLLabsでA+の評価を得られるようにしている。


 Let's Encryptやるコンテナを使うTipsとしては、ディレクトリ”/etc/letsencrypt”に外部ボリュームをマウントしたほうがいい。Dockerコマンドなりcomposeで設定できるから。じゃないと発行済み証明書がどんどん廃棄されていって、すぐに発行回数上限に達してしばらく使えなくなってしまう。
comment: 0

CircleCI 2.0でDockerイメージをビルドしてデプロイとかプッシュするための、わりと簡素な構成のconfig.yml

 CircleCIでCIをする。Dockerやるならそのアウトプットはコンテナで出てくる。CircleCI 2.0でビルド、テスト、デプロイとステージで環境がわかれてる。どうやってビルドしたコンテナを持ち越せばいいんだと。
 ステージ間でディレクトリを共有する仕組みと、Dockerのほうでイメージを単体ファイル化、およびそのロードをする仕組みを使って、ステージ間でのDockerイメージ持ち越しをやってみた。わりと最小構成になるように書いた。
https://circleci.com/gh/hMatoba/PlayCircleCi/46

 
config.yml

version: 2.0
jobs:
build:
docker:
- image: alpine

working_directory: ~/repo

steps:
- run: mkdir -p /tmp/workspace/docker
- run:
name: install docker
command: |
apk update
apk add docker
- checkout

# ↓コンテナの中でコンテナを使えないことを考えると、リモートのDockerを使うのに必須っぽいおまじない
- setup_remote_docker:
docker_layer_caching: true

- run:
name: build
command: |
docker pull alpine
docker save alpine > /tmp/workspace/docker/alpine-image.tar
# ↑Dockerイメージのファイル化
# 持ち越したいファイルをディレクトリごとアップ
- persist_to_workspace:
root: /tmp/workspace
paths:
- ./


deploy:
docker:
- image: alpine

working_directory: ~/repo
steps:
- run:
name: install
command: |
apk update
apk add docker
apk add ca-certificates # ↓でのattachに必要
- run: mkdir -p /tmp/workspace
# ↑で作業ファイルを置くディレクトリを作って、↓で持ち越されたディレクトリをアタッチしている
- attach_workspace:
at: /tmp/workspace

- setup_remote_docker:
docker_layer_caching: true

- run:
name: deploy
command: |
docker images
docker load < /tmp/workspace/docker/alpine-image.tar
docker images
workflows:
version: 2
build-and-deploy:
jobs:
- build:
filters:
branches:
only: master

- deploy:
requires:
- build
filters:
branches:
only: master
comment: 0