JoTech

日々学んだことを垂れ流します

Loading the data just for you.

VeleroでEKSクラスタをバックアップ

VeleroでEKSクラスタのバックアップを試してみたので手順をまとめます。
今回はプロバイダーがAWS、オブジェクトストアがS3なのでvelero-plugin-for-awsの手順に従ってインストールを行います。 その他サポートしているプロバイダーはこちらを参照してください。

github.com

実行環境

  • macOS Catalina Version 10.15.7
  • velero v1.5.2

CLIのインストール

Homebrewで velero をインストールします。

$ brew install velero

S3バケット作成

バックアップを保存するS3バケットを作成します。

$ BUCKET=<YOUR_BUCKET>
$ REGION=ap-northeast-1
$ aws s3api create-bucket \
    --bucket $BUCKET \
    --region $REGION \
    --create-bucket-configuration LocationConstraint=$REGION

# パブリックアクセスをブロック
$ aws s3api put-public-access-block \
    --bucket $BUCKET \
    --region $REGION \
    --public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

f:id:hiroki-sawano:20201218185128p:plain

IAMユーザの作成

IAMユーザ velero を作成します。

$ aws iam create-user --user-name velero

f:id:hiroki-sawano:20201218185214p:plain

IAMポリシーの追加

IAMユーザ velero に次の通り、IAMポリシーを割り当てます。

$ cat > velero-policy.json <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeVolumes",
                "ec2:DescribeSnapshots",
                "ec2:CreateTags",
                "ec2:CreateVolume",
                "ec2:CreateSnapshot",
                "ec2:DeleteSnapshot"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:PutObject",
                "s3:AbortMultipartUpload",
                "s3:ListMultipartUploadParts"
            ],
            "Resource": [
                "arn:aws:s3:::${BUCKET}/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::${BUCKET}"
            ]
        }
    ]
}
EOF

$ aws iam put-user-policy \
   --user-name velero \
   --policy-name velero \
   --policy-document file://velero-policy.json

f:id:hiroki-sawano:20201218185656p:plain

アクセスキーの作成

velero ユーザのアクセスキーを設定します。

$ aws iam create-access-key --user-name velero

{
    "AccessKey": {
        "UserName": "velero",
        "AccessKeyId": "<access-key>",
        "Status": "Active",
        "SecretAccessKey": "<secret-access-key>",
        "CreateDate": "2020-12-18T09:58:03+00:00"
    }
}
$ cat > velero-credentials <<EOF
[default]
aws_access_key_id=<access-key>
aws_secret_access_key=<secret-access-key>
EOF

Veleroのインストール

EKSクラスタにVeleroをインストールします。

$ velero install \
    --provider aws \
    --plugins velero/velero-plugin-for-aws:v1.1.0 \
    --bucket $BUCKET \
    --backup-location-config region=$REGION \
    --snapshot-location-config region=$REGION \
    --secret-file ./velero-credentials

...
Waiting for resources to be ready in cluster...
Namespace/velero: attempting to create resource
Namespace/velero: created
ClusterRoleBinding/velero: attempting to create resource
ClusterRoleBinding/velero: created
ServiceAccount/velero: attempting to create resource
ServiceAccount/velero: created
Secret/cloud-credentials: attempting to create resource
Secret/cloud-credentials: created
BackupStorageLocation/default: attempting to create resource
BackupStorageLocation/default: created
VolumeSnapshotLocation/default: attempting to create resource
VolumeSnapshotLocation/default: created
Deployment/velero: attempting to create resource
Deployment/velero: created
Velero is installed! ⛵ Use 'kubectl logs deployment/velero -n velero' to view the status.

velero 名前空間にVeleroがデプロイされました。

$ kubectl get all -n velero
NAME                          READY   STATUS    RESTARTS   AGE
pod/velero-866bf8c8d5-dgtcq   1/1     Running   0          90s

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/velero   1/1     1            1           90s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/velero-866bf8c8d5   1         1         1       90s

バックアップ対象となるPersistentVolumeを用意

VeleroはPersistentVolume(EBS)のスナップショットを取得してくれるので、 動作確認のために適当なボリュームを用意しておきます。

❯ kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM   STORAGECLASS   REASON   AGE
pvc-...   10Gi       RWO            Delete           Bound    ...     gp2                     22d
pvc-...   1Gi        RWO            Delete           Bound    ...     gp2                     21d
pvc-...   10Gi       RWO            Delete           Bound    ...     gp2                     23d
pvc-...   10Gi       RWO            Delete           Bound    ...     gp2                     23d
pvc-...   10Gi       RWO            Delete           Bound    ...     gp2                     22d
pvc-...   10Gi       RWO            Delete           Bound    ...     gp2                     22d

バックアップ

次のコマンドでバックアップを取得します。

❯ velero backup create backup-20201218
Backup request "backup-20201218" submitted successfully.
Run `velero backup describe backup-20201218` or `velero backup logs backup-20201218` for more details.

バックアップの状況は velero backup describe で確認できます。

  • バックアップ中
❯ velero backup describe backup-20201218
Name:         backup-20201218
Namespace:    velero
Labels:       velero.io/storage-location=default
Annotations:  velero.io/source-cluster-k8s-gitversion=v1.18.9-eks-d1db3c
              velero.io/source-cluster-k8s-major-version=1
              velero.io/source-cluster-k8s-minor-version=18+

Phase:  InProgress

Errors:    0
Warnings:  0

Namespaces:
  Included:  *
  Excluded:  <none>

Resources:
  Included:        *
  Excluded:        <none>
  Cluster-scoped:  auto

Label selector:  <none>

Storage Location:  default

Velero-Native Snapshot PVs:  auto

TTL:  720h0m0s

Hooks:  <none>

Backup Format Version:  1.1.0

Started:    2020-12-18 19:30:00 +0900 JST
Completed:  <n/a>

Expiration:  2021-01-17 19:30:00 +0900 JST

Estimated total items to be backed up:  607
Items backed up so far:                 147

Velero-Native Snapshots: <none included>
  • バックアップ完了
❯ velero backup describe backup-20201218
Name:         backup-20201218
Namespace:    velero
Labels:       velero.io/storage-location=default
Annotations:  velero.io/source-cluster-k8s-gitversion=v1.18.9-eks-d1db3c
              velero.io/source-cluster-k8s-major-version=1
              velero.io/source-cluster-k8s-minor-version=18+

Phase:  Completed

Errors:    0
Warnings:  0

Namespaces:
  Included:  *
  Excluded:  <none>

Resources:
  Included:        *
  Excluded:        <none>
  Cluster-scoped:  auto

Label selector:  <none>

Storage Location:  default

Velero-Native Snapshot PVs:  auto

TTL:  720h0m0s

Hooks:  <none>

Backup Format Version:  1.1.0

Started:    2020-12-18 19:30:00 +0900 JST
Completed:  2020-12-18 19:30:12 +0900 JST

Expiration:  2021-01-17 19:30:00 +0900 JST

Total items to be backed up:  561
Items backed up:              561

Velero-Native Snapshots:  6 of 6 snapshots completed successfully (specify --details for more information)

取得したバックアップは velero get backups で一覧表示できます。

$ velero get backups
NAME                   STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
backup-20201218        Completed   0        0          2020-12-18 19:30:00 +0900 JST   29d       default            <none>

S3バケット作成で用意したバケットにバックアップが作成されました。

f:id:hiroki-sawano:20201218193401p:plain

PVはEBSのスナップショットとしてバックアップされています。

f:id:hiroki-sawano:20201218193854p:plain

バックアップの削除

velero backup delete でS3に保存したバックアップやEBSのスナップショットも全て削除してくれます。

$ velero backup delete backup-20201218
Are you sure you want to continue (Y/N)? Y
Request to delete backup "backup-20201218" submitted successfully.
The backup will be fully deleted after all associated data (disk snapshots, backup files, restores) are removed.

バックアップスケジュールの設定

実運用では自動的に日時バックアップを取得したいので、次のマニフェストを作成します。
spec.schedule にバックアップの取得スケジュール(UTC)、 spec.template.includedNamespaces にバックアップする名前空間spec.template.ttl に保持期間を指定します。
Schedule APIの詳細についてはこちらを参照してください。

apiVersion: velero.io/v1
kind: Schedule
metadata:
  name: daily
  namespace: velero
spec:
  schedule: 0 0 * * *
  template:
    includedNamespaces:
    - '*'
    ttl: 720h0m0s
$ kubectl apply -f daily-backup.yml 

0時(UTC)にバックアップされるかと思いきや、即時バックアップが作成されました。 特に問題でもないですが、こちらのIssueの通り、指定した時刻だけでなく即時バックアップされる不具合があるそうです。

$ velero get backups
NAME                   STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
daily-20201218105219   Completed   0        0          2020-12-18 19:52:19 +0900 JST   29d       default            <none>

日をおいて確認すると、日毎のバックアップが期待通り取得されていました。

❯ velero get backups
NAME                   STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
daily-20201220000053   Completed   0        0          2020-12-20 09:00:53 +0900 JST   29d       default            <none>
daily-20201219000053   Completed   0        0          2020-12-19 09:00:53 +0900 JST   28d       default            <none>
daily-20201218105219   Completed   0        0          2020-12-18 19:52:19 +0900 JST   27d       default            <none>

f:id:hiroki-sawano:20201220220053p:plain

参考

JupyterHubをKubernetes(Amazon EKS)にインストール(zero-to-jupyterhub-k8s 0.9.0)

Amazon EKSにJupyterHub zero-to-jupyterhub-k8s(Z2JH)でインストールする方法をまとめます。

github.com

以下の手順に従って作業を実施します。

Zero to JupyterHub with Kubernetes — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

Kubernetesのセットアップ

EC2をワーカーノードとしたEKSクラスタを作成します。Fargateへの導入も試しましたが、執筆時点(2020/11/27)では後述の通り未対応のようです。

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: <cluster-name>
  region: ap-northeast-1
  version: "1.18"

managedNodeGroups:
  - name: <node-groupname>
    ...
$ eksctl create cluster -f cluster.yaml

Helmのセットアップ

Helmをインストールします。

$ curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 11213  100 11213    0     0  38010      0 --:--:-- --:--:-- --:--:-- 38010
Downloading https://get.helm.sh/helm-v3.4.1-darwin-amd64.tar.gz
Verifying checksum... Done.
Preparing to install helm into /usr/local/bin
Password:
helm installed into /usr/local/bin/helm

以下のコマンドを実行し、空のリストが表示されればインストール成功です。

$ helm list
NAME    NAMESPACE       REVISION        UPDATED STATUS  CHART   APP VERSION

JupyterHub のセットアップ

設定ファイルの準備

セキュリティトークproxy.secretToken を設定した設定ファイル config.yaml を作成します。
config.yaml はHelmのValuesファイルです。

$ cat << EOF > config.yaml
proxy:
  secretToken: "$(openssl rand -hex 32)"
EOF

JupyterHubのインストール

JupyterHubのHelmチャートリポジトリを追加します。

$ helm repo add jupyterhub https://jupyterhub.github.io/helm-chart/
"jupyterhub" has been added to your repositories

$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "jupyterhub" chart repository
Update Complete. ⎈Happy Helming!⎈

config.yaml で設定したチャートをインストールします。
$RELEASE にはHelmのReleaseを、 $NAMESPACE にはKubernetesNamespaceを設定しています。

$ RELEASE=jhub
$ NAMESPACE=jhub

$ helm upgrade --cleanup-on-fail \
  --install $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --create-namespace \
  --version=0.9.0 \
  --values config.yaml

Release "jhub" does not exist. Installing it now.

Podの起動を確認します。

$ kubectl get pods -n jhub
NAME                              READY   STATUS    RESTARTS   AGE
continuous-image-puller-7hwqt     1/1     Running   0          2m6s
continuous-image-puller-7p8ln     1/1     Running   0          2m6s
continuous-image-puller-86c7j     1/1     Running   0          2m6s
hub-85f5d4b66b-nsk6q              1/1     Running   0          2m6s
proxy-694ff48877-l5dph            1/1     Running   0          2m6s
user-scheduler-56557c648d-sfh7f   1/1     Running   0          2m6s
user-scheduler-56557c648d-t5hb8   1/1     Running   0          2m6s

Service proxy-publicLoadBalancerとして作成されています。

$ kubectl get svc proxy-public -n jhub
NAME           TYPE           CLUSTER-IP       EXTERNAL-IP                                                                   PORT(S)                      AGE
proxy-public   LoadBalancer   ...   ...   443:31337/TCP,80:30630/TCP   3h25m

公式の手順通りであればここで proxy-publicEXTERNAL-IP に接続できますが、 バージョン 0.9.0 だと以下のIssueによりロードバランサが落ちるため接続できません。
デフォルトでHTTPSが有効( proxy.https.enabled=true )となったことで、HTTPSが未設定の場合にヘルスチェックで落ちるようです。

github.com

HTTPS対応は後述するACMの証明書を使用したSSLの設定で対応するとして、一旦 https.enabledfalse を設定することで問題を回避します。

proxy:
  secretToken: "..."
  https:
    enabled: false

設定を反映します。

$ helm upgrade --cleanup-on-fail \
  $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --version=0.9.0 \
  --values config.yaml

これで EXTERNAL-IP に接続できるようになります。
初期設定のAuthenticatorDummy Authenticatorなので好きなユーザでログインできます。

f:id:hiroki-sawano:20201125155418p:plain

ログインするとJupyter Notebookイメージを使用したシングルユーザJupyterノートブックサーバがPod jupyter-<username> (以降、ユーザPod)で起動し、ストレージ(PersistentVolume/PersistentVolumeClaim)が割り当てられます。

f:id:hiroki-sawano:20201127132034p:plain

以下はユーザ rick でログインした場合に作成されるリソースの例です。

$ kubectl get pods -n jhub
NAME                              READY   STATUS    RESTARTS   AGE
...
jupyter-rick                      1/1     Running   0          3m9s

$ kubectl get pv -n jhub
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                        STORAGECLASS   REASON   AGE
...
pvc-c60b01b1-7f42-44a6-8662-f54631bc5a52   10Gi       RWO            Delete           Bound    jhub/claim-rick              gp2                     21h

$ kubectl get pvc -n jhub
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
...
claim-rick      Bound    pvc-c60b01b1-7f42-44a6-8662-f54631bc5a52   10Gi       RWO            gp2            21h

ユーザPodは画面右上の QuitControl Panel->Stop My Server で削除されますが、 JupyterHubは一定時間ユーザによるアクションがない非アクティブなユーザPodを自動的に削除し、リソースを開放してくれます。
この挙動は config.yamlcull で変更可能です。

Customizing User Management — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

PVとPVCは明確に削除しない限りはユーザがサーバをシャットダウンしても残り続けます。
標準ではEBS(General Purpose SSD)にデータが保存されますが config.yamlsingleuser.storage で変更可能です。

Customizing User Storage — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

ACMの証明書を使用したSSLの設定

JupyterHubのインストールではHTTPで接続しましたが、より安全に通信するためにACMで管理する証明書を使用してSSLの設定を行います。
config.yamlproxy.httpsproxy.service の設定を追記します。
proxy.https.hosts には接続に使用するホスト名を、 proxy.service.annotationsservice.beta.kubernetes.io/aws-load-balancer-ssl-cert には証明書のARNを指定してください。

Security — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

proxy:
  secretToken: "..."
  https:
    enabled: true
    type: offload
    hosts: <hostname>
  service:
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "<arn>"
      service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "tcp"
      service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "https"
      service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: '3600'

設定を反映します。

$ helm upgrade --cleanup-on-fail \
  $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --version=0.9.0 \
  --values config.yaml

続けてRoute53でエイリアスレコードを作成し、 proxy.https.hosts に設定したホスト名で proxy-public (CLB)にトラフィックを流します。

f:id:hiroki-sawano:20201125170534p:plain

これでSSL通信に対応することができました。

f:id:hiroki-sawano:20201126185036p:plain

Jupyter Notebookイメージの変更

ユーザが使用するJupyter NotebookのDockerイメージは singleuser.image で指定することができます。

Customizing User Environment — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

Jupyter Docker Stacksから目的にあったイメージを選択します。
以下はJulia、Python、Rのデータ分析ライブラリが詰まったdatascience-notebookを使用する例です。

singleuser:
  image:
    name: jupyter/datascience-notebook
    tag: 177037d09156

設定を反映します。

$ helm upgrade --cleanup-on-fail \
  $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --version=0.9.0 \
  --values config.yaml

Nodeにアクセスし、期待通りのイメージがPullされていることを確認できました。

$ ssh -i /path/to/key ec2-user@<node-external-ip>
[ec2-user@ip-... ~]$ docker images jupyter/datascience-notebook
REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
jupyter/datascience-notebook   177037d09156        fb0e8e0b07c7        2 years ago         6.11GB

これでJulia、Python、Rのノートブックが作成できるようになりました。

f:id:hiroki-sawano:20201125181509p:plain

Adminユーザの設定

ユーザのサーバを管理するAdminユーザは auth.admin.users で設定できます。

Customizing User Management — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

auth:
  admin:
    users:
      - adminuser1
      - adminuser2

Adminユーザは Control Panel->Admin から各ユーザのサーバを起動・停止したり、ユーザのノートブックにアクセスすることができます。

f:id:hiroki-sawano:20201127140903p:plain

Authenticatorの変更

標準のDummy Authenticatorを適切なAuthenticatorに変更します。
以下のリンク先を参考にセットアップすることで、GitHubGoogle等のユーザでログインすることができます。

Authentication — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

標準でサポートしていないAuthenticatorを使用する場合は、後述するHubイメージの変更で使用したいAuthenticatorをJupyterHubにインストールし、任意の設定を適用(jupyterhub_config.pyの設定)c.JupyterHub.authenticator_classを設定します。

Hubイメージの変更

Hubイメージをカスタマイズしたい場合には以下の手順を実行します。

  • jupyterhub/k8s-hub:0.9.0 をベースとしたDockerfileを作成(※タグはHelmチャートのバージョンによって異なる)
  • 作成したイメージをDockerレジストリ(ECR)にPush
  • config.yamlhub.image でECRのレポジトリとタグを指定

ここでは例として、REMOTE_USERでユーザを認証するREMOTE_USER authenticatorをHubイメージに追加してみます。

FROM jupyterhub/k8s-hub:0.9.0
RUN pip install jhub_remote_user_authenticator

ビルドしたコンテナイメージをECRにPushします。名前やタグは任意ですがベースイメージと合わせました。

f:id:hiroki-sawano:20201125201459p:plain

config.yaml を編集し、ECRに登録したHubイメージを指定します。

hub:
  image:
    name: ...ecr.ap-northeast-1.amazonaws.com/k8s-hub
    tag: 0.9.0

設定を反映します。

$ helm upgrade --cleanup-on-fail \
  $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --version=0.9.0 \
  --values config.yaml

起動したHubのPodに接続し、インストールの成功を確認します。

$ kubectl exec -it <hub-pod> -n jhub -- bash
jovyan@hub-...:/srv/jupyterhub$ pip freeze | grep authenticator
jhub-remote-user-authenticator==0.1.0
...

任意の設定を適用(jupyterhub_config.pyの設定)

Helmチャートで用意された設定では対応できない場合、 hub.extraConfig でJupyterHubの設定ファイル( jupyterhub_config.py )に任意のコードを追加することができます。

Advanced Topics — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

前述のHubイメージの変更で導入したREMOTE_USER authenticatorを使用してみます。

hub:
  extraConfig:
    authConfig: |
      c.JupyterHub.authenticator_class = 'jhub_remote_user_authenticator.remote_user_auth.RemoteUserAuthenticator'

Pod起動時のログを確認し、 Using Authenticator: ...RemoteUserAuthenticator と表示されていることが確認できました。

$ stern hub -n jhub
+ hub-c88c447c4-9c6t2 › hub
hub-c88c447c4-9c6t2 hub Loading /etc/jupyterhub/config/values.yaml
hub-c88c447c4-9c6t2 hub Loading /etc/jupyterhub/secret/values.yaml
hub-c88c447c4-9c6t2 hub Loading extra config: authConfig
hub-c88c447c4-9c6t2 hub [I 2020-11-25 11:32:20.104 JupyterHub app:2240] Running JupyterHub version 1.1.0
hub-c88c447c4-9c6t2 hub [I 2020-11-25 11:32:20.105 JupyterHub app:2271] Using Authenticator: jhub_remote_user_authenticator.remote_user_auth.RemoteUserAuthenticator
...

実際にHubにアクセスしてみると、REMOTE_USERがHTTPヘッダーにないため 401 : Unauthorized エラーとなります。

f:id:hiroki-sawano:20201125203433p:plain

Chrome拡張 ModHeaderでREMOTE_USERを設定するとログインに成功します。

f:id:hiroki-sawano:20201125204723p:plain

f:id:hiroki-sawano:20201125204913p:plain

おまけ:EKS on Fargateは未サポート

おまけです。今回ご紹介したHelmチャートはFargateに対応していませんが、インストールに挑戦してみた結果を残しておきます。
確認には以下のエントリで構築したEKSクラスタを使用しました。

hiroki-sawano.hatenablog.com

JupyterHubのインストールと同様に、以下のコマンドでチャートをインストールします(Namespaceはdefaultを指定)。

$ RELEASE=jhub
$ NAMESPACE=default

$ helm upgrade --cleanup-on-fail \
  --install $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --create-namespace \
  --version=0.9.0 \
  --values config.yaml

チャートのインストールがいつまで経っても終わらず、タイムアウトしてしまいました。

Error: failed pre-install: timed out waiting for the condition
$ helm list
NAME    NAMESPACE   REVISION    UPDATED                                 STATUS  CHART               APP VERSION
jhub    default     1           2020-11-23 20:19:41.511629 +0900 JST    failed  jupyterhub-0.9.0    1.1.0   

Podを確認すると hook-image-awaiter-xxxxx がエラー終了しています。

$ kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
hook-image-awaiter-52k6w          0/1     Pending   0          3s
hook-image-awaiter-ksxb5          0/1     Error     0          3m49s
hook-image-awaiter-p6tx4          0/1     Error     0          7m31s

エラーログは以下の通りです。

$ kubectl logs hook-image-awaiter-p6tx4
2020/11/23 11:23:28 Get https://10.100.0.1:443/apis/apps/v1/namespaces/default/daemonsets/hook-image-puller: dial tcp 10.100.0.1:443: getsockopt: connection timed out

hook-image-puller にFargateがサポートしていないDaemonSetを使用しているためだと思われます。
hook-image-puller は事前にDockerイメージを各NodeにPullしてくれるリソースです。

With the hook-image-puller enabled (the default), the user images being introduced will be pulled to the nodes before the hub pod is updated to utilize the new image.

関連するIssueも見つかりました。特に問題は解決しないままクローズされています。

github.com

ユーザPod起動時にイメージをPullする時間を許容できると仮定し、 DaemonSetを使わないようにhook-image-puller を無効化してみます。
同様にノード追加時にイメージをプルする continuous-image-puller も無効化します。

Optimizations — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

proxy:
  secretToken: "..."
prePuller:
  hook:
    enabled: false
  continuous:
    enabled: false

作成したリソースを削除し、再度 helm upgrade を実行します。
コマンドの実行が完了し、一見うまくいったように見えます。

$ helm upgrade --cleanup-on-fail \
  --install $RELEASE jupyterhub/jupyterhub \
  --namespace $NAMESPACE \
  --create-namespace \
  --version=0.9.0 \
  --values config.yaml

Release "jhub" does not exist. Installing it now.
NAME: jhub
LAST DEPLOYED: Mon Nov 23 21:47:05 2020
NAMESPACE: jhub
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Thank you for installing JupyterHub!

Your release is named jhub and installed into the namespace jhub.

You can find if the hub and proxy is ready by doing:

 kubectl --namespace=jhub get pod

and watching for both those pods to be in status 'Running'.

You can find the public IP of the JupyterHub by doing:

 kubectl --namespace=jhub get svc proxy-public

It might take a few minutes for it to appear!

Note that this is still an alpha release! If you have questions, feel free to
  1. Read the guide at https://z2jh.jupyter.org
  2. Chat with us at https://gitter.im/jupyterhub/jupyterhub
  3. File issues at https://github.com/jupyterhub/zero-to-jupyterhub-k8s/issues

しかし、HubのPod( hub-xxxx )がPendingのままで起動することはありませんでした。

$ kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
hub-5787d5cbc7-lnhsg              0/1     Pending   0          4m2s
proxy-694ff48877-8l7zl            1/1     Running   0          4m2s
user-scheduler-5f6c6d896d-8cnqh   1/1     Running   0          4m2s
user-scheduler-5f6c6d896d-l28mf   1/1     Running   0          4m2s

ここで大人しくEC2を使用する方針に切り替えたのでこれ以上の調査は行っていません。
正式にサポートされた際には再度確認したいと思います。

さいごに

本エントリではZ2JHを使用してJupyterHubをEKSに導入する手順をまとめました。
J2JHでは今回触れたこと以外にもユーザリソースの設定アクセス制限JupyterLabを標準で使用するなど、様々な設定が可能です。
詳細が気になる方はCustomization GuideAdministrator Guideをご確認ください。

DjangoでViewのリクエスト前後に任意の処理を差し込む

デコレータを使用し、DjangoのViewでGETやPOSTリクエストの処理前後に任意の処理を差し込む方法をまとめます。

やりたいこと

例えば以下のようなViewにおいて、 MyView.get の処理前に @pre_request で指定した program1 関数を実行し、 処理後に @post_response で指定した program2 関数を実行できるようにします。

class MyView(View):
    @pre_request(program1)
    @post_response(program2)
    def get(self, request, *args, **kwargs):
        ...

デコレータの作成

decorators モジュールに事前処理を実行するデコレータ pre_request と事後処理を実行するデコレータ post_response を実装します。
いずれも実行する関数( program )を引数にとります。
実のところ特にDjangoの要素はなく、単なるPythonのデコレータです。

from functools import wraps


def pre_request(program):
    """事前処理"""
    def _decorator(function):
        @wraps(function)
        def wrap(self, request, *args, **kwargs):
            # 引数の関数を実行
            program(self, request, *args, **kwargs)
            # デコレートした関数(getなど)を実行
            return function(self, request, *args, **kwargs)
        return wrap
    return _decorator


def post_response(program):
    """事後処理"""
    def _decorator(function):
        @wraps(function)
        def wrap(self, request, *args, **kwargs):
            # デコレートした関数(getなど)を実行
            response = function(self, request, *args, **kwargs)
            # 引数の関数を実行
            # HTTPレスポンスも引数に追加
            return program(self, request, response, *args, **kwargs)
        return wrap
    return _decorator

Viewの実装

あとは decorators から pre_requestpost_response をインポートして使うだけです。

from django.views.generic.base import View

from .decorators import post_response, pre_request

def program1(self, request, *args, **kwargs):
    """事前処理"""
    ...

def program2(self, request, response, *args, **kwargs):
    """事後処理"""
    ...
    # レスポンスを返すこと
    return response


class MyView(View):
    @pre_request(program1)
    @post_response(program2)
    def get(self, request, *args, **kwargs):
        ...

利用例

(実用性は無視して)簡単な利用例を挙げておきます。
get()post() の引数を扱えるので、工夫次第で色々できます。

事前処理でビューへのアクセス権限を判定

以下の関数は一般ユーザーのアクセスを拒否します。
この例はあまり実用性がないですが、@permisson_requiredでは対応しきれないような決めの細かい権限管理が必要な場合などに使用できるかもしれません。

from django.core.exceptions import PermissionDenied


def do_not_accept_general_user(self, request, *args, **kwargs):
    """一般ユーザのアクセス拒否"""
    if not request.user.is_superuser:
        raise PermissionDenied()

事後処理でレスポンスを更新

事後処理は HttpResponse を受け取るため、コンテキストの追加変更やレスポンスそのものの上書き(遷移先の変更やURLパラメタの追加など)が可能です。

def update_response(self, request, response, *args, **kwargs):
    """コンテキストの書き換え"""
    if something:
        response.context_data['foo'] = '..'
    return response

まとめ

デコレータでViewに任意の事前処理、事後処理を差し込む方法をご紹介しました。
Viewに共通する処理をどのように切り出すかは状況に依りますが(CBVの継承やバックエンド、シグナルの実装など)、任意の機能を付け外しできるデコレータは使い方次第で大きな効果を発揮すると思います。

SAMLtestでSAML2.0(Shibboleth)認証をテスト

SAML2.0 IdP/SPのテストに便利なサービスSAMLtest.idを見つけました。
実際にShibboleth認証に対応したSPのテストに使ってみたので、本エントリに手順をまとめます。

f:id:hiroki-sawano:20201110195434p:plain

テスト対象のSP

$ yum update -y

# Shibbolethのインストール
$ yum install -y wget
$ wget 'https://shibboleth.net/cgi-bin/sp_repo.cgi?platform=CentOS_7'
$ cp sp_repo.cgi\?platform=* /etc/yum.repos.d/shibboleth.repo
$ yum install -y shibboleth

# Apacheのインストール
$ yum install -y httpd mod_ssl

使い方

SPのメタデータをアップロード

Upload MetadataからSPのメタデータをアップロードします。

f:id:hiroki-sawano:20201110190146p:plain

Shibboleth.sso/Metadata から取得したメタデータをアップロードしました。

f:id:hiroki-sawano:20201110214138p:plain

IdPの追加

Download Metadataに従い、SAMLtestのメタデータを追加し、デフォルトのIdPとしてhttps://samltest.id/saml/idpを指定しました。

<SPConfig ...>
  <ApplicationDefaults ...>
    <Sessions ...>
      <SSO entityID="https://samltest.id/saml/idp">
        SAML2
      </SSO>
    </Sessions>

    <MetadataProvider type="XML" validate="true"
            url="https://samltest.id/saml/idp"
            backingFilePath="SAMLtest.xml">
         <!-- You should always check the signature and freshness of remote
                 metadata.  It's commented out until you get the basics working.
              <MetadataFilter type="RequireValidUntil" maxValidityInterval="2419200"/>
              <MetadataFilter type="Signature" certificate="signet.crt" verifyBackup="false"/>
            -->
    </MetadataProvider>
...

SPにログイン

以上の設定によりSPにアクセスするとSAMLtestのIdPにリダイレクトされるようになります。
ログインに使用できるテストユーザはIdPのログイン画面に表示されています。

f:id:hiroki-sawano:20201110194200p:plain

IdPでログインに成功すると以下の通り各種属性がSPに送出されます。

f:id:hiroki-sawano:20201110190319p:plain

まとめ

本エントリではSAML認証のテストに役立つSAMLtestをご紹介しました。
SSOが当たり前となった今、このようなサービスの存在は大変ありがたいです。
SAMLtestだけでは届かない部分ももちろんありますが、SP構築中の簡易な接続確認などにおいて、今後とも活用していこうと思います。

EKS(on Fargate)でEFSにデータを永続化

EKSでは積極的にRDS等のマネージドサービスでデータを保存したいところですが、種々の事情により永続ボリュームを用意しなければならないこともあります。
先日、PostgreSQLコンテナのデータをEFSを用いて永続化してみたので以降に手順をまとめます。

Amazon EFS CSI driverのデプロイ

リンク先の「Option B」を参考にAmazon EFS CSI driverをデプロイします。

Use persistent storage in Amazon EKS

この手順を終えると以下のようにEFSファイルシステムが出来上がります。

f:id:hiroki-sawano:20201101170114p:plain

永続ストレージの設定

次のマニフェストStorageClassPersistentVolumeおよびPersistentVolumeClaimを作成します。
PVの spec.sci.volumeHandle には前述の手順で作成したEFSのファイルシステムIDを指定します。
PVの spec.capacity.storage やPVCの spec.resources.requests.storageKubernetesの必須項目であるため設定が必要ですが、EFSは伸縮自在なファイルシステムであるため実際に容量は制限されません。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: efs-sc
provisioner: efs.csi.aws.com
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: efs-pv
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: efs-sc
  csi:
    driver: efs.csi.aws.com
    volumeHandle: fs-xxxxxxxx
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: efs-claim
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: efs-sc
  resources:
    requests:
      storage: 5Gi
$ kubectl apply -f efs.yml
storageclass.storage.k8s.io/efs-sc created
persistentvolume/efs-pv created
persistentvolumeclaim/efs-claim created

EFSをマウントするDeploymentの作成

EFSでデータを永続化するPostgreSQLコンテナを用意します。
使用するコンテナイメージは以下のエントリで紹介したCentOS7ベースのPostgreSQLです(バージョンは9.6に変更)。
起動時にエントリポイントでDBクラスタを作成( initdb )します。

hiroki-sawano.hatenablog.com

DBクラスタディレクト/var/lib/pgsql/9.6/data (以降、PGDATA)に persistent-storage と名付けたボリュームをマウントします。

apiVersion: apps/v1
kind: Deployment
metadata:
  ...
spec:
  ...
  template:
    ...
    spec:
      containers:
      - name: db
        ...
        volumeMounts:
        - mountPath: /var/lib/pgsql/9.6/data
          name: persistent-storage
      volumes:
      - name: persistent-storage
        persistentVolumeClaim:
          claimName: efs-claim
$ kubectl apply -f db.yml
deployment.apps/db created

PGDATAの書き込みに失敗

ログを確認してみるとPGDATAを initdb で初期化する際、権限不足でエラーとなっていました。
ボリュームはrootでマウントされるので、実行ユーザであるpostgresが書き込み権限を持っていなかったわけです。

$ kubectl logs <db-pod-name>
...
initdb: could not change permissions of directory "/var/lib/pgsql/9.6/data": Operation not permitted
...

initContainersで所有者変更

fsGroupでいける気がしましたが、どうにもうまくいかなかったのでinitContainersを使います。

stackoverflow.com

db コンテナが起動する前に change-data-dir-ownership コンテナでPGDATAの所有者を 26:26 ( postgres:postgres )に変更します。

注意: postgresのUIDとGIDは環境により異なる場合があります(PostgreSQL: Re: Default UID for postgres user in linux

apiVersion: apps/v1
kind: Deployment
metadata:
  ...
spec:
  ...
  template:
    ...
    spec:
      initContainers:
      - name: change-data-dir-ownership
        image: alpine:3
        command:
        - chown
        - -R
        - 26:26
        - /var/lib/pgsql/9.6/data
        volumeMounts:
        - name: persistent-storage
          mountPath: /var/lib/pgsql/9.6/data
      containers:
      ...
$ kubectl delete -f db.yml
$ kubectl apply -f db.yml

正常起動の確認

これでデータベースの初期化に成功し、コンテナが正常稼働しました。

$ kubectl logs -f  <db-pod-name>
...

fixing permissions on existing directory /var/lib/pgsql/9.6/data ... ok
creating subdirectories ... ok
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default timezone ... UTC
selecting dynamic shared memory implementation ... posix
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

Success. You can now start the database server using:

    pg_ctl -D /var/lib/pgsql/9.6/data -l logfile start


WARNING: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.
waiting for server to start....< 2020-10-27 05:01:48.580 UTC >LOG:  redirecting log output to logging collector process
< 2020-10-27 05:01:48.580 UTC >HINT:  Future log output will appear in directory "pg_log".
 done
server started
CREATE DATABASE
...
waiting for server to shut down.... done
server stopped
< 2020-10-27 05:02:24.104 UTC >LOG:  redirecting log output to logging collector process
< 2020-10-27 05:02:24.104 UTC >HINT:  Future log output will appear in directory "pg_log".

データベースの一覧を確認します。

$ kubectl exec -it <db-pod-name> -- psql -l
                             List of databases
   Name    |  Owner   | Encoding | Collate | Ctype |   Access privileges   
-----------+----------+----------+---------+-------+-----------------------
 postgres  | postgres | UTF8     | C       | C     | 
 template0 | postgres | UTF8     | C       | C     | =c/postgres          +
           |          |          |         |       | postgres=CTc/postgres
 template1 | postgres | UTF8     | C       | C     | =c/postgres          +
           |          |          |         |       | postgres=CTc/postgres
 your_db   | postgres | UTF8     | C       | C     | 
(4 rows)

PGDATA以下のファイルとディレクトリも確認しておきます。

$ kubectl exec -it  <db-pod-name> -- ls /var/lib/pgsql/9.6/data
PG_VERSION    pg_dynshmem    pg_multixact  pg_stat  pg_xlog
base          pg_hba.conf    pg_notify     pg_stat_tmp  postgresql.auto.conf
global        pg_ident.conf  pg_replslot   pg_subtrans  postgresql.conf
pg_clog       pg_log         pg_serial     pg_tblspc    postmaster.opts
pg_commit_ts  pg_logical     pg_snapshots  pg_twophase  postmaster.pid

再デプロイし、DB初期化処理がスキップされ、かつ前回作成したデータベースが存在することを確認します。

$ kubectl delete -f db.yml
$ kubectl apply -f db.yml
$ kubectl logs <db-pod-name>
< 2020-10-27 05:18:17.340 UTC >LOG:  redirecting log output to logging collector process
< 2020-10-27 05:18:17.340 UTC >HINT:  Future log output will appear in directory "pg_log".

$ kubectl exec -it <db-pod-name> -- psql -l
                             List of databases
   Name    |  Owner   | Encoding | Collate | Ctype |   Access privileges   
-----------+----------+----------+---------+-------+-----------------------
 postgres  | postgres | UTF8     | C       | C     | 
 template0 | postgres | UTF8     | C       | C     | =c/postgres          +
           |          |          |         |       | postgres=CTc/postgres
 template1 | postgres | UTF8     | C       | C     | =c/postgres          +
           |          |          |         |       | postgres=CTc/postgres
 your_db   | postgres | UTF8     | C       | C     | 
(4 rows)

MongoDB Node.js DriverでAmazon DocumentDB(TLS有効)に接続するとunable to get local issuer certificate

AWSコンソールで確認したURIを指定してMongoDB Node.js driver( mongodb 3.0.2 )でDocumentDB(TLS有効)に接続しようとしたところ、 MongoNetworkError: unable to get local issuer certificate となってしまいました。

事象

DBの接続情報は作成したクラスタConnectivity & security で確認しました。

f:id:hiroki-sawano:20201025194809p:plain

まずは指示通りCA証明書をダウンロードします。

❯ wget https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem

続けて、 MongoClient.connect で上図に記載のURIをそのまま指定しました。

MongoClient.connect('mongodb://<username>:<password>@....docdb.amazonaws.com:27017/?ssl=true&ssl_ca_certs=rds-combined-ca-bundle.pem&replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false');

すると以下のエラーが発生しました。

Failed Mongo connection: failed to connect to server [...] on first connect [MongoNetworkError: unable to get local issuer certificate]

原因

以下のリンク先によると、 ssltls に、 ssl_ca_certstlsCAFile にしないと駄目だったようです。

https://developer.mongodb.com/community/forums/t/mongonetworkerror-unable-to-get-local-issuer-certificate/3518/5

対策

URIを以下の通り書き換えることで接続に成功しました。

MongoClient.connect('mongodb://<username>:<password>@....docdb.amazonaws.com:27017/?tls=true&tlsCAFile=rds-combined-ca-bundle.pem&replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false');

Amazon DocumentDBでcollStatsは機能するがcollstatsだとCollection not found

事象

今まで自前で立てたMongoDBで問題なく動作していた

pymongo.database.Database.command("collstats", "<collection-name>")

が、Amazon DocumentDBではコレクションが存在するにも関わらず

{ "ok" : 0, "errmsg" : "Collection not found", "code" : 10 }

を返すようになってしまいました。

database – Database level operations — PyMongo 3.9.0 documentation

原因

pymongo 側を疑って無駄に時間を浪費しましたが、 そもそもDocumentDBが collstats だとエラーにはならないものの期待した結果を返さず、 collStats だと正しくコレクションの統計情報を返すようです。
通常のMongoDBだとどちらのコマンドでも動作します。

# mongo --ssl --host <docdb-cluster-endpoint>:27017 --sslCAFile rds-combined-ca-bundle.pem --username <username> --password <password>

rs0:PRIMARY> db.runCommand({"collstats": "users"})
{ "ok" : 0, "errmsg" : "Collection not found", "code" : 10 }
rs0:PRIMARY> db.runCommand({"collStats": "users"})
{
    "ns" : "...",
    "count" : 1,
    "size" : 1490,
    "avgObjSize" : 1490,
    "storageSize" : 16384,
    "capped" : false,
    "nindexes" : 5,
    "totalIndexSize" : 81920,
    "indexSizes" : {
        ...
    },
    "ok" : 1
}

対策

以下のコードに変更することでPython上でも統計情報の取得ができるようになりました。

pymongo.database.Database.command("collStats", "<collection-name>")

DocumentDBはMongo3.6互換ですが細かい差異があるようなので気をつけないとダメですね。