Cloud9 で SAM を利用し AWS サービス毎の請求額を毎日 Slack に通知する
コーヒーが好きな emi です。
AWSサービス毎の請求額を毎日 Slack に通知するため、以下のブログ AWSサービス毎の請求額を毎日Slackに通知してみた を見ながら設定しようとしたのですが、手元の Windows 11 端末に AWS CLI、AWS SAM CLI、Python などの開発環境を整えるのが面倒…!!と思いました。
そこで、AWS Cloud9 を使って手軽に一時的な開発環境を構築し、AWS Serverless Application Model (SAM) でサーバレス通知システムを構築しました。
AWS Serverless Application Model (SAM) とは
AWS SAM は、サーバーレスアプリケーション構築用のオープンソースフレームワークです。関数、API、データベース、イベントソースマッピングなどを定義し、CloudFormation テンプレート(YAML)を使用して簡単に構築できます。
AWS Cloud9 とは
AWS Cloud9 は、ブラウザのみでコードを記述、実行、デバッグできるクラウドベースの統合開発環境 (IDE) です。
内部的には EC2 インスタンスに開発環境が搭載されたものです。
構成
「毎日、指定時刻になると Lambda を起動し、Lambda が請求額を取得& Slack に Post する」というサーバーレスな構成を作成します。
デプロイのため Cloud9 で SAM を使います。
Cloud9 は AWS CLI や AWS SAM のために一時的に利用するだけなので、環境作成後は削除します。
事前準備
以下ブログの「事前準備」部分を実施してください。
- AWS 請求の設定
- Slack の設定
- Webhook URLをメモしておく
やってみる
Cloud9 環境の構築
デフォルト VPC に Amazon Linux 2 ベースの Cloud9 環境を作成します。
AWS マネジメントコンソールで Cloud9 コンソールに移動し、[Create environment] をクリックします。
Create environment の画面に遷移します。
- Name:sam-environment
- Description:sam-environment
のみ入力し、その他の設定はすべてデフォルトのまま進め、[Create] をクリックます。
Name は任意の名前で構いません。
1~2 分程度待ち、緑のバーで「Successfully created ~~」と表示されたら OK です。 [Open] をクリックし、Cloud9 環境を開きます。
少し待つと画像のような画面が開きます。
画面下部のターミナルでコマンドを入力して操作していきます。
aws --version
コマンドで AWS CLI のバージョンを確認します。
- 実行結果
kitani.emi:~/environment $ aws --version aws-cli/1.19.112 Python/2.7.18 Linux/4.14.311-233.529.amzn2.x86_64 botocore/1.20.112 kitani.emi:~/environment $
sam --version
コマンドで SAM CLI のバージョンを確認します。
- 実行結果
kitani.emi:~/environment $ sam --version SAM CLI, version 1.57.0 kitani.emi:~/environment $
python --version
コマンドで Python のバージョンを確認します。
- 実行結果
kitani.emi:~/environment $ python --version Python 3.7.16 kitani.emi:~/environment $
Python のバージョンアップ
Python 3.8 は Amazon Linux 2 のデフォルトリポジトリに含まれています。
Q: Amazon Linux Extras リポジトリからソフトウェアパッケージをインストールするにはどうすればよいですか?
A: 利用できるパッケージの一覧は、Amazon Linux 2 のシェルで amazon-linux-extras コマンドを使って表示できます。Extras のパッケージは "sudo amazon-linux-extras install" コマンドを使ってインストールできます。
次のコマンドを実行してPython 3.8 をインストールします。
- 実行コマンド
sudo amazon-linux-extras install python3.8
実行結果(クリックで展開)
kitani.emi:~/environment $ sudo amazon-linux-extras install python3.8 Installing python38 Loaded plugins: extras_suggestions, langpacks, priorities, update-motd Cleaning repos: amzn2-core amzn2extra-docker amzn2extra-epel amzn2extra-lamp-mariadb10.2-php7.2 amzn2extra-python3.8 epel hashicorp 33 metadata files removed 11 sqlite files removed 0 metadata files removed Loaded plugins: extras_suggestions, langpacks, priorities, update-motd amzn2-core | 3.7 kB 00:00:00 amzn2extra-docker | 3.0 kB 00:00:00 amzn2extra-epel | 3.0 kB 00:00:00 amzn2extra-lamp-mariadb10.2-php7.2 | 3.0 kB 00:00:00 amzn2extra-python3.8 | 3.0 kB 00:00:00 epel/x86_64/metalink | 5.7 kB 00:00:00 epel | 4.7 kB 00:00:00 hashicorp | 1.4 kB 00:00:00 (1/15): amzn2-core/2/x86_64/group_gz | 2.5 kB 00:00:00 (2/15): amzn2-core/2/x86_64/updateinfo | 586 kB 00:00:00 (3/15): amzn2extra-epel/2/x86_64/primary_db | 1.8 kB 00:00:00 (4/15): amzn2extra-lamp-mariadb10.2-php7.2/2/x86_64/updateinfo | 76 B 00:00:00 (5/15): amzn2extra-lamp-mariadb10.2-php7.2/2/x86_64/primary_db | 506 kB 00:00:00 (6/15): amzn2extra-python3.8/2/x86_64/updateinfo | 76 B 00:00:00 (7/15): amzn2extra-python3.8/2/x86_64/primary_db | 67 kB 00:00:00 (8/15): amzn2extra-docker/2/x86_64/updateinfo | 9.1 kB 00:00:00 (9/15): amzn2extra-docker/2/x86_64/primary_db | 107 kB 00:00:00 (10/15): amzn2extra-epel/2/x86_64/updateinfo | 76 B 00:00:00 (11/15): epel/x86_64/group_gz | 99 kB 00:00:00 (12/15): epel/x86_64/updateinfo | 1.0 MB 00:00:00 (13/15): hashicorp/x86_64/primary | 156 kB 00:00:00 (14/15): epel/x86_64/primary_db | 7.0 MB 00:00:00 (15/15): amzn2-core/2/x86_64/primary_db | 71 MB 00:00:01 hashicorp 1122/1122 244 packages excluded due to repository priority protections Resolving Dependencies --> Running transaction check ---> Package python38.x86_64 0:3.8.16-1.amzn2.0.2 will be installed --> Processing Dependency: python38-libs(x86-64) = 3.8.16-1.amzn2.0.2 for package: python38-3.8.16-1.amzn2.0.2.x86_64 --> Processing Dependency: python38-setuptools for package: python38-3.8.16-1.amzn2.0.2.x86_64 --> Processing Dependency: python38-pip for package: python38-3.8.16-1.amzn2.0.2.x86_64 --> Processing Dependency: libpython3.8.so.1.0()(64bit) for package: python38-3.8.16-1.amzn2.0.2.x86_64 --> Running transaction check ---> Package python38-libs.x86_64 0:3.8.16-1.amzn2.0.2 will be installed ---> Package python38-pip.noarch 0:21.0.1-4.amzn2.0.1 will be installed ---> Package python38-setuptools.noarch 0:38.4.0-4.amzn2.0.1 will be installed --> Finished Dependency Resolution Dependencies Resolved =================================================================================================================================================== Package Arch Version Repository Size =================================================================================================================================================== Installing: python38 x86_64 3.8.16-1.amzn2.0.2 amzn2extra-python3.8 70 k Installing for dependencies: python38-libs x86_64 3.8.16-1.amzn2.0.2 amzn2extra-python3.8 10 M python38-pip noarch 21.0.1-4.amzn2.0.1 amzn2extra-python3.8 2.1 M python38-setuptools noarch 38.4.0-4.amzn2.0.1 amzn2extra-python3.8 619 k Transaction Summary =================================================================================================================================================== Install 1 Package (+3 Dependent packages) Total download size: 13 M Installed size: 54 M Is this ok [y/d/N]: y Downloading packages: (1/4): python38-3.8.16-1.amzn2.0.2.x86_64.rpm | 70 kB 00:00:00 (2/4): python38-pip-21.0.1-4.amzn2.0.1.noarch.rpm | 2.1 MB 00:00:00 (3/4): python38-setuptools-38.4.0-4.amzn2.0.1.noarch.rpm | 619 kB 00:00:00 (4/4): python38-libs-3.8.16-1.amzn2.0.2.x86_64.rpm | 10 MB 00:00:00 --------------------------------------------------------------------------------------------------------------------------------------------------- Total 28 MB/s | 13 MB 00:00:00 Running transaction check Running transaction test Transaction test succeeded Running transaction Installing : python38-setuptools-38.4.0-4.amzn2.0.1.noarch 1/4 Installing : python38-pip-21.0.1-4.amzn2.0.1.noarch 2/4 Installing : python38-3.8.16-1.amzn2.0.2.x86_64 3/4 Installing : python38-libs-3.8.16-1.amzn2.0.2.x86_64 4/4 Verifying : python38-libs-3.8.16-1.amzn2.0.2.x86_64 1/4 Verifying : python38-3.8.16-1.amzn2.0.2.x86_64 2/4 Verifying : python38-setuptools-38.4.0-4.amzn2.0.1.noarch 3/4 Verifying : python38-pip-21.0.1-4.amzn2.0.1.noarch 4/4 Installed: python38.x86_64 0:3.8.16-1.amzn2.0.2 Dependency Installed: python38-libs.x86_64 0:3.8.16-1.amzn2.0.2 python38-pip.noarch 0:21.0.1-4.amzn2.0.1 python38-setuptools.noarch 0:38.4.0-4.amzn2.0.1 Complete! 0 ansible2 available \ [ =2.4.2 =2.4.6 =2.8 =stable ] 2 httpd_modules available [ =1.0 =stable ] 3 memcached1.5 available \ [ =1.5.1 =1.5.16 =1.5.17 ] 6 postgresql10 available [ =10 =stable ] 9 R3.4 available [ =3.4.3 =stable ] 10 rust1 available \ [ =1.22.1 =1.26.0 =1.26.1 =1.27.2 =1.31.0 =1.38.0 =stable ] 17 *lamp-mariadb10.2-php7.2=latest enabled \ [ =10.2.10_7.2.0 =10.2.10_7.2.4 =10.2.10_7.2.5 =10.2.10_7.2.8 =10.2.10_7.2.11 =10.2.10_7.2.13 =10.2.10_7.2.14 =10.2.10_7.2.16 =10.2.10_7.2.17 =10.2.10_7.2.19 =10.2.10_7.2.22 =10.2.10_7.2.23 =10.2.10_7.2.24 =stable ] 18 libreoffice available \ [ =5.0.6.2_15 =5.3.6.1 =stable ] 19 gimp available [ =2.8.22 ] 20 docker=latest enabled \ [ =17.12.1 =18.03.1 =18.06.1 =18.09.9 =stable ] 21 mate-desktop1.x available \ [ =1.19.0 =1.20.0 =stable ] 22 GraphicsMagick1.3 available \ [ =1.3.29 =1.3.32 =1.3.34 =stable ] 23 tomcat8.5 available \ [ =8.5.31 =8.5.32 =8.5.38 =8.5.40 =8.5.42 =8.5.50 =stable ] 24 epel=latest enabled [ =7.11 =stable ] 25 testing available [ =1.0 =stable ] 26 ecs available [ =stable ] 27 corretto8 available \ [ =1.8.0_192 =1.8.0_202 =1.8.0_212 =1.8.0_222 =1.8.0_232 =1.8.0_242 =stable ] 29 golang1.11 available \ [ =1.11.3 =1.11.11 =1.11.13 =stable ] 30 squid4 available [ =4 =stable ] 32 lustre2.10 available \ [ =2.10.5 =2.10.8 =stable ] 33 java-openjdk11 available [ =11 =stable ] 34 lynis available [ =stable ] 36 BCC available [ =0.x =stable ] 37 mono available [ =5.x =stable ] 38 nginx1 available [ =stable ] 40 mock available [ =stable ] 41 postgresql11 available [ =11 =stable ] 43 livepatch available [ =stable ] 44 python3.8=latest enabled [ =stable ] 45 haproxy2 available [ =stable ] 46 collectd available [ =stable ] 47 aws-nitro-enclaves-cli available [ =stable ] 48 R4 available [ =stable ] 49 kernel-5.4 available [ =stable ] 50 selinux-ng available [ =stable ] _ php8.0 available [ =stable ] 52 tomcat9 available [ =stable ] 53 unbound1.13 available [ =stable ] _ mariadb10.5 available [ =stable ] 55 kernel-5.10 available [ =stable ] 56 redis6 available [ =stable ] 57 ruby3.0 available [ =stable ] 58 postgresql12 available [ =stable ] 59 postgresql13 available [ =stable ] 60 mock2 available [ =stable ] 61 dnsmasq2.85 available [ =stable ] 62 kernel-5.15 available [ =stable ] 63 postgresql14 available [ =stable ] 64 firefox available [ =stable ] 65 lustre available [ =stable ] _ php8.1 available [ =stable ] 67 awscli1 available [ =stable ] _ php8.2 available [ =stable ] 69 dnsmasq available [ =stable ] 70 unbound1.17 available [ =stable ] 71 golang1.19 available [ =stable ] * Extra topic has reached end of support. kitani.emi:~/environment $
python3.8 --version
コマンドで Python のバージョンを確認します。
- 実行結果
kitani.emi:~/environment $ python3.8 --version Python 3.8.16 kitani.emi:~/environment $
sam init
sam init
コマンドで、プロジェクトのフォルダを作成します。
- 実行コマンド
sam init --runtime python3.8 --name AWS_Billing
以下のように対話形式で進みます。
kitani.emi:~/environment $ sam init --runtime python3.8 --name AWS_Billing Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice:
1 を入力して Enter を押下します。
Choice: 1 Choose an AWS Quick Start application template 1 - Hello World Example 2 - Infrastructure event management 3 - Multi-step workflow 4 - Lambda EFS example Template:
1 を入力して Enter を押下します。
Template: 1 Based on your selections, the only Package type available is Zip. We will proceed to selecting the Package type as Zip. Based on your selections, the only dependency manager available is pip. We will proceed copying the template using pip. Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]:
アプリケーションの関数の X-Ray トレースを有効にしたいですか?と聞かれています。
N を入力して Enter を押下します。
Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]: N Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment) ----------------------- Generating application: ----------------------- Name: AWS_Billing Runtime: python3.8 Architectures: x86_64 Dependency Manager: pip Application Template: hello-world Output Directory: . Next steps can be found in the README file at ./AWS_Billing/README.md Commands you can use next ========================= [*] Create pipeline: cd AWS_Billing && sam pipeline init --bootstrap [*] Validate SAM template: sam validate [*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch SAM CLI update available (1.80.0); (1.57.0 installed) To download: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html kitani.emi:~/environment $
ls -l
を実行すると、AWS_Billing というディレクトリができているのが確認できます。
- 実行結果
kitani.emi:~/environment $ ls -l total 4 drwxrwxr-x 5 ec2-user ec2-user 127 Apr 18 11:13 AWS_Billing -rw-r--r-- 1 ec2-user ec2-user 569 Apr 11 13:25 README.md kitani.emi:~/environment $
Cloud9 の画面左側を見ると、AWS_Billing ディレクトリの中身が確認できます。
以下のような構成になっています。
. ├── AWS_Billing │ ├── events │ │ └── event.json │ ├── hello_world │ │ ├── app.py │ │ ├── __init__.py │ │ └── requirements.txt │ ├── __init__.py │ ├── README.md │ ├── template.yaml │ └── tests │ ├── __init__.py │ ├── integration │ │ ├── __init__.py │ │ └── test_api_gateway.py │ ├── requirements.txt │ └── unit │ ├── __init__.py │ └── test_handler.py └── README.md
template ファイルを編集(template.yaml)
AWS_Billing ディレクトリ配下の template.yaml を編集します。
Cloud9 の画面左側で template.yaml を選択すると、ターミナル上部で編集することができます。
template.yaml 編集前(クリックで展開)
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > AWS_Billing Sample SAM Template for AWS_Billing # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: Timeout: 3 Resources: HelloWorldFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.8 Architectures: - x86_64 Events: HelloWorld: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /hello Method: get Outputs: # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function # Find out more about other implicit resources you can reference within SAM # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api HelloWorldApi: Description: "API Gateway endpoint URL for Prod stage for Hello World function" Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" HelloWorldFunction: Description: "Hello World Lambda Function ARN" Value: !GetAtt HelloWorldFunction.Arn HelloWorldFunctionIamRole: Description: "Implicit IAM Role created for Hello World function" Value: !GetAtt HelloWorldFunctionRole.Arn
編集前の内容をすべて削除し、templateファイル を参考に以下の内容で上書きし保存します。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Notify Slack every day of AWS billing Globals: Function: Timeout: 10 Parameters: SlackWebhookUrl: Type: String Default: hoge Resources: # https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html # https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-lambda-state-machine-cloudformation.html BillingIamRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: "sts:AssumeRole" Policies: - PolicyName: "NotifySlackToBillingLambdaPolicy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" - "ce:GetCostAndUsage" Resource: "*" HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.8 # python3.6→python3.8 に変更 Environment: Variables: # このURLはコミット&公開したくないため、デプロイ時にコマンドで設定する SLACK_WEBHOOK_URL: !Ref SlackWebhookUrl Role: !GetAtt BillingIamRole.Arn Events: NotifySlack: Type: Schedule Properties: Schedule: cron(0 0 * * ? *) # 日本時間AM9時に毎日通知する Outputs: HelloWorldFunction: Description: "Hello World Lambda Function ARN" Value: !GetAtt HelloWorldFunction.Arn HelloWorldFunctionIamRole: Description: "Implicit IAM Role created for Hello World function" Value: !GetAtt BillingIamRole.Arn
Lambda 関数の中身を編集(app.py)
AWS_Billing ディレクトリ配下の app.py を編集し、Lambda 関数の中身を記述します。
app.py 編集前(クリックで展開)
import json # import requests def lambda_handler(event, context): """Sample pure Lambda function Parameters ---------- event: dict, required API Gateway Lambda Proxy Input Format Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format context: object, required Lambda Context runtime methods and attributes Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html Returns ------ API Gateway Lambda Proxy Output Format: dict Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html """ # try: # ip = requests.get("http://checkip.amazonaws.com/") # except requests.RequestException as e: # # Send some context about this error to Lambda Logs # print(e) # raise e return { "statusCode": 200, "body": json.dumps({ "message": "hello world", # "location": ip.text.replace("\n", "") }), }
編集前の内容をすべて削除し、Lambda関数の作成 を参考に以下の内容で上書きし保存します。
import os import boto3 import json import requests from datetime import datetime, timedelta, date SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL'] def lambda_handler(event, context) -> None: client = boto3.client('ce', region_name='us-east-1') # 合計とサービス毎の請求額を取得する total_billing = get_total_billing(client) service_billings = get_service_billings(client) # Slack用のメッセージを作成して投げる (title, detail) = get_message(total_billing, service_billings) post_slack(title, detail) def get_total_billing(client) -> dict: (start_date, end_date) = get_total_cost_date_range() # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ce.html#CostExplorer.Client.get_cost_and_usage response = client.get_cost_and_usage( TimePeriod={ 'Start': start_date, 'End': end_date }, Granularity='MONTHLY', Metrics=[ 'AmortizedCost' ] ) return { 'start': response['ResultsByTime'][0]['TimePeriod']['Start'], 'end': response['ResultsByTime'][0]['TimePeriod']['End'], 'billing': response['ResultsByTime'][0]['Total']['AmortizedCost']['Amount'], } def get_service_billings(client) -> list: (start_date, end_date) = get_total_cost_date_range() # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ce.html#CostExplorer.Client.get_cost_and_usage response = client.get_cost_and_usage( TimePeriod={ 'Start': start_date, 'End': end_date }, Granularity='MONTHLY', Metrics=[ 'AmortizedCost' ], GroupBy=[ { 'Type': 'DIMENSION', 'Key': 'SERVICE' } ] ) billings = [] for item in response['ResultsByTime'][0]['Groups']: billings.append({ 'service_name': item['Keys'][0], 'billing': item['Metrics']['AmortizedCost']['Amount'] }) return billings def get_message(total_billing: dict, service_billings: list) -> (str, str): start = datetime.strptime(total_billing['start'], '%Y-%m-%d').strftime('%m/%d') # Endの日付は結果に含まないため、表示上は前日にしておく end_today = datetime.strptime(total_billing['end'], '%Y-%m-%d') end_yesterday = (end_today - timedelta(days=1)).strftime('%m/%d') total = round(float(total_billing['billing']), 2) title = f'{start}~{end_yesterday}の請求額は、{total:.2f} USDです。' details = [] for item in service_billings: service_name = item['service_name'] billing = round(float(item['billing']), 2) if billing == 0.0: # 請求無し(0.0 USD)の場合は、内訳を表示しない continue details.append(f' ・{service_name}: {billing:.2f} USD') return title, '\n'.join(details) def post_slack(title: str, detail: str) -> None: # https://api.slack.com/incoming-webhooks # https://api.slack.com/docs/message-formatting # https://api.slack.com/docs/messages/builder payload = { 'attachments': [ { 'color': '#36a64f', 'pretext': title, 'text': detail } ] } # http://requests-docs-ja.readthedocs.io/en/latest/user/quickstart/ try: response = requests.post(SLACK_WEBHOOK_URL, data=json.dumps(payload)) except requests.exceptions.RequestException as e: print(e) else: print(response.status_code) def get_total_cost_date_range() -> (str, str): start_date = get_begin_of_month() end_date = get_today() # get_cost_and_usage()のstartとendに同じ日付は指定不可のため、 # 「今日が1日」なら、「先月1日から今月1日(今日)」までの範囲にする if start_date == end_date: end_of_month = datetime.strptime(start_date, '%Y-%m-%d') + timedelta(days=-1) begin_of_month = end_of_month.replace(day=1) return begin_of_month.date().isoformat(), end_date return start_date, end_date def get_begin_of_month() -> str: return date.today().replace(day=1).isoformat() def get_prev_day(prev: int) -> str: return (date.today() - timedelta(days=prev)).isoformat() def get_today() -> str: return date.today().isoformat()
S3 バケットの作成
コード等を格納するための S3 バケットを作成します。
s3://xxxxx
の xxxxx
部分に作成する S3 バケット名を入れて実行してください。
今回は sam-bucket-emikitani
という名前で作成します。
- 実行コマンド
aws s3 mb s3://sam-bucket-emikitani
-
実行結果
kitani.emi:~/environment $ aws s3 mb s3://sam-bucket-emikitani make_bucket: sam-bucket-emikitani kitani.emi:~/environment $
sam build
cd AWS_Billing/
コマンドでAWS_Billing ディレクトリに移動します。
- 実行結果
kitani.emi:~/environment $ cd AWS_Billing/ kitani.emi:~/environment/AWS_Billing $
sam build
コマンドでビルドします。
- 実行結果
kitani.emi:~/environment/AWS_Billing $ sam build Building codeuri: /home/ec2-user/environment/AWS_Billing/hello_world runtime: python3.8 metadata: {} architecture: x86_64 functions: HelloWorldFunction Running PythonPipBuilder:ResolveDependencies Running PythonPipBuilder:CopySource Build Succeeded Built Artifacts : .aws-sam/build Built Template : .aws-sam/build/template.yaml Commands you can use next ========================= [*] Validate SAM template: sam validate [*] Invoke Function: sam local invoke [*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch [*] Deploy: sam deploy --guided kitani.emi:~/environment/AWS_Billing $
sam package
続いて以下のコマンドを実行し、コード一式を S3 バケットにアップロードします。
- 実行コマンド
sam package \ --output-template-file packaged.yaml \ --s3-bucket sam-bucket-emikitani
-
実行結果
kitani.emi:~/environment/AWS_Billing $ sam package \ > --output-template-file packaged.yaml \ > --s3-bucket sam-bucket-emikitani Uploading to 82dfd5a6da2b821fb0c5d84f4d08ad16 617632 / 617632 (100.00%) Successfully packaged artifacts and wrote output template to file packaged.yaml. Execute the following command to deploy the packaged template sam deploy --template-file /home/ec2-user/environment/AWS_Billing/packaged.yaml --stack-name <YOUR STACK NAME> kitani.emi:~/environment/AWS_Billing $
sam deploy
最後にデプロイします。template.yaml の環境変数をオーバーライドし、ここで Slack の Webhook URL を設定します。
- 実行コマンド
sam deploy \ --template-file packaged.yaml \ --stack-name NotifyBillingToSlack \ --capabilities CAPABILITY_IAM \ --parameter-overrides SlackWebhookUrl=https://hooks.slack.com/services/xxxxxxxxxxxxx
SlackWebhookUrl=https://hooks.slack.com/services/xxxxxxxxxxxxx
にはコピーしておいた Slack の Webhook URL を入れてください。
sam deploy 実行結果(クリックで展開)
kitani.emi:~/environment/AWS_Billing $ sam deploy \ > --template-file packaged.yaml \ > --stack-name NotifyBillingToSlack \ > --capabilities CAPABILITY_IAM \ > --parameter-overrides SlackWebhookUrl=https://hooks.slack.com/services/xxxxxxxxxxxxx Deploying with following values =============================== Stack name : NotifyBillingToSlack Region : None Confirm changeset : False Disable rollback : False Deployment s3 bucket : None Capabilities : ["CAPABILITY_IAM"] Parameter overrides : {"SlackWebhookUrl": "https://hooks.slack.com/services/xxxxxxxxxxxxx"} Signing Profiles : {} Initiating deployment ===================== Waiting for changeset to be created.. CloudFormation stack changeset --------------------------------------------------------------------------------------------------------------------- Operation LogicalResourceId ResourceType Replacement --------------------------------------------------------------------------------------------------------------------- + Add BillingIamRole AWS::IAM::Role N/A + Add HelloWorldFunctionNotifySla AWS::Lambda::Permission N/A ckPermission + Add HelloWorldFunctionNotifySla AWS::Events::Rule N/A ck + Add HelloWorldFunction AWS::Lambda::Function N/A --------------------------------------------------------------------------------------------------------------------- Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:123456789012:changeSet/samcli-deploy1681817652/11a848f8-0228-41e0-b39e-xxxxxxxxxxxx 2023-04-18 11:34:23 - Waiting for stack create/update to complete CloudFormation events from stack operations (refresh every 0.5 seconds) --------------------------------------------------------------------------------------------------------------------- ResourceStatus ResourceType LogicalResourceId ResourceStatusReason --------------------------------------------------------------------------------------------------------------------- CREATE_IN_PROGRESS AWS::IAM::Role BillingIamRole - CREATE_IN_PROGRESS AWS::IAM::Role BillingIamRole Resource creation Initiated CREATE_COMPLETE AWS::IAM::Role BillingIamRole - CREATE_IN_PROGRESS AWS::Lambda::Function HelloWorldFunction - CREATE_IN_PROGRESS AWS::Lambda::Function HelloWorldFunction Resource creation Initiated CREATE_COMPLETE AWS::Lambda::Function HelloWorldFunction - CREATE_IN_PROGRESS AWS::Events::Rule HelloWorldFunctionNotifySla - ck CREATE_IN_PROGRESS AWS::Events::Rule HelloWorldFunctionNotifySla Resource creation Initiated ck CREATE_COMPLETE AWS::Events::Rule HelloWorldFunctionNotifySla - ck CREATE_IN_PROGRESS AWS::Lambda::Permission HelloWorldFunctionNotifySla - ckPermission CREATE_IN_PROGRESS AWS::Lambda::Permission HelloWorldFunctionNotifySla Resource creation Initiated ckPermission CREATE_COMPLETE AWS::Lambda::Permission HelloWorldFunctionNotifySla - ckPermission CREATE_COMPLETE AWS::CloudFormation::Stack NotifyBillingToSlack - --------------------------------------------------------------------------------------------------------------------- CloudFormation outputs from deployed stack ----------------------------------------------------------------------------------------------------------------------- Outputs ----------------------------------------------------------------------------------------------------------------------- Key HelloWorldFunctionIamRole Description Implicit IAM Role created for Hello World function Value arn:aws:iam::123456789012:role/NotifyBillingToSlack-BillingIamRole-xxxxxxxxxxxx Key HelloWorldFunction Description Hello World Lambda Function ARN Value arn:aws:lambda:ap-northeast-1:123456789012:function:NotifyBillingToSlack- HelloWorldFunction-xxxxxxxxxxx ----------------------------------------------------------------------------------------------------------------------- Successfully created/updated stack - NotifyBillingToSlack in None kitani.emi:~/environment/AWS_Billing $
裏では CloudFormation スタックが作成されているので、スタックの展開が完了するまで時間がかかります。
Cloud9 のターミナル上でスタックの進行状況が確認できますし、CloudFormation コンソールからもスタックの進行状況を確認することができます。
スタックの展開完了後 Lambda コンソールを確認すると、 Lambda 関数が作成されています。
動作確認
Lambda コンソールから手動でテストしてみます。
テストタブを開き、[テスト] をクリックします。
関数の実行が成功します。
Slack にも通知されました。
Cost Explorer の表示と見比べて、料金が大体一致しているのがわかります。
AM 9:00 にも時間通り通知されました。
Cloud9 環境の削除
SAM の実行に使用した Cloud9 環境を削除します。
Cloud9 コンソールに移動し、作成した Cloud9 環境を選択して [Delete] をクリックします。
テキストフィールドに Delete と入力し、[Delete] をクリックします。
終わりに
手元に開発環境がなくても、Cloud9 を利用してサーバーレス環境の構築ができました。
どなたかのお役に立てば幸いです。