ROSA で AWS EFS CSI Driver Operator を使う


# 0.前提

ROSA HCP クラスターは、[こちら](https://qiita.com/Yuhkih/items/38a8ea5c2e1ae470ebf4)の Qiita記事にある方法で作成済みの環境を使用します。

作成された ROSA HCP クラスターを使って、Red Hat の Black Belt チームで作成された[こちらの手順](https://cloud.redhat.com/experts/rosa/aws-efs/)と、KB でリリースされている [こちらの情報](https://access.redhat.com/articles/6966373?extIdCarryOver=true&sc_cid=7015Y0000048WGaQAM)を参照しながら作成しました。



# 1.環境変数の準備

ROSA Cluster に `oc login` 後、


```
oc login <API Server Endpoint> --username cluster-admin --password <password>
```
と
```
rosa login
```
は済ましておいてください。

以下を実行してセットアップに必要な環境変数を準備します。

 `AWS_REGION` をセットします。`ap-northeast-1` (東京リージョン) にしてますが、環境に合わせて変更してください。

```
export AWS_REGION="ap-northeast-1"
```

`ClUSTER_NAME` は rosa cluster が環境内に一つの時だけ正しく値が取得できるようになっています。

```
export CLUSTER_NAME=$(rosa list clusters -o json | jq '.[0].name' | sed 's/"//g') 
export OIDC_PROVIDER=$(oc get authentication.config.openshift.io cluster -o json \
| jq -r .spec.serviceAccountIssuer| sed -e "s/^https:\/\///")
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export SCRATCH_DIR=/tmp/scratch
export AWS_PAGER=""
mkdir -p $SCRATCH_DIR
```


# 2. IAM Policy の準備

(1) `IAM Policy` の元ネタになる `json`ファイルを作成します。

```
cat << EOF > $SCRATCH_DIR/efs-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "elasticfilesystem:DescribeAccessPoints",
        "elasticfilesystem:DescribeFileSystems",
        "elasticfilesystem:DescribeMountTargets",
        "elasticfilesystem:TagResource",
        "ec2:DescribeAvailabilityZones"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "elasticfilesystem:CreateAccessPoint"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:RequestTag/efs.csi.aws.com/cluster": "true"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "elasticfilesystem:DeleteAccessPoint",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/efs.csi.aws.com/cluster": "true"
        }
      }
    }
  ]
}
EOF
```

 (2) `json` ファイルから `IAM Policy` を作成します。

```
POLICY=$(aws iam create-policy --policy-name "${CLUSTER_NAME}-rosa-efs-csi" \
   --policy-document file://$SCRATCH_DIR/efs-policy.json \
   --query 'Policy.Arn' --output text) || \
   POLICY=$(aws iam list-policies \
   --query 'Policies[?PolicyName==`rosa-efs-csi`].Arn' \
   --output text)
echo $POLICY
```

(3) Trust Policy を作成します。
この `Policy` は、この後作成する  `IAM Role` の中で使用します。

```
cat <<EOF > $SCRATCH_DIR/TrustPolicy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_PROVIDER}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "${OIDC_PROVIDER}:sub": [
            "system:serviceaccount:openshift-cluster-csi-drivers:aws-efs-csi-driver-operator",
            "system:serviceaccount:openshift-cluster-csi-drivers:aws-efs-csi-driver-controller-sa"
          ]
        }
      }
    }
  ]
}
EOF
```

(4) `EFS CSI Driver Operator` に使う `IAM Role` を (3)で作成した `Trust Policy` を使って作成します。

```
ROLE=$(aws iam create-role \
  --role-name "${CLUSTER_NAME}-aws-efs-csi-operator" \
  --assume-role-policy-document file://$SCRATCH_DIR/TrustPolicy.json \
  --query "Role.Arn" --output text)
echo $ROLE
```

(5) 上記で作成した `IAM Role` に `IAM Policy` をアタッチします。

```
aws iam attach-role-policy \
   --role-name "${CLUSTER_NAME}-aws-efs-csi-operator" \
   --policy-arn $POLICY
```

# 3. AWS EFS CSI Driver Operator  の インストール

`EFS CSI Driver Operator` をインストールします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/af61dc34-9c10-e9a3-b2f4-0739c64ddc85.png)

`Operator Hub` の GUIからインストールしても良いのですが、この手順では `CLI` で `EFS CSI Driver Operator` をインストールしてしまいます。(GUIインストールでも一部、CLI作業が発生します)

(1) `AWS EFS CSI Driver Operator` で使用する `IAM Role` の `ARN` を入れた `Secret`を作成します。

```
cat << EOF | oc apply -f -
apiVersion: v1
kind: Secret
metadata:
 name: aws-efs-cloud-credentials
 namespace: openshift-cluster-csi-drivers
stringData:
  credentials: |-
    [default]
    role_arn = $ROLE
    web_identity_token_file = /var/run/secrets/openshift/serviceaccount/token
EOF
```



(2) `AWS EFS CSI Driver Operator` をインストールします。

```
cat <<EOF | oc create -f -
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
  generateName: openshift-cluster-csi-drivers-
  namespace: openshift-cluster-csi-drivers
---
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  labels:
    operators.coreos.com/aws-efs-csi-driver-operator.openshift-cluster-csi-drivers: ""
  name: aws-efs-csi-driver-operator
  namespace: openshift-cluster-csi-drivers
spec:
  channel: stable
  installPlanApproval: Automatic
  name: aws-efs-csi-driver-operator
  source: redhat-operators
  sourceNamespace: openshift-marketplace
EOF
```

(3) `Operator` のインストールが完了するまで待ちます。

```
watch oc get deployment aws-efs-csi-driver-operator -n openshift-cluster-csi-drivers
```

以下でも確認できます。

```
oc get pods -n openshift-cluster-csi-drivers | grep efs
```

(4) `ClusterCSIDriver` CRD を作成します。

これで各 `Node` に `CSI Driver` の `Pod` が作成されます。

```
cat <<EOF | oc apply -f -
apiVersion: operator.openshift.io/v1
kind: ClusterCSIDriver
metadata:
    name: efs.csi.aws.com
spec:
  managementState: Managed
EOF
```

(5) `CSI Driver` のインストールの完了を待ちます。

```
watch oc get daemonset aws-efs-csi-driver-node -n openshift-cluster-csi-drivers
```

# 4. EFS の作成の準備

(1) 以下のコマンドを実行して環境変数を準備します。

```
NODE=$(oc get nodes --selector=node-role.kubernetes.io/worker \
  -o jsonpath='{.items[0].metadata.name}')
VPC=$(aws ec2 describe-instances \
  --filters "Name=private-dns-name,Values=$NODE" \
  --query 'Reservations[*].Instances[*].{VpcId:VpcId}' \
  --region $AWS_REGION \
  | jq -r '.[0][0].VpcId')
CIDR=$(aws ec2 describe-vpcs \
  --filters "Name=vpc-id,Values=$VPC" \
  --query 'Vpcs[*].CidrBlock' \
  --region $AWS_REGION \
  | jq -r '.[0]')
SG=$(aws ec2 describe-instances --filters \
  "Name=private-dns-name,Values=$NODE" \
  --query 'Reservations[*].Instances[*].{SecurityGroups:SecurityGroups}' \
  --region $AWS_REGION \
  | jq -r '.[0][0].SecurityGroups[0].GroupId')
echo "CIDR - $CIDR,  SG - $SG"
```

(2) `EFS` volume をマウントできるように、`Security Group` を Update します。

この `Security Group` は `ROSA` の `Worker Node` 用の `Security Group` で、`ROSA` の `VPC` に割り当てたアドレスレンジ(`$CIDR`) から、`EFS` の `TCP Port` `2049` へのアクセスを可能にするものです。

```
aws ec2 authorize-security-group-ingress \
 --group-id $SG \
 --protocol tcp \
 --port 2049 \
 --cidr $CIDR | jq .
```

# 5. EFS を作成する。

`EFS` には、`Regional` と `Single AZ` の2種類の `EFS` があります。

`Regional` は、`AZ`障害に対応できます。一方、`Single AZ`は、`AZ`障害に対応できませんが、コストを安く済ます事ができます。テスト環境やデータアナリシスなどで一時的なデータを吐き出すのはこちらが良いでしょう。

この手順では、`EFS`を使用する`StorageClass` は一つしか作らないので、どちらか一つだけを選択して作成してください。

この手順では Cluster がインストールされている Private Subnet を


## A) Regional の EFS を作成する場合

(1) `EFS` を作成します。
```
EFS=$(aws efs create-file-system --creation-token efs-token-1 \
   --region ${AWS_REGION} \
   --encrypted | jq -r '.FileSystemId')
echo $EFS
```

AWS Console の EFS の項目には以下のような FileSystem が作成されます。(`Availability Zone`が`Regional` になっている事に注目します) 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/f67ccb8f-1426-248a-0deb-f12a13a63eb7.png)

(2) `EFS` の [Mount Target](https://docs.aws.amazon.com/ja_jp/efs/latest/ug/accessing-fs.html)を作成します。`Mount Target Id` を取得して、`$MOUNT_TARGET` に格納します。

```
for  SUBNET in $(rosa list machinepools -c $CLUSTER_NAME  -o json | jq  -r '.[].subnet'); do \
    MOUNT_TARGET=$(aws efs create-mount-target --file-system-id $EFS \
       --subnet-id $SUBNET --security-groups $SG \
       --region $AWS_REGION \
       | jq -r '.MountTargetId'); \
    echo $MOUNT_TARGET; \
 done
 ```

 この例では、各 Zone 毎に一つの `Mount Target` を作成しています。[こちら](https://docs.aws.amazon.com/ja_jp/efs/latest/ug/accessing-fs.html)によると、各`AZ`毎に作成できる `Mount Target` は最大一つです）


 ## B) Single AZ の EFS を作成する場合

(1) `EFS` をマウントするための AZ を一つ選びます。

ここでは、サブネットを適当に一つ取り出して、そのサブネットの `AZ` を `EFS` に接続する `AZ` とする事にします。

```
SUBNET=$(rosa list machinepools -c $CLUSTER_NAME  -o json | jq  -r '.[0].subnet')
AWS_ZONE=$(aws ec2 describe-subnets --filters Name=subnet-id,Values=$SUBNET \
  --region $AWS_REGION | jq -r '.Subnets[0].AvailabilityZone')
```
(2) `EFS` を作成します。
AWS Console の `EFS` の項目には以下のような FileSystem が作成されます。(`Availability Zone`がこの例では`ap-northeast-1c` で、一つの `AZ` だけになっている事に注目します) 
```
EFS=$(aws efs create-file-system --creation-token efs-token-1 \
   --availability-zone-name $AWS_ZONE \
   --region $AWS_REGION \
   --encrypted | jq -r '.FileSystemId')
echo $EFS
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/afbb8376-a0d9-2445-ec49-718b1368dbd6.png)

(3) `EFS` の [Mount Target](https://docs.aws.amazon.com/ja_jp/efs/latest/ug/accessing-fs.html)を作成します。 `Mount Target Id` を取得して、`$MOUNT_TARGET` に格納します。

```
MOUNT_TARGET=$(aws efs create-mount-target --file-system-id $EFS \
  --subnet-id $SUBNET --security-groups $SG \
  --region $AWS_REGION \
  | jq -r '.MountTargetId')
echo $MOUNT_TARGET
```

# 6. EFS 用の Storage Class の作成

`EFS` Volume 用の `StorageClass` を作成します。
この `StorageClass` を使って `EFS` Volume が作成できるようになります。

```
cat <<EOF | oc apply -f -
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: efs-sc
provisioner: efs.csi.aws.com
parameters:
  provisioningMode: efs-ap
  fileSystemId: $EFS
  directoryPerms: "700"
  gidRangeStart: "1000"
  gidRangeEnd: "2000"
  basePath: "/dynamic_provisioning"
EOF
```

# 7. 実際に Pod をデプロイして EFS Volume の動作を確認

(1) 実験用の `namespace` を作成します。

```
oc new-project efs-demo
```

(2) `PVC` を作成します。
中では、先ほど作成した `EFS` Volume 用の `StorageClass` を使用しています。

```
cat <<EOF | oc apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-efs-volume
spec:
  storageClassName: efs-sc
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
EOF
```

(3) PVCが作成されるのを待ちます。２分程度で、 `RWX` の PVC のデプロイが完了するはずです。

```
watch oc get pvc
```

(4) `EFS` Volume にメッセージを書き込むテスト用の `Pod` を作成します。

5秒起きに、作成した `EFS` Volume 上のファイルに `hello efs` と書き出します。

```
cat <<EOF | oc apply -f -
apiVersion: v1
kind: Pod
metadata:
 name: test-efs
spec:
 volumes:
   - name: efs-storage-vol
     persistentVolumeClaim:
       claimName: pvc-efs-volume
 containers:
   - name: test-efs
     image: centos:latest
     command: [ "/bin/bash", "-c", "--" ]
     args: [ "while true; do echo 'hello efs' | tee -a /mnt/efs-data/verify-efs && sleep 5; done;" ]
     volumeMounts:
       - mountPath: "/mnt/efs-data"
         name: efs-storage-vol
EOF
```

(5) Test 用の `Pod` がデプロイされるのを待ちます。

```
watch oc get pod test-efs
```

(6) 次に、`EFS` Volume 上のファイルを読み込む別のPodを作成します。

先ほど作成した `EFS` Volume 上のファイルを `tail` します。

```
cat <<EOF | oc apply -f -
apiVersion: v1
kind: Pod
metadata:
 name: test-efs-read
spec:
 volumes:
   - name: efs-storage-vol
     persistentVolumeClaim:
       claimName: pvc-efs-volume
 containers:
   - name: test-efs-read
     image: centos:latest
     command: [ "/bin/bash", "-c", "--" ]
     args: [ "tail -f /mnt/efs-data/verify-efs" ]
     volumeMounts:
       - mountPath: "/mnt/efs-data"
         name: efs-storage-vol
EOF
```
(7) ファイルを`tail` している `Pod` のログを確認します。

`tail` は標準出力に吐かれているので、`oc logs`で `tail` の結果が確認できます。

```
oc logs test-efs-read
```

以下のような出力が確認できるはずです。

```
hello efs
hello efs
hello efs
hello efs
hello efs
hello efs
hello efs
hello efs
hello efs
hello efs
```

# 8. 環境の削除

(1) デプロイした Test 用の Podを削除します。

```
oc delete pod -n efs-demo test-efs test-efs-read
```

(2) デプロイした EFS Volume を削除します。

```
oc delete -n efs-demo pvc pvc-efs-volume
```

(3) 作成した Namespace を削除します。

```
oc delete project efs-demo
```

(4) 作成した Storage Class を削除します。

```
oc delete storageclass efs-sc
```

(5) EFS File System を削除します。

Mount Target を削除します。
```
for MOUNT_TARGET in $(aws efs describe-mount-targets --file-system-id $EFS \
	--region $AWS_REGION \
	 | jq -r '.MountTargets[].MountTargetId'); do \
	echo $MOUNT_TARGET; \
	aws efs delete-mount-target --mount-target-id $MOUNT_TARGET --region $AWS_REGION; \
done
```
FileSystem を削除します。(Mount Target を削除してから数十秒時間を置く必要があります)
```
aws efs delete-file-system --file-system-id $EFS --region $AWS_REGION
```

(6) IAM Policy と IAM Role をデタッチします。
アタッチしたままだと削除できないので、デタッチします。
```
aws iam detach-role-policy \
   --role-name "${CLUSTER_NAME}-aws-efs-csi-operator" \
   --policy-arn $POLICY
```

(7) 作成したIAM Role を削除します。

```
aws iam delete-role --role-name \
   ${CLUSTER_NAME}-aws-efs-csi-operator
```

(8) 作成した IAM Policy を削除します。
```
aws iam delete-policy --policy-arn \
   $POLICY
```

# 9.[おまけ] NodeからEFSへのアクセス経路を少し深掘り

Regional の EFS を使用した場合に、各 Node から EFS がどのように見えるのか確認してみます。

各AZ の Compute Node 上から EFS の DNS 名を解決するとどのような IPが返ってくるか確認してみます。

環境の EFS の DNS 名は、AWS Console から確認すると`fs-01e64ba4bc876e58c.efs.ap-northeast-1.amazonaws.com`となっていました。これを各 Node 上で名前解決してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/e90def78-3eb2-0132-261a-2b635d490460.png)

尚、この手順の5番で作成した各 AZ (の Private Subnet) の `Mount Target` は以下の通りで、各AZ内の Private IP が割り振られています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/3b44faaa-7e58-8c22-b038-8afc8006834c.png)、

各 AZに配置された、Node で debug pod を起動して EFS の DNS名`fs-01e64ba4bc876e58c.efs.ap-northeast-1.amazonaws.com`を解決してみます。

```
$ oc get nodes
NAME                                            STATUS   ROLES    AGE   VERSION
ip-10-0-1-244.ap-northeast-1.compute.internal   Ready    worker   13m   v1.29.6+aba1e8d
ip-10-0-2-214.ap-northeast-1.compute.internal   Ready    worker   23m   v1.29.6+aba1e8d
ip-10-0-3-103.ap-northeast-1.compute.internal   Ready    worker   22m   v1.29.6+aba1e8d
$

$ oc debug node/ip-10-0-2-214.ap-northeast-1.compute.internal
Temporary namespace openshift-debug-47hcg is created for debugging node...
Starting pod/ip-10-0-2-214ap-northeast-1computeinternal-debug-ddjdm ...
To use host binaries, run `chroot /host`
Pod IP: 10.0.2.214
If you don't see a command prompt, try pressing enter.
sh-5.1# dig +short fs-01e64ba4bc876e58c.efs.ap-northeast-1.amazonaws.com
10.0.2.133
sh-5.1#

$ oc debug node/ip-10-0-1-244.ap-northeast-1.compute.internal
Temporary namespace openshift-debug-94mp9 is created for debugging node...
Starting pod/ip-10-0-1-244ap-northeast-1computeinternal-debug-r5gft ...
To use host binaries, run `chroot /host`
Pod IP: 10.0.1.244
If you don't see a command prompt, try pressing enter.
sh-5.1# dig +short fs-01e64ba4bc876e58c.efs.ap-northeast-1.amazonaws.com
10.0.1.154
sh-5.1# 

$ oc debug node/ip-10-0-3-103.ap-northeast-1.compute.internal
Temporary namespace openshift-debug-mnzsh is created for debugging node...
Starting pod/ip-10-0-3-103ap-northeast-1computeinternal-debug-l4n7p ...
To use host binaries, run `chroot /host`                
If you don't see a command prompt, try pressing enter.
sh-5.1# dig +short fs-01e64ba4bc876e58c.efs.ap-northeast-1.amazonaws.com
10.0.3.72
sh-5.1# 
```

同じDNS名が、それぞれのAZにある Mount Target のIP に解決されていました。

各 Nodeからの EFS へのアクセスは、各 Node が存在する AZ 内の Mount Target (ENI)のIP経由でアクセスが行われるようです。

環境の構成を図にまとめると以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/d1089c23-7b6b-155e-a148-08265af59035.png)