Visual Studio CodeでRDS停止のLambda関数作ってみる

Amazon RDS, Amazon Web Services, AWS Lambda, PowerShell, Python, Visual Studio Code, プログラミング, 開発環境・ツール

こんにちは、プラットフォーム技術部の佐藤です。

以前の記事で「AWS CLIでRDSをこまめに止めてコスト削減」という記事を掲載しました。私のプロジェクトでも実践したいと思ったのですが、悲しいことにAWS CLIを定期的に実行できるような常時起動サーバーがありません。

そんなときに、Lambda と CloudWatch を使用してスケジュールされた間隔でインスタンスを起動および停止するという記事を見かけました。これと同じ方法でRDSの停止もおこなえるのではないかと思い、少々乱暴ではありますが「動いているRDSインスタンス一覧を取得して全て止める。」というLambda関数を作ってみようと思いたちました。

ただ、Lambdaで動かすコードを直接書いて動作確認するのはなかなか大変です。いったんローカル環境で開発してAWS環境にアップロードするという方法を試してみました。

実はローカルで開発してみようと思い立ったのはaws-sam-localやpython-lambda-localを知ったことがきっかけだったのですが、今回やろうとすることはAWS CLIで実行する内容を単純にBoto3を使ったPythonコードで作り替えるだけなので、そこまで大掛かりなことは必要ありませんでした。

ですので、aws-sam-localを使ってバリバリ開発できるような方には、物足りない内容かと思います。

AWS CLI代わりの処理をLambda関数で作ってみたいなと思いつつ、普段はIDEなどあまり馴染みのない方へのご参考になればと思います。

開発環境の準備

予め準備済みのもの

ローカルの開発環境としてはWindows 10 Proで、IDEはVisual Studio Codeを使いました。Lambda実行環境にはPython 3.7を採用します。なお、Python実行環境構築にはPipenvを使います。 以下は既にインストール済みでPATHが通っているものとして始めます。

  • Python 3.7.2 X86_64
  • Visual Studio Code

開発用のフォルダーはC:\myProjectを作成して始めます。

PS C:\myProject> code --version
1.32.3
a3db5be9b5c6ba46bb7555ec5d60178ecc2eaae4
x64
PS C:\myProject> python -V
Python 3.7.2
PS C:\myProject>

Visual Studio CodeにPython拡張をインストール

Python – Visual Studio MarketplaceからPython拡張をインストールします。以下の例では拡張をダウンロードし、コマンドラインでインストールしていますが、Visual Studio Codeの画面操作でもインストール可能です。

PS C:\myProject> code --install-extension C:\Users\ksato\Downloads\ms-python.python-2019.2.5558.vsix
(node:2072) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues.
Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
Extension 'ms-python.python-2019.2.5558.vsix' was successfully installed!
PS C:\myProject>

Pipenvモジュールのインストール

pipコマンドでPipenvモジュールをインストールします。

PS C:\myProject> pip install --user pipenv
Collecting pipenv
  Downloading https://files.pythonhosted.org/packages/13/b4/3ffa55f77161cff9a5220f162670f7c5eb00df52e00939e203f601b0f579/pipenv-2018.11.26-py3-none-any.whl (5.2MB)
    100% |████████████████████████████████| 5.2MB 2.0MB/s
Collecting certifi (from pipenv)
  Downloading https://files.pythonhosted.org/packages/60/75/f692a584e85b7eaba0e03827b3d51f45f571c2e793dd731e598828d380aa/certifi-2019.3.9-py2.py3-none-any.whl (158kB)
    100% |████████████████████████████████| 163kB 3.9MB/s
Collecting virtualenv-clone>=0.2.5 (from pipenv)
  Downloading https://files.pythonhosted.org/packages/e3/d9/d9c56deb483c4d3289a00b12046e41428be64e8236fa210111a1f57cc42d/virtualenv_clone-0.5.1-py2.py3-none-any.whl
Requirement already satisfied: pip>=9.0.1 in c:\program files\python37\lib\site-packages (from pipenv) (18.1)
Collecting virtualenv (from pipenv)
  Downloading https://files.pythonhosted.org/packages/33/5d/314c760d4204f64e4a968275182b7751bd5c3249094757b39ba987dcfb5a/virtualenv-16.4.3-py2.py3-none-any.whl (2.0MB)
    100% |████████████████████████████████| 2.0MB 3.1MB/s
Requirement already satisfied: setuptools>=36.2.1 in c:\program files\python37\lib\site-packages (from pipenv) (40.6.2)
Installing collected packages: certifi, virtualenv-clone, virtualenv, pipenv
Successfully installed certifi-2019.3.9 pipenv-2018.11.26 virtualenv-16.4.3 virtualenv-clone-0.5.1
You are using pip version 18.1, however version 19.0.3 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.
PS C:\myProject>

Pipenv用環境変数設定

この後作成するPipenv環境は、プロジェクトフォルダーの下に作成したいので、環境変数にPIPENV_VENV_IN_PROJECT=trueを設定しておきます。

Pipenv環境作成

プロジェクトフォルダーにPipenv環境を作成、Python用SDKであるBoto3モジュールをインストールします。ちなみにbotoとは「アマゾンカワイルカ」の英名。洒落たネーミングですよね。

PS C:\myProject> pipenv install boto3
Creating a virtualenv for this project…
Pipfile: C:\myProject\Pipfile
Using c:\program files\python37\python.exe (3.7.2) to create virtualenv…
[ ===] Creating virtual environment...Already using interpreter c:\program files\python37\python.exe
Using base prefix 'c:\\program files\\python37'
New python executable in C:\myProject\.venv\Scripts\python.exe
Installing setuptools, pip, wheel...
done.

Successfully created virtual environment!
Virtualenv location: C:\myProject\.venv
Creating a Pipfile for this project…
Installing boto3…
Adding boto3 to Pipfile's [packages]…
Installation Succeeded
Pipfile.lock not found, creating…
Locking [dev-packages] dependencies…
Locking [packages] dependencies…
Success!
Updated Pipfile.lock (8382f5)!
Installing dependencies from Pipfile.lock (8382f5)…
  ================================ 8/8 - 00:00:08
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
PS C:\myProject>

Visual Studio Codeの起動と環境設定

プロジェクトフォルダーでVisual Studio Codeを起動します。

PS C:\myProject> code .

起動すると左ペインにmyProjectが表示されますが、まだPipenv関連のファイルだけが存在している状態になっています。プロジェクトフォルダーの下にLambda関数用のstopDBInstancesフォルダーを作って、更にfunction.pyを新規作成します。

Lambda関数用フォルダーとファイルを新規作成

function.pyを作成するとPythonと認識し「pylintがない」というエラーが表示されます。ここでは追加設定不要なようにpylintを素直にインストールしますが、お好みのLinterをインストールしてください。

Linter未インストールの警告

また、これから作成するコードをローカル実行するにはアクセスキーの設定が必要になりますが、Lambda関数内には記載したくないので環境変数で設定します。Visual Studio Codeでは.envファイルを作成するとそのワークスペースでの環境変数を設定することができます。伏字にしますがこんな感じで(Administrator権限です)。

AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX
AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

デバッグしつつ製造開始

まずはDBクラスターの一覧を取得

Boto3のドキュメントでRDSのAPIを眺めてみると、describe_db_instances()でインスタンスの一覧を取得できそうです。インスタンス停止はstop_db_instances()、クラスター停止はstop_db_clusters()といったところでしょうか。

まずは動いているDBクラスターの一覧を取得してみようと以下のコードを用意しました。最後の2行はLambdaとしては不要ですが、ローカルデバッグしやすいように記述しています。

import boto3

region = 'ap-northeast-1'
rds = boto3.client('rds', region_name=region)

def lambda_handler(event, context):
    dbs = rds.describe_db_instances()
    dbclusters = []
    for db in dbs['DBInstances']:
        if db['DBInstanceStatus'] == 'available':
            dbclusters.append(db['DBClusterIdentifier'])

if __name__ == '__main__':
    lambda_handler(None, None)

では早速デバッグ実行してみましょう。

経過をみたいのでforループにブレークポイントを設定しています。メニューの[Debug]->[Start Debugging]から開始します。ブレークポイントで停止したら確認しつつF10キーで先に進めます。「順調にdbclustersに追加されていっているなぁ」と眺めていましたが、あらまぁException発生です。

DBClusterIdentifierキーでのException

DBClusterIdentifierというキーでエラーですと出ていますので、左ペインのVariablesでdb構造体を確認してみます。

DBClusterIdentifierキー値の確認

db_describe_instances()ではRDSやAurora関係なく一覧を取得してくるのですが、RDSインスタンスにはDBClusterIdentifier値が存在しないのでエラーになっているようです。

RDSインスタンスはクラスターとは別の方法で止める必要があるので別のリストに積んでいくことにします。DBClusterIdentifier値が取得できない場合はインスタンスと判断して別リストに保存していくようにしてみました。

import boto3

region = 'ap-northeast-1'
rds = boto3.client('rds', region_name=region)

def lambda_handler(event, context):
    dbs = rds.describe_db_instances()
    dbclusters = []
    dbinstances = []
    for db in dbs['DBInstances']:
        if db['DBInstanceStatus'] == 'available':
            try:
                dbclusters.append(db['DBClusterIdentifier'])
            except:
                dbinstances.append(db['DBInstanceIdentifier'])

if __name__ == '__main__':
    lambda_handler(None, None)

再びデバッグ実行してみると、これでavailableなステータスになっているDBクラスターとDBインスタンスの一覧が取得できていることがわかります。が、listだと既出のIDが出現してもそのまま追加されて格好悪いことがわかりました。

DBクラスターとインスタンスの一覧

なので、重複を排除してくれるsetに変更します。メソッドもappendからaddに変わってこんなコードになります。

import boto3

region = 'ap-northeast-1'
rds = boto3.client('rds', region_name=region)

def lambda_handler(event, context):
    dbs = rds.describe_db_instances()
    dbclusters = set([])
    dbinstances = set([])
    for db in dbs['DBInstances']:
        if db['DBInstanceStatus'] == 'available':
            try:
                dbclusters.add(db['DBClusterIdentifier'])
            except:
                dbinstances.add(db['DBInstanceIdentifier'])

if __name__ == '__main__':
    lambda_handler(None, None)

デバッグ実行してVariablesを確認してみると値が重複せず格納できていることが確認できました。

DBクラスターとインスタンスの一覧(重複排除)

クラスター、インスタンスの停止処理を追加

一覧は取得できたので、クラスターはstop_db_cluster()で、インスタンスはstop_db_instance()でそれぞれ停止する処理を追加します。

import boto3

region = 'ap-northeast-1'
rds = boto3.client('rds', region_name=region)

def lambda_handler(event, context):
    dbs = rds.describe_db_instances()
    dbclusters = set([])
    dbinstances = set([])
    for db in dbs['DBInstances']:
        if db['DBInstanceStatus'] == 'available':
            try:
                dbclusters.add(db['DBClusterIdentifier'])
            except:
                dbinstances.add(db['DBInstanceIdentifier'])
    for db in dbclusters:
        print("Stopping DB Cluster: " + db)
        rds.stop_db_cluster(DBClusterIdentifier=db)
    for db in dbinstances:
        print("Stopping DB Instance: " + db)
        rds.stop_db_instance(DBInstanceIdentifier=db)

if __name__ == '__main__':
    lambda_handler(None, None)

実行してみて、availableなステータスのインスタンスが停止されれば成功です。

DBクラスターとインスタンスの停止処理実行

AWSにアップロード

動作するLambda関数は出来上がりました。AWSのWebコンソール画面でコピー&ペーストしてもよいのですが、折角ですのでlambda-uploaderを使って登録してみましょう。

lambda-uploaderモジュールのインストール

統合ターミナルからpipenvコマンドでインストールします。

PS C:\myProject> pipenv install lambda-uploader
Installing lambda-uploader…
[    ][packages] Installing...…
Installation Succeeded
Pipfile.lock (4437b4) out of date, updating to (3ec54b)…
Locking [dev-packages] dependencies…
Success!
Locking [packages] dependencies…
Success!
Updated Pipfile.lock (4437b4)!
Installing dependencies from Pipfile.lock (4437b4)…
  ================================ 10/10 - 00:00:03
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
PS C:\myProject>

lambda-uploader用設定

stopDBInstancesフォルダーの中にlambda.jsonとして以下にようなファイルを作成します。

{
    "name": "stopDBInstances",
    "description": "Stop RDS instances in running state.",
    "region": "ap-northeast-1",
    "runtime": "python3.7",
    "handler": "function.lambda_handler",
    "role": "arn:aws:iam::XXXXXXXXXXXX:role/lambda_start_stop_rds",
    "ignore": [
        "^lambda\\.json$"
    ],
    "timeout": 30,
    "memory": 128
}

roleに定義しているlambda_start_stop_rdsですが、以下のようなインラインポリシーをもったroleを予め作成してあります。今回の関数では厳密にはStart権限までは不要です。またアカウントID部分は適宜修正してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "rds:Describe*",
                "rds:Start*",
                "rds:Stop*"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}

アップロード実行

pipenv shellコマンドで環境切り替え後、lambda-uploaderでLambda関数をアップロードします。

PS C:\myProject> pipenv shell
Loading .env environment variables…
Launching subshell in virtual environment…
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6

PS C:\myProject> cd stopDBInstances
PS C:\myProject\stopDBInstances> lambda-uploader .
Building Package
Uploading Package
Fin
PS C:\myProject\stopDBInstances>

AWS管理コンソールで確認すると、無事アップロードされていることが確認できます。

アップロードされたLambda関数

定期実行用のCloudWatchイベントルール作成

今回は毎晩21時に停止処理を実行するCloudWatchのイベントルールを作成しました。

定期実行用CloudWatchイベントルール

これでAWS CLI実行環境もたない人のためのRDSの停止処理が完成です!

今となってはLambdaでシェルも実行できるようになったので「AWS CLI直接実行すればいいじゃん」という声も聞こえそうですが、そこそこ複雑でデバッグ環境が欲しくなるような処理を作る場合には有用な手段かと思います。

今回はVisual Studio Codeですが、EclipseやPyCharm、WingIDE等でも同様のことができると思いますので、何か参考になる部分があれば喜ばしく思います。

  • 株式会社アークシステムの来訪管理・会議室予約システム BRoomHubs
  • 低コスト・短納期で提供するまるごとおまかせZabbix