【Bash】シェルスクリプトで多数のIAMユーザーを一括削除

# はじめに
AWS環境の運用業務では、IAMユーザーの管理を行います。
部署異動や退職などにより、使われていないIAMユーザーが増えることは割とよくあることだと思います。
そのため、定期的なIAMユーザーの棚卸は、セキュリティを維持し、リソースを最適化するために欠かせません。

少数のユーザーであればコンソール上で手動削除することも可能ですが、数十のユーザーがいる場合は手間がかかるだけでなく、誤って重要なユーザーを削除してしまうリスクがあります。

こうした課題を解決する手段として、AWS CLIとシェルスクリプトを使用した複数ユーザーの一括削除が有効です。
シェルスクリプトを使用することで、手動実行に比べて以下のメリットがあります。
　作業時間を短縮
　他の作業と並行して実行
　　シェルスクリプトを流しながら、他の作業を進めることができる
　人為的なミスを防ぐ
　　処理をコードで自動化することで冪等性を担保（100回やっても同じ結果になる）
 
このように、シェルスクリプトの実行により、不要なIAMユーザーを効率的かつ正確に削除できます。

また、AWSには、マネジメントコンソール上でシェルを実行できるCloudShellというサービスがあります。
このサービスを利用することで、追加の環境設定やツールのインストールが不要であり、シェルスクリプトの実行が容易になります。

この記事では、CloudShellとシェルスクリプトを活用して、複数のIAMユーザーを効率的に削除する方法を紹介します。

# 方法
## シェルスクリプトの処理の流れ
シェルスクリプトを使用してIAMユーザーを削除する場合、削除の前に、ユーザーに関連する以下の処理を実施する必要があります。
　ログインプロファイルの削除
　署名用証明書の削除
　Git認証情報の削除
　SSHキーの削除
　ユーザーにアタッチされたマネージドポリシーのデタッチ
　ユーザーにアタッチされたインラインポリシーの削除
　グループからユーザーを除外
　アクセスキーの削除
　MFAデバイスの削除

これらの手順を理解して、シェルスクリプトを適切に作成して実行することで、複数のIAMユーザーを効率的に削除することができます。
この点を踏まえて、シェルスクリプトによる、IAMユーザー一括削除の流れを以下のようにしました。
　1. 削除ユーザー名が記載されたテキストファイルを1行ずつ読み込み
　　1-1. テキストから取得したユーザー名をもとに、IAMユーザーが存在するか確認
　　　1-1-1. IAMユーザーが存在する場合、以下の処理を実行
　　　　1-1-1-1. 署名用証明書の削除
　　　　1-1-1-2. Git認証情報の削除
　　　　1-1-1-3. SSHキーの削除
　　　　1-1-1-4. ユーザーに直接アタッチされているマネージドポリシーをデタッチ
　　　　1-1-1-5. ユーザーに直接アタッチしたインラインポリシーを削除
　　　　1-1-1-6. グループからユーザーを除外
　　　　1-1-1-7. アクセスキー削除
　　　　1-1-1-8. MFAデバイス削除
　　　　1-1-1-9. ログインプロファイル削除
　　　　1-1-1-10. 上記処理を実施後、ユーザーを削除
　　　1-1-2. IAMユーザーが存在しない場合、処理をスキップし、次のユーザーに処理を移す


## 必要なファイルの用意
上記の流れで処理を行うので、以下のファイルが必要になります。
　削除するユーザー名を記載したテキストファイル
　削除処理を実行するシェルスクリプト

### 削除するユーザー名を記載したテキストファイル
今回使用するシェルスクリプトは、削除するIAMユーザー名が記載されたテキストファイルを１行ずつ読み込み、
取得したユーザー名を使用して削除処理を行います。
そのため、まずはこのテキストファイルを用意します。
以下のように、削除するIAMユーザー名が記載されたテキストファイルを用意します。
注意点として、改行コードはLF形式にします。
この形式にしないと、スクリプトでテキストファイルを正しく読み込めず、エラーが起きて処理に失敗します。
```delete_users.txt
test-user1
test-user2
test-user3
```

（補足）LF形式の改行コードは、サクラエディタでテキストを開いたときに、改行部分が下矢印になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2485934/bf408fc8-4fa0-82fe-5258-c10d86162027.png)


### 削除処理を行うシェルスクリプト


シェルスクリプトの内容は以下になります。
```delete_users.sh
#!/bin/bash

deleteuserlist=$1

# 1.テキストファイルから削除ユーザー名を取得
while read -r USERNAME; do
    # 1-1.ユーザーの存在確認
    if aws iam get-user --user-name "$USERNAME" &> /dev/null; then
        # 1-1-1.ユーザーが存在する場合、以下の処理を実施
        echo "User Name: $USERNAME"

        # 1-1-1-1.ユーザーに紐づく署名用証明書を削除
        aws iam list-signing-certificates --user-name "$USERNAME" | jq -r '.Certificates[].CertificateId' | while read -r CERTIFICATE_ID; do
            aws iam delete-signing-certificate --user-name "$USERNAME" --certificate-id "$CERTIFICATE_ID"
            echo "ユーザー $USERNAME の署名用証明書を削除しました。"
        done

        # 1-1-1-2.ユーザーに紐づくGIt認証情報を削除
        aws iam list-service-specific-credentials --user-name "$USERNAME" | jq -r '.ServiceSpecificCredentials[].ServiceSpecificCredentialId' | while read -r GIT_CREDENTIAL_ID; do
            aws iam delete-service-specific-credential --user-name "$USERNAME" --service-specific-credential-id "$GIT_CREDENTIAL_ID"
            echo "ユーザー $USERNAME のGit認証情報を削除しました。"
        done

        # 1-1-1-3.ユーザーに紐づくSSHキーを削除
        aws iam list-ssh-public-keys --user-name "$USERNAME" | jq -r '.SSHPublicKeys[].SSHPublicKeyId' | while read -r SSH_KEY_ID; do
            aws iam delete-ssh-public-key --user-name "$USERNAME" --ssh-public-key-id "$SSH_KEY_ID"
            echo "ユーザー $USERNAME のSSHキーを削除しました。"
        done

        # 1-1-1-4.ユーザーに直接アタッチしたマネージドポリシーをデタッチ
        aws iam list-attached-user-policies --user-name "$USERNAME" | jq -r '.AttachedPolicies[].PolicyArn' | while read -r MANAGED_POLICY_ARN; do
            aws iam detach-user-policy --user-name "$USERNAME" --policy-arn "$MANAGED_POLICY_ARN"
            echo "ユーザー $USERNAME からマネージドポリシーをデタッチしました。"
        done

        # 1-1-1-5.ユーザーに直接アタッチしたインラインポリシーを削除
        aws iam list-user-policies --user-name "$USERNAME" | jq -r '.PolicyNames[]' | while read -r INLINE_POLICY_NAME; do
            aws iam delete-user-policy --user-name "$USERNAME" --policy-name "$INLINE_POLICY_NAME"
            echo "ユーザー $USERNAME からインラインポリシーを削除しました。"
        done


        # 1-1-1-6.グループからユーザーを除外
        aws iam list-groups-for-user --user-name "$USERNAME" | jq -r '.Groups[].GroupName' | while read -r GROUP_NAME; do
            aws iam remove-user-from-group --group-name "$GROUP_NAME" --user-name "$USERNAME"
            echo "グループからユーザー $USERNAME を削除しました。"
        done

        # 1-1-1-7.ユーザーに紐づくアクセスキー削除
        aws iam list-access-keys --user-name "$USERNAME" | jq -r '.AccessKeyMetadata[].AccessKeyId' | while read -r ACCESS_KEY_ID; do
            aws iam delete-access-key --user-name "$USERNAME" --access-key "$ACCESS_KEY_ID"
            echo "ユーザー $USERNAME のアクセスキーを削除しました。"
        done

        # 1-1-1-8.ユーザーに紐づくMFAデバイスの削除
        aws iam list-mfa-devices --user-name "$USERNAME" | jq -r '.MFADevices[].SerialNumber' | while read -r MFA_DEVICE_ARN; do
            aws iam deactivate-mfa-device --user-name "$USERNAME" --serial-number "$MFA_DEVICE_ARN"
            aws iam delete-virtual-mfa-device --serial-number "$MFA_DEVICE_ARN"
            echo "ユーザー $USERNAME のMFAデバイスを削除しました。"
        done

        # 1-1-1-9.ログインプロファイルの削除
        if result=$(aws iam delete-login-profile --user-name="$USERNAME" 2>&1); then
            echo "ユーザー $USERNAME のログインプロファイルを削除しました。"
        else
            # エラーメッセージを取得
            error_message=$(echo "$result" | grep -oP '(?<=An error occurred \().*(?=\))')

            # エラーメッセージが「NoSuchEntity」であればログインプロファイルが存在しない旨のメッセージを出力
            if [[ "$error_message" == "NoSuchEntity" ]]; then
                echo "ユーザー $USERNAME のログインプロファイルが存在しませんでした。ログインプロファイルの削除をスキップします。"
            else
                # その他のエラーの場合はエラーメッセージを表示し、シェルスクリプトの実行を終了
                echo "$error_message"
                echo "ユーザー $USERNAME のログインプロファイル削除で想定外のエラーが発生しました。処理を終了します。"
                exit 1
            fi
        fi

        # 1-1-1-10.ユーザーの削除
        aws iam delete-user --user-name "$USERNAME"
        echo "ユーザー $USERNAME を削除しました。"
    else
        # 1-1-2.ユーザーが存在しない場合は処理をスキップ
        echo "ユーザー $USERNAME は見つかりませんでした。削除スキップします。"
    fi
done < ./"$deleteuserlist".txt

echo "すべてのユーザーを削除しました。"
```

1-1-1-1 ~ 1-1-1-8の処理では、listコマンドとjqコマンドを組み合わせて、リソースを取得し、リソースが存在する場合のみ削除等の処理を行います。
IAMユーザーによっては、リソースが存在するものと存在しないものがあるため、リソースの存在確認を行った後、各リソースに応じた処理を行います。

#### 1-1-1-9の処理について
1-1-1-9（ログインプロファイルの削除）は他とは違い、listコマンドが存在しないので、これだけは他リソースと異なるロジックで削除処理を行います。

まず、aws iam delete-login-profileコマンドを実行し、ログインプロファイルの削除を実行し、このコマンドの成功・失敗で条件分岐を行います。
```sample.sh
# ログインプロファイルの削除
if result=$(aws iam delete-login-profile --user-name="$USERNAME" 2>&1); then
    echo "ユーザー $USERNAME のログインプロファイルを削除しました。"
else
    # ここにエラー発生時の処理を記載
fi
```

条件分岐する理由として、他リソースと同様、削除するユーザーにログインプロファイルが存在しない（つまりマネジメントコンソールにログインできない/しない）場合や、それ以外の要因でコマンドが失敗する可能性があるためです。

コマンド失敗時の処理では、ログインプロファイルが存在しない場合は削除処理をスキップし、それ以外の要因でエラーが起きた場合はスクリプトの処理を終了します。

これらの処理の流れをもう少し具体的に書くと以下になります。
　エラーメッセージを取得
　grepコマンドで"An error occurred ()"という文字列の括弧の間にあるエラーコードを抽出
　抽出したエラーコードが「NoSuchEntity」か確認（このコードはプロファイルが存在しないことを意味するエラーコードです）
　　エラーコードが「NoSuchEntity」である場合
  　　　ログインプロファイルが存在しないことを意味するので、ログインプロファイルの削除処理をスキップ
　　エラーコードが「NoSuchEntity」でない場合（=他のエラーが発生した場合）
　　　エラーメッセージを表示し、スクリプトの処理を終了

```sample.sh
# ログインプロファイルの削除
if result=$(aws iam delete-login-profile --user-name="$USERNAME" 2>&1); then
    echo "ユーザー $USERNAME のログインプロファイルを削除しました。"
else
    # エラーメッセージを取得
    error_message=$(echo "$result" | grep -oP '(?<=An error occurred \().*(?=\))')

    # エラーメッセージが「NoSuchEntity」であればログインプロファイルが存在しない旨のメッセージを出力
    if [[ "$error_message" == "NoSuchEntity" ]]; then
        echo "ユーザー $USERNAME のログインプロファイルが存在しませんでした。ログインプロファイルの削除をスキップします。"
    else
        # その他のエラーの場合はエラーメッセージを表示し、シェルスクリプトの実行を終了
        echo "$error_message"
        echo "ユーザー $USERNAME のログインプロファイル削除で想定外のエラーが発生しました。処理を終了します。"
        exit 1
    fi
fi
```


## シェルスクリプトの実行
必要なファイルの用意ができたら、CloudShellでシェルスクリプトを実行します。

マネジメントコンソールにログインし、シェルスクリプトを開きます。
（下図赤枠ボタン）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2485934/deb6cd46-f11f-6380-f26a-2640d54cecb8.png)


シェルスクリプトを開いたら、アクション > ファイルのアップロードをクリックし、以下のファイルをアップロードします。
　ユーザーリストのテキストファイル
　IAMユーザー削除シェルスクリプト

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2485934/e6fda84c-e6fa-27b8-1461-58111df1c028.png)


アップロードしたシェルスクリプトファイルに、実行権限を付与します。
```
chmod +x delete_users.sh
```

実行権限が付与されていることを確認します。
（ユーザー権限に「x」が表示されている or ファイル名が緑色で表示されていればOK）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2485934/5b3eb1ee-6101-8481-91b9-f9b66edaec43.png)


以下のコマンドを実行してシェルスクリプトの処理を実行します。
テキストファイル名は拡張子を除いた名前を指定します。
　./<シェルスクリプトのファイル名> <削除ユーザーリストのテキストファイル名>

・コマンド例
```
./delete_users.sh delete_users
```


今回のシェルスクリプトでは以下のようなメッセージが表示されます。
最後に「すべてのユーザーを削除しました。」と表示されれば処理終了です。

```
User Name: test-user1
ユーザー test-user1 の署名用証明書を削除しました。
ユーザー test-user1 のGit認証情報を削除しました。
ユーザー test-user1 のSSHキーを削除しました。
ユーザー test-user1 のアクセスキーを削除しました。
ユーザー test-user1 のログインプロファイルを削除しました。
ユーザー test-user1 を削除しました。
User Name: test-user2
ユーザー test-user2 の署名用証明書を削除しました。
ユーザー test-user2 の署名用証明書を削除しました。
ユーザー test-user2 のGit認証情報を削除しました。
ユーザー test-user2 のGit認証情報を削除しました。
ユーザー test-user2 からマネージドポリシーをデタッチしました。
ユーザー test-user2 からマネージドポリシーをデタッチしました。
ユーザー test-user2 からインラインポリシーを削除しました。
ユーザー test-user2 からインラインポリシーを削除しました。
グループからユーザー test-user2 を削除しました。
グループからユーザー test-user2 を削除しました。
ユーザー test-user2 のアクセスキーを削除しました。
ユーザー test-user2 のアクセスキーを削除しました。
ユーザー test-user2 のMFAデバイスを削除しました。
ユーザー test-user2 のログインプロファイルを削除しました。
ユーザー test-user2 を削除しました。
User Name: test-user3
ユーザー test-user3 のログインプロファイルが存在しませんでした。ログインプロファイルの削除をスキップします。
ユーザー test-user3 を削除しました。
すべてのユーザーを削除しました。
```

コンソール画面上に、削除したユーザーが表示されていなければOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2485934/5414ce98-9975-739f-1ee6-9cb65dd0939a.png)


# 参考資料

ログインプロファイル削除失敗時のエラーコード取得では、正規表現でのgrepコマンドの先読み・後読みを使用しています。
これらの仕組みについては、以下の記事が参考になります。

https://zenn.dev/usamik26/articles/regex-lookahead

AWS CLIによるIAMユーザー削除の流れは以下記事をご参考ください。
記事には、各コマンドのリファレンスのリンクもあるので、コマンドの仕様も確認できます。

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_manage.html#id_users_deleting_cli