AWS Cloud9代替環境をAWS Service Catalogを用いて構築する方法


## 背景

AWS Cloud9は2024年7月に新規利用受付を終了しました。既存ユーザーは引き続き利用可能ですが、新規アカウントでは利用開始できなくなっています。
> https://aws.amazon.com/jp/blogs/news/how-to-migrate-from-aws-cloud9-to-aws-ide-toolkits-or-aws-cloudshell/

この変更を受け、**AWS Service Catalog を利用した Cloud9 代替環境のプロビジョニング仕組み**を構築しました。私の所属するチームでは、Cloud9は特に性能テストやデータベース操作など、特定の開発・運用タスクで重宝されていたため、同等の機能を持つ代替環境が必要でした。

本記事では、AWS Service Catalogを活用した代替環境の具体的な実装内容ご紹介します。

:::note warn
本記事では AWS Service Catalog の詳細な説明は割愛します。
概要については以下の記事をご参照ください。
https://www.tis.jp/special/platform_knowledge/cloud21/
:::

## 構築概要

 Cloud9の代替環境をAWS Service Catalogを活用して構築しました。具体的には、以下の実装を行いました。

- **Service CatalogでEC2環境をプロビジョニング**  
  EC2インスタンスをCloud9の代替環境として利用できるように、Service Catalogを通じて簡単に環境を作成・削除できる仕組みを構築しました。

- **Service CatalogでStep Functionsワークフローをプロビジョニング**  
  EC2作成時に設定した使用期限を超過した場合に、AMIを取得した上で自動的にインスタンスを削除するワークフローを構築しました。

## なぜこの方法を選択したのか
当初、Cloud9 の代替環境を設計するにあたり、チーム内でユースケースを整理したところ、以下の要件に対応できる環境が必要だと判断しました。

### 必要な要件
|  要件 | 詳細 |
|:-:|:-:|
| ネットワーク疎通確認や権限利用  | ・JMeter用端末としての利用 <br> ・RDSへのSQL実行 <br>・ECRへのイメージプッシュ <br>・ECS Execの実行|
| 社用PCの制約  |  ・ローカルからのSSH接続が制限されている環境での利用  |



また、EC2を複数環境に迅速に展開していくためには以下が必要だと考えました。

1. **自動削除の必要性** ⏱️  
   Cloud9のメリットとして、デフォルトでIDEを終了して30分後自動的に停止するコスト削減機能があります。EC2でも使用期限を設定し、期限を過ぎたリソースを自動的に削除する仕組みが重要でした。

2. **複数環境での利用** 🔄  
   多数のAWS環境があり、どの環境でも利用できる汎用的な仕組みが必要でした。

3. **アプリ担当者による自己完結性** 👨‍💻  
   アプリ担当者が自らプロビジョニング操作を行える仕組みを提供することで、インフラ担当への依存を減らし、コミュニケーションコストを削減できます。

### CloudShell ではなく EC2＋Service Catalog を選んだ理由

CloudShell も候補でしたが、以下の課題により採用を見送りました。

- **IAM 権限管理が複雑**
  - CloudShell の使用を制限する場合、追加のポリシー設定が必要となり運用が複雑化する

- **利用クォータの制約**
  - ストレージ容量などの制限が初期状態では小さく、本格運用を行うにはネックになると判断

- **セッションの自動切断への対応**
  - 長時間の操作や大規模作業時にセッションが切断されやすく、JMeterの実行時などのタイミングで運用上の工夫が求められる

## アーキテクチャ

作成したServiceCatalog製品では以下が実施可能です。
1. EC2の構築
2. StepFunctions(ステートマシン)の構築

### 1. EC2の構築

起動パターンは以下2つを用意しており、柔軟にAMIの設定が可能です。
- デフォルトAMIからEC2を構築
- 利用中の環境で既に作成したAMIからEC2構築用

起動されるインスタンスでは以下を設定しています：
- **インスタンスタイプ**: デフォルトで設定（ユーザー指定可能）
- **VPC、サブネット**: 都度指定
- **セキュリティグループ**: 外部からの通信は全て拒否、インターネットへのHTTP/HTTPS通信は許可
- **IAMRole**: S3特定バケットに対するフルアクセス権限、CloudWatch Logsへの書き込み権限
- **EBS**: 50GBのgp3ボリュームをアタッチ

デフォルトAMIから起動する際は、Userdataで以下をインストール：
- MySQLクライアント
- PostgreSQLクライアント
- Docker
- JMeter
- AWS CLI
- その他必要なツール

### 2. StepFunctionsの構築

1. **期限判定（Lambda処理）**
   - Lambda関数で以下を確認:
     - 3か月以上前にこの仕組みで作成されたAMIがあるか
     - 使用期限を過ぎたEC2インスタンスがあるか
   - どちらも存在しない場合は処理終了
   - 存在する場合はそれぞれの削除処理に進む

2. **3か月以上前のAMIがある場合**
   - DeregisterImage: AMIを登録解除
   - DeleteSnapshot: AMIに関連付けられたスナップショットを削除

3. **期限を過ぎたEC2がある場合**
   - CreateImage: EC2インスタンスからAMIを作成
   - TerminateProvisionedProduct: ServiceCatalogを終了させることでEC2を削除

【参考】StepFunctions定義図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581135/8f876994-807e-4052-97b9-4d57da908ec4.png)


このステートマシンは日次で実行され、各種処理を行っています。

## Service Catalog利用にあたって苦労・意識したこと

### 1. EC2の終了方法

**課題**: 親製品IDを直接取得できないため、判別する仕組みが必要だった。

Service Catalogの製品終了時に利用するAPI「TerminateProvisionedProduct」では、終了対象の製品IDを指定する必要があります。しかし、EC2を作成する製品が親製品から呼び出される今回のケースでは、EC2自体には親製品の情報が記載されていません。

**対応策**:  
関連する情報を調査したところ、親製品を終了するための情報は作成後のCloudFormationスタックのリソース部分に存在することが判明しました。そこで、その情報を基に親製品のIDを特定するロジックをLambda関数で実装しました。

```python
def get_expired_parent_products(child_product_id):
    """子製品IDを基に、親製品を特定する"""
    try:
        # 現在のAWSアカウント番号を取得
        account_id = sts_client.get_caller_identity()['Account']

        # 親製品をフィルタリングして取得
        response = servicecatalog_client.search_provisioned_products(
            Filters={'SearchQuery': ["name:親製品の名前"]}
        )
        parent_products = response.get('ProvisionedProducts', [])

        for parent_product in parent_products:
            parent_product_id = parent_product['Id']

            # 親製品のCloudFormationスタックリソースを確認
            try:
                stack_name = f"SC-{account_id}-{parent_product_id}"
                stack_resources = cloudformation_client.describe_stack_resources(
                    StackName=stack_name
                ).get('StackResources', [])

                for resource in stack_resources:
                    # 子製品IDに一致するリソースを探す
                    if resource.get('PhysicalResourceId') == child_product_id:
                        print(f"親製品のID: {parent_product_id} が見つかりました。")
                        return {
                            "parentProductId": parent_product_id,
                            "parentProductName": parent_product['Name'],
                            "parentProductArn": parent_product['Arn']
                        }
            except cloudformation_client.exceptions.ClientError as e:
                print(f"スタックリソースの取得でエラー: {str(e)}")
                continue

        print("親製品が見つかりませんでした。")
        return None

    except servicecatalog_client.exceptions.ClientError as e:
        print(f"Service Catalogのプロビジョニング情報取得でエラー: {str(e)}")
        raise e
```

### 2. StepFunctions(ステートマシン)の作成方法

複数のEC2が作成されている場合にも対応できるよう、Step Functionsを1つだけ作成し、必要に応じて自動削除を行う仕組みを実装しました。また、AMI作成時に固有の命名規則を定めることにより、AMIが複数存在していても、この仕組みで作成されたものだけを削除できるよう設定しています。

## おわりに

Cloud9の新規利用停止に伴い、AWS Service Catalog を用いた代替環境の構築に取り組みました。Service Catalogを活用することで、特定のユースケースに最適化された柔軟な環境を実現できました。

最も重要だったのは、「何のために必要か？」というユースケースを先に整理することでした。要件が明確であれば、それ以外の要素を切り捨てながら設計をシンプルに進めることができます。

今後も、既存サービスの組み合わせで新たな価値を生み出していきたいと思います。