Libry Developers Blog

Libry開発チームによるプロダクト開発ブログ

EKS, Flux, KMS, sopsを使ったsecureなGitOps環境を構築してみた

f:id:libry:20200801172858p:plain
GitOps構成図

良いエンジニアライフを送ろう!

Libryでスクラムマスター兼フロントエンドエンジニアをしている岩西である。

今回はフロントエンドエンジニアと称しながら、その枠を超えてLibryのインフラ部分の運用改善にどのような取り組みをしているのか、一部分だけでも技術的に掘り下げて話していけたらと思う。

ここでは上の構成図の青枠で囲んだ部分(CD)を中心に書いていこうと思う。

Libryの運用改善に対する取り組み

Libryではプロダクトの改善に加えて、サービス運用改善に向けても取り組んでいる。

例えば自社開発のプロダクトをDockerコンテナを使って開発〜本番環境まで一貫して実行できるように、コンテナ周りの環境整備を徐々に進めていったりとかをしている。

そしてスモールスタートで本番環境にKubernetesを使ったコンテナオーケストレーションツールの導入も進めている。

実際に現在Libry周辺プロダクトの一部サービスはKubernetes上で稼働している。

無論まだまだこのあたりのツール運用には課題があり、そこを少しずつ取り除いていきながら従来よりもインフラ周りで運用しやすい環境を作っていく必要があるが、近いうちEC2上で稼働する全てのプロダクトをコンテナで置き換えKubernetes上に載せることになるだろう。

EKS on Fargate

Dockerコンテナを実行する環境の候補としてAWSの場合ECSやEKS、最近だとRed Hat OpenShiftを使う方法も考えられる。

GCPの場合だとGCEやGKE、Cloud Runあたりが検討に上がってくるだろう。

Libryではクラウド環境としてAWSを採用していたこともあり、スモールスタートで取り組むためAWS上でDocker実行環境を構築することになった。

その中でちょうど今回の取り組みを始めたタイミングでFargateがEKSサポートしたこともありEKS on Fargateが候補となった。

EKSはAWSが提供しているフルマネージド型のKubernetesサービスであり、ざっくり言ってしまうとマスターノードの部分をよしなにしてくれるサービスとなっている。

そのためFargateが来る前はEC2のワーカーノードを立てて管理する必要があった。

しかしFargateによるサポートが入ったEKSでは、このワーカーノードの部分もよしなにしてくれるようになり、より管理の手間を省くことができるようになってきた。(もちろん、その分EKS on Fargate固有の制約もあったりする。)

今回EKS on Fargateを使うことでKubernetesの知見を活かしつつ、サーバー管理の手間を省いたり、スケーリングによる効率的なコンピューティングリソースの活用を実現できたりするのではないかと考え、EKS on Fargateを使ってコンテナ運用環境を構築することを決定した。

eksctlを使ってEKSクラスターを作成

ここではサンプルとしてtest-clusterというEKSクラスターを作成する方法を書き残す。

EKS周りのsetupにはeksctlが使いやすい。

Fargate profileやKMSのサービスアカウントなどを定義した、ざっくりと以下のようなYAML(VPCの設定やPolicyは適宜書き換える必要あり)を書いて保存し、

./path/to/test-cluster.yaml

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: test-cluster
  region: ap-northeast-1
  version: "1.16"

vpc:
  subnets:
    public:
      ap-northeast-1a:
        id: subnet-XXXXXXXXXXXXXXXXX
      ap-northeast-1c:
        id: subnet-XXXXXXXXXXXXXXXXX
      ap-northeast-1d:
        id: subnet-XXXXXXXXXXXXXXXXX
    private:
      ap-northeast-1a:
        id: subnet-XXXXXXXXXXXXXXXXX
      ap-northeast-1c:
        id: subnet-XXXXXXXXXXXXXXXXX
      ap-northeast-1d:
        id: subnet-XXXXXXXXXXXXXXXXX

fargateProfiles:
  - name: fp-default
    selectors:
      - namespace: default
      - namespace: kube-system
  - name: fp-flux
    selectors:
      - namespace: flux

iam:
  withOIDC: true
  serviceAccounts:
    - metadata:
        name: key-management-service
        namespace: default
      attachPolicyARNs:
        - "arn:aws:iam::XXXXXXXXXXXX:policy/KeyManagementServiceReadIAMPolicy"

cloudWatch:
  clusterLogging:
    enableTypes:
      - "*"

eksctlのインストールやAWSとの認証などを済ませた環境で以下のコマンドを叩くとEKS on Fargateのクラスターが作成できる。

eksctl create cluster -f ./path/to/test-cluster.yaml

AWSコンソールや kubectl でtest-clusterが作成されたことが確認できる。

GitOpsとFlux

LibryではGitHubを使ってソースコードを管理しており、KubernetesのmanifestsなどもGitHubを使って管理している。

インフラ環境を開発者が容易に参照でき、コード上での変更・レビューができるようにGitHubをSingle Source of Truth(信頼できる唯一の情報源)としたGitOpsをLibryの運用方針として取ることにした。

GitOps実現においては、CNCFのsandbox projectにもなっているFluxを使ってみることにした。

FluxではKubernetes側からpullしてmanifestsを適用していくので、CIツールにkubectl操作のための設定する必要がなく、セキュアで手軽である。

ちなみにもう一つ有名なGitOpsツールとしてArgoがあり、さらにその2つが力を合わしたArgo Flux projectが動いていたりしている。

この中で言及されているGitOps Engineは、今の計画ではFlux v2と統合されないという旨のアナウンスがでていて今後の動向を注意深く見ていく必要があるが、近い将来より充実したGitOps環境が実現できるようになっているかもしれない。

Fluxのsetup

Fluxの使い方は簡単で、ここのGet Startedに沿って進めていけばいい。

fluxctlをインストールし、Kubernetes上にflux用のnamespaceを用意する。

kubectl create namespace flux

Kubernetesのmanifestsなどを配置するgit repository(LibryではInfra repositoryと呼んでいる)を作成し、そこの任意のディレクトリにmanifestsを配置する。

manifestsは何でも良いが、今回はサンプルとして以下を使用する。

./path/to/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  selector:
    matchLabels:
      app: sample
  replicas: 3
  template:
    metadata:
      labels:
        app: sample
    spec:
      containers:
      - image: busybox:1.32.0
        name: sample
        command:
        - "/bin/sh"
        - "-c"
        - |
          echo "started"
          while :
          do
            date
            sleep 1m
          done

例えば以下のような構成の場合、

  • Infra repository: git@github.com:libry-inc/test_cluster.git
  • ディレクトリパス: manifests/test

以下のコマンドを実行することでFluxのdeployができる。

fluxctl install \
--git-user=flux \
--git-email=flux@users.noreply.github.com \
--git-url=git@github.com:libry-inc/test_cluster.git \
--git-path=manifests/test \
--namespace=flux | kubectl apply -f -

さらに以下のコマンドでFluxのdeployが完了するのを待つことができ、

kubectl -n flux rollout status deployment/flux

successfully rolled out と表示されたらFluxのdeploy完了となる。

もし今回指定したInfra repositoryがprivateの場合は、以下のコマンドを実行してFluxコンテナ側で生成したssh公開鍵をInfra repositoryのdeploy keyとして登録しておくと良い。

fluxctl identity --k8s-fwd-ns flux

上手く稼働していると、しばらく後にInfra repositoryに配置したmanifestsが適用されているのが確認できる。

$ kubectl get deployment
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
sample-deployment   3/3     3            3           7m45s

secretsをGitHub上で管理する

GitOpsではGitHub上のコードがSingle Source of Truthである。

そのため、DBへの接続情報やAPI Keyなどの機密情報もGitHubで管理することになる。

Kubernetesのsecrets manifestsは、base64エンコードがかけられたYAMLなので容易に中身を読み解くことができる。

安全性を考えるのであれば、GitHubの権限とsecretsのような機密情報のアクセス権限は別のものとして管理する方が良いだろう。

そこで今回はAWS KMSを利用してsecretsを暗号化・復号化し、暗号化したものをGitHub上で管理していく方針を取ることにした。

sopsについて

AWS KMSを使って暗号化・復号化するにはsopsを使うと便利だ。

sopsを使って機密情報が含まれるファイルを暗号化すると、暗号化に使用したメタ情報とともに暗号化されたファイルが生成される。

今回はKubernetesのsecretsとして扱うので、それをさらにsecrets manifestsに変換することになる。

少し煩雑だが以下のような段階を踏むことになる。

  1. 平文の機密情報が含まれるファイル
    • git管理しない
  2. sopsで暗号化したファイル
    • git管理してもしなくてもいい
    • 今回はInfra repositoryにsecretsというディレクトリを作成して、そこにgit管理している
  3. secrets manifests形式のYAMLファイル
    • git管理する
    • Fluxが監視しているディレクトリに配置しなければならない

sopsとKMSを使って暗号化し、secretsを作成する

今回は以下のファイルを暗号化してみる。

./path/to/test-secret

This is secret text.

AWS KMSに任意の鍵を作成し、それのARNを取得する。

sopsがインストールされ、KMSの鍵のアクセス権限を持つ環境で以下のコマンドを実行すると暗号化されたtest-secret.encが生成される。

sops -e --kms "arn:aws:kms:ap-northeast-1:XXXXXXXXXXXX:key/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" ./path/to/test-secret > ./path/to/test-secret.enc

メタ情報を持っているので、編集するときは以下のコマンドで済む。

sops ./path/to/test-secret.enc

またgitのdifferを設定すると、復号化した内容でdiffをみることもできるので、機密情報の変更履歴も追うことができる。

暗号化したファイルをさらにsecrets manifestsにするには以下のコマンドを実行する。

kubectl create secret generic test-secret-name --from-file test-secret.enc=./path/to/test-secret.enc --dry-run -o yaml > ./path/to/secrets.yaml

生成された./path/to/secrets.yamlをInfra repositoryのmanifestsが配置されているところに追加するとFluxが読み取ってsecretsリソースをEKS上に作成する。

ちなみにsecretsなどをイミュータブルにする場合は、test-secret.encのハッシュを計算し、test-secret-nameを置き換えるようなスクリプトを用意すると便利である。

EKS上から復号化する

復号化するために、EKSのサービスアカウントに今回のKMSの鍵のアクセス権限を付与する。

今回はkey-management-serviceというサービスアカウントを作っているので、以下のコマンドでサービスアカウントのIAM RoleのARNを取得することができる。

kubectl get serviceaccount/key-management-service -o jsonpath='{.metadata.annotations.eks\.amazonaws\.com/role-arn}'

取得したARNをKMSの鍵のキーユーザーとして紐づけるとEKS上のコンテナからKMSの鍵にアクセスすることができる。

実際に復号化を行う処理はInitContainerを使って記述すると良い。

先ほどのInfra repository上のdeployment.yamlを以下のように書き換える。

./path/to/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  selector:
    matchLabels:
      app: sample
  replicas: 3
  template:
    metadata:
      labels:
        app: sample
    spec:
      serviceAccountName: key-management-service
      volumes:
      - name: secret-dir
        secret:
          secretName: test-secret-name
      - name: env-dir
        emptyDir:
          medium: Memory
      containers:
      - image: busybox:1.32.0
        name: sample
        volumeMounts:
        - name: env-dir
          mountPath: /var/workspace/env
        command:
        - "/bin/sh"
        - "-c"
        - |
          cat /var/workspace/env/test-secret
          while :
          do
            date
            sleep 1m
          done
      initContainers:
      - name: decrypt-secret
        image: mozilla/sops:v3.6.0-alpine
        volumeMounts:
        - name: secret-dir
          mountPath: /root/workspace/secrets
        - name: env-dir
          mountPath: /root/workspace/env
        command:
          - "/bin/bash"
          - "-c"
          - |
            [ -d env/test-secret ] && exit 0
            cd /root/workspace
            sops -d ./secrets/test-secret.enc > ./env/test-secret

変更内容をInfra repositoryに反映し、しばらくするとコンテナ上でtest-secretを復号化して読み込まれていることが確認できる。

$ kubectl logs pod/sample-deployment-XXXXXXXXX-XXXXX
This is secret text.
Sat Aug  1 07:57:40 UTC 2020
Sat Aug  1 07:58:40 UTC 2020

最後に

Libryでは現在、このようにしてGitOps構成図にあるような環境をスモールスタートではあるが運用しはじめている。

今回書くことができなかったCIの部分やこれからさらに取り組んでいくことなどまだまだこの領域には奥が深い内容が多くあり、フロントエンドエンジニアと称しながらも役割を超えて楽しく携わっている。

課題は多くあり、この運用が変わる可能性は十分にあるが、少しでもLibryがどんな技術を扱っているのか参考になれば幸いである。