29
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

SRAAdvent Calendar 2018

Day 4

AWSのリソースって使っていない時は止めたいよね!

Last updated at Posted at 2018-12-03

#AWSの料金が思ったより高かった
AWSに限らず、クラウドでは簡単にサーバを立てて使う事ができるので、ちょっとした実験やプログラムの勉強、PoC環境や開発環境を作るのに、とても便利です。
ですが、こういう用途の場合、予算があまり無い事が多いので、月末に請求書を見て「えっ、思っていたより高い」と思う事があります(ありました)。
そこで、AWSの料金を安くするために色々試してみました。
##AWS料金の大半はEC2
請求書の内訳をみると、80~90%はEC2でした。
なので、EC2の料金を抑えるのがAWS料金を抑えるのに最も効果的です。
EC2料金の内訳は以下の通りです。

  • Amazon Elastic Compute Cloud NatGateway
    • NATに対する時間課金
  • Amazon Elastic Compute Cloud running Linux/UNIX
    • EC2に対する時間課金
  • EBS
    • EC2が使っているEBSに対するサイズ課金(EC2を止めていても掛かる)
  • Elastic IP Addresses
    • インスタンスにアタッチされていないIPアドレスに対する時間課金
  • Elastic Load Balancing - Application
    • ロードバランサーに対する時間課金

###EBS以外は時間課金
EC2の料金はEBS以外は時間課金です。使わなければ料金は掛かりません。
しかし、EBSのサイズ課金はEC2を止めていても掛かるので、無駄にデカいサイズのSSDを使っていると、無駄に課金されます。
:thumbsup: EC2作成時のディスサイズは必要最低限にして、足りなくなったら追加しましょう。
:thumbsup: AMIで保存されるイメージのサイズも課金対象なので、不要なAMIは削除しましょう。
#使っていない時間は止めれば良いよね
時間課金を抑えるには、インスタンスの稼働時間を減らせば良いのですが、使う時間を減らすのでは本末転倒なので、使っていない時間帯(夜中、人によっては朝)にインスタンスを止めたいと思います。
##Lambda関数でインスタンスを止める
使わない時に止めたいのはEC2インスタンスですが、NATゲートウェイを使っている場合、この課金も安くないので、止めたいところです。残念ながら、2018年11月現在、NATゲートウェイはEC2インスタンスのように停止する事が出来ず、削除することしかできません。
NATゲートウェイを削除した場合、アタッチされているElastic IP(以下EIP)が不使用状態になって、(NATゲートウェイを止めている間)EIPに時間課金が掛かります。
EIPはネットワークインターフェースにアッタッチされているので、NATゲートウェイを削除しても直ぐにリリースする事が出来ません。なので、EC2やNATを停止、削除するLambda関数と、使っていないEIPを削除するLambda関数を2つ作る必要があります。
###Lambda関数用のIAMロール作成
Lambda関数に、EC2インスタンスの起動・停止、NATゲートウェイの作成・削除、EIPの作成・解放権限を持たせるために、以下のロールを付与したIAMロールを作成します。

  • AmazonEC2FullAccess
  • AmazonVPCFullAccess
  • AWSLambdaBasicExecutionRole

###インスタンス停止用Lambda
上記で作成したロールを使って、停止用のLambdaを作成します。

lambda.PNG
インスタンスを停止するスクリプトはこんな感じです。
記述できる言語は、C#、Go、Java、Node.js、Pythonです。
ここではPythonでサンプルを書いてみました。
python3.6
import os
import boto3

subnet = os.environ['SUBNET_ID']
rtb = os.environ['RTB_ID']
region = os.environ['REGION']
ec2ids = os.environ['EC2']
client = boto3.client('ec2')

def stop_ec2():
    instances = [id.strip() for id in ec2ids.split(',')]
    ec2 = boto3.client('ec2', region_name=region)
    ec2.stop_instances(InstanceIds=instances)

def stop_natgw():
    filters = [ { 'Name': 'subnet-id', 'Values': [subnet] },
                { 'Name': 'state', 'Values': ['available'] } ]
    response = client.describe_nat_gateways(Filters=filters)
    natgw = response['NatGateways'][0]['NatGatewayId']
    client.delete_nat_gateway(NatGatewayId=natgw)

def detach_natgw():
    response = client.delete_route(
        DestinationCidrBlock = '0.0.0.0/0',
        RouteTableId = rtb
    )

def lambda_handler(event, context):
    stop_ec2()
    detach_natgw()
    stop_natgw()

この関数はリージョンID、サブネットID、ルータID、EC2インスタンスIDを環境変数で定義しています。
環境変数には文字列しか記述できませんので、停止したいEC2インスタンスが複数ある場合は、カンマ(,)で区切って複数記述してください。

env.PNG
###不使用EIPリリース用Lambda
ネットワークインターフェースにアタッチされていないEIPをリリースするLambdaはこんな感じです。
python3.6
import boto3

client = boto3.client('ec2')

def delete_ip():
    response = client.describe_addresses(
        Filters=[
            {'Name': 'domain','Values': ['vpc'] }
        ]
    )
    for rec in response['Addresses']:
        if "NetworkInterfaceId" not in rec:
            client.release_address(AllocationId=rec['AllocationId'])

def lambda_handler(event, context):
    delete_ip()

#使う時には動いていて欲しいよね
という事で、止めたままだと寂しいので、自動起動用のLambdaも作ってみましょう。
###インスタンス起動用Lambda
NATゲートウェイ作成時に自動的にEIPを作成してアタッチします。
環境変数の指定は停止用Lambdaと同じです。
スクリプトはこんな感じです。

python3.6
import os
import boto3

subnet = os.environ['SUBNET_ID']
rtb = os.environ['RTB_ID']
region = os.environ['REGION']
ec2ids = os.environ['EC2']
client = boto3.client('ec2')

def start_ec2():
    instances = [id.strip() for id in ec2ids.split(',')]
    ec2 = boto3.client('ec2', region_name=region)
    ec2.start_instances(InstanceIds=instances)

def start_natgw():
    # EIPを新規作成
    result = client.allocate_address(
        Domain='vpc'
    )
    eip = result['AllocationId']
    # IPを固定にしたい場合は、使いたいEIPのアロケーションIDを環境変数にセットして以下を使う
    # eip = os.environ['EIP']
    response = client.create_nat_gateway(
        AllocationId=eip,
        SubnetId=subnet
    )
    natid = response['NatGateway']['NatGatewayId']
    client.get_waiter('nat_gateway_available').wait(NatGatewayIds=[natid])
    return(natid)

def atatch_natgw(natgw):
    response = client.create_route(
        DestinationCidrBlock = '0.0.0.0/0',
        NatGatewayId = natgw,
        RouteTableId = rtb
    )

def lambda_handler(event, context):
    start_ec2()
    natgw = start_natgw()
    atatch_natgw(natgw)

ここで注意する事は、EIPを新規に作成する毎にIPアドレスが変わる事です。
NATゲートウェイのIPを固定にしたい場合は、creata_nat_gateway()のAllocationIDパラメータにアッタッチしたいEIPのアロケーションIDをセットします(その場合、NATゲートウェイ停止中もEIPを保持し続けるので、EIPに時間課金が発生します)。
##CloudWatchで自動起動・停止
Lambdaを手動で動かして、動くことが確認できれば、自動起動・停止にしてしまいましょう。
CloudWatchのルール設定を使って、cron形式の指定でLambda関数を実行することができます。

rule.PNG

時間に指定はUTCなので注意が必要です。
例えば、平日の午前8時に起動したい時は以下のように指定します。
0 23 ? * SUN-THU *
或いは、平日の午後8時に起動したい時は以下のように指定します。
0 11 ? * MON-FRI *

#さいごに
AWSのサービスを操作するためのPythonのライブラリ群は、こちらのドキュメントを参考にしました。今回はEC2のClientだけで作ってみましたが、色々なサービスを操作出来るようなので、また試してみたいと思っています。

さて、ここで一つ注意点(落とし穴)があります。
EC2インスタンスの時間課金は秒単位ですが、NATゲートウェイやEIPは時間単位のようです。
上記のLambdaをテストする際、起動・停止する度に、(数秒しか使っていなくても)NATゲートウェイやEIPが1時間分づつ課金されてしまいます。
20回くらいテストしたので20時間分課金されてました。。。 :scream:
課金を安くしようとしたのに、余計な課金が発生するという罠が!

それはともかく、新年早々、余計な課金に驚かなくて済むように、年末年始は使っていないリソースを止めましょう!(なお、本記事はSRA Advent Calendar 2018に掲載のものです)

29
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?