Life with IT

山好きITエンジニア 木檜(こぐれ)和明 による発信の場

Amazon Web Services

2019/10/21更新

AWS SAMのCLIであるaws-sam-cliを使うとローカルでLambda環境を作ってテストし、それをAWS上にデプロイできる。

ここではaws-sam-cliのインストールからローカルでのテスト、AWS上にデプロイするまでの手順を示す。

aws-sam-cliをインストールするOSはUbuntu 18.04とする。

aws-sam-cliのインストールと動作確認

動作環境

aws-sam-cliを使うにあたりあらかじめ以下をインストールしておく。

Python 3.x
pip
Docker
AWS CLI
$ python --version
Python 3.6.8

$ pip --version
pip 19.3.1 from /usr/local/lib/python3.6/dist-packages/pip (python 3.6)

$ docker --version
Docker version 18.09.7, build 2d0083d

$ aws --version
aws-cli/1.16.263 Python/3.6.8 Linux/4.15.0-65-generic botocore/1.12.253

aws-sam-cliインストール

aws-sam-cliはpipで簡単にインストールできる。

$ sudo pip install aws-sam-cli

$ sam --version
SAM CLI, version 0.22.0

サンプルアプリケーション作成(Python3.6)

aws-sam-cliがインストールできたらサンプルアプリケーションを作成してみる。

Lambdaは様々な言語で記述できるが、ここではPython3.6のサンプルアプリケーションを作成する。

$ sam init --runtime python3.6 --name sam-python
[+] Initializing project structure...

Project generated: ./sam-python

Steps you can take next within the project folder
===================================================
[*] Invoke Function: sam local invoke HelloWorldFunction --event event.json
[*] Start API Gateway locally: sam local start-api

Read sam-python/README.md for further instructions

[*] Project initialization is now complete

$ tree sam-python
sam-python
├── README.md
├── events
│   └── event.json ................. Lambdaに渡すイベントの定義
├── hello_world
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-36.pyc
│   │   └── app.cpython-36.pyc
│   ├── app.py ..................... Lambda関数本体
│   └── requirements.txt
├── template.yaml .................. SAMテンプレート
└── tests
    └── unit
        ├── __init__.py
        ├── __pycache__
        │   ├── __init__.cpython-36.pyc
        │   └── test_handler.cpython-36.pyc
        └── test_handler.py

6 directories, 12 files
単独実行

アプリケーションができたら実行してみる。

$ cd sam-python

$ sam local invoke HelloWorldFunction --event events/event.json --log-file lambda.log
Invoking app.lambda_handler (python3.6)
2019-10-20 23:13:06 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:python3.6 Docker container image...(略)...
Mounting ~/sam-python/hello_world as /var/task:ro,delegated inside runtime container

実行結果はログファイルに出力される。

$ cat lambda.log
START RequestId: 6eab94c3-2835-4241-9b97-b94b2bd60f2f Version: $LATEST
END RequestId: 6eab94c3-2835-4241-9b97-b94b2bd60f2f
REPORT RequestId: 6eab94c3-2835-4241-9b97-b94b2bd60f2f Duration: 0 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 19 MB

{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}

--eventで指定したイベントの定義は以下のようにして作成できる。

$ sam local generate-event [OPTIONS] COMMAND [ARGS] > event.json

COMMANDには様々なAWSのサービスが指定でき、それぞれARGSが異なるのでhelpを参照する。

$ sam local generate-event --help
:
Commands:
  alexa-skills-kit
  alexa-smart-home
  apigateway
  batch
  cloudformation
  cloudfront
  cloudwatch
  codecommit
  codepipeline
  cognito
  config
  connect
  dynamodb
  kinesis
  lex
  rekognition
  s3
  ses
  sns
  sqs
  stepfunctions

(S3用イベントの場合)

$ sam local generate-event s3 --help
:
Commands:
  delete  Generates an Amazon S3 Delete Event
  put     Generates an Amazon S3 Put Event

$ sam local generate-event s3 put --help
:

$ sam local generate-event s3 put --help
Usage: sam local generate-event s3 put [OPTIONS]

Options:
  --region TEXT     Specify the region name you'd like, otherwise the default
                    = us-east-1
  --partition TEXT  Specify the partition name you'd like, otherwise the
                    default = aws
  --bucket TEXT     Specify the bucket name you'd like, otherwise the default
                    = example-bucket
  --key TEXT        Specify the key name you'd like, otherwise the default =
                    test/key
  --debug           Turn on debug logging to print debug message generated by
                    SAM CLI.
  --help            Show this message and exit.
API Gateway経由の実行

次にAPI Gateway経由で実行する。まずはAPI Gatewayを作成してリクエストを待ち受ける。

$ sam local start-api --log-file lambda.log
Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2019-10-20 23:15:33  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

この状態で別ターミナルからcurlでアクセスするとAPI Gateway経由でLambdaが起動する。

$ curl http://127.0.0.1:3000/hello
{"message": "hello world"}

元ターミナルには以下のように出力される。

Invoking app.lambda_handler (python3.6)
2019-10-20 23:15:54 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:python3.6 Docker container image......
Mounting /home/xxxxx/sam-python/hello_world as /var/task:ro,delegated inside runtime container
No Content-Type given. Defaulting to 'application/json'.
2019-10-20 23:15:59 127.0.0.1 - - [20/Oct/2019 23:15:59] "GET /hello HTTP/1.1" 200 -
-> Ctrl + cで中断

ログファイルには以下のように記録される。

$ cat lambda.log
START RequestId: 7b951478-46f9-427a-a9fc-5c2ac8b9966f Version: $LATEST
END RequestId: 7b951478-46f9-427a-a9fc-5c2ac8b9966f
REPORT RequestId: 7b951478-46f9-427a-a9fc-5c2ac8b9966f Duration: 0 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 19 MB

またLambdaはDockerコンテナ上で実行されるのでDockerイメージがダウンロードされている。

$ docker images
REPOSITORY      TAG         IMAGE ID        CREATED         SIZE
lambci/lambda   python3.6   2659a569a6df    2 weeks ago     866MB
ローカルエンドポイント経由の実行

ローカルエンドポイントを作成してAWS CLIから実行することもできる。

$ sam local start-lambda --log-file lambda.log
Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template through the endpoint.
2019-10-21 00:12:59  * Running on http://127.0.0.1:3001/ (Press CTRL+C to quit)

この状態で別ターミナルからAWS CLIでLambdaを実行させる。

$ aws lambda invoke --function-name HelloWorldFunction --endpoint-url http://127.0.0.1:3001 out.txt
{
    "StatusCode": 200
}

$ cat out.txt
{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}

元ターミナルには以下のように出力される。

Invoking app.lambda_handler (python3.6)
2019-10-21 00:14:35 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:python3.6 Docker container image......
Mounting /home/xxxxx/sam-python/hello_world as /var/task:ro,delegated inside runtime container
2019-10-21 00:14:40 127.0.0.1 - - [21/Oct/2019 00:14:40] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -
-> Ctrl + cで中断

ログファイルには以下のように記録される。

$ cat lambda.log
START RequestId: c3da9704-d7a4-45bf-b484-a0fd274c8036 Version: $LATEST
END RequestId: c3da9704-d7a4-45bf-b484-a0fd274c8036
REPORT RequestId: c3da9704-d7a4-45bf-b484-a0fd274c8036 Duration: 0 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 19 MB
Node.jsのサンプルアプリケーション

他にも例えばNode.jsのサンプルアプリケーションを作成する場合も--runtimeの指定を変えるだけでよい。

$ sam init --runtime nodejs --name sam-nodejs
[+] Initializing project structure...

Project generated: ./sam-nodejs

Steps you can take next within the project folder
===================================================
[*] Invoke Function: sam local invoke HelloWorldFunction --event event.json
[*] Start API Gateway locally: sam local start-api

Read sam-nodejs/README.md for further instructions

[*] Project initialization is now complete

$ tree sam-nodejs
sam-nodejs
├── README.md
├── events
│   └── event.json ................. Lambdaに渡すイベントの定義
├── hello-world
│   ├── app.js ..................... Lambda関数本体
│   ├── package.json
│   └── tests
│       └── unit
│           └── test-handler.js
└── template.yaml .................. SAMテンプレート

4 directories, 6 files

Lambdaの実行方法は先のPythonの時と同様なので割愛する。

AWS上にデプロイ

AWS上にデプロイ

ローカルで動作確認できたのでCloudFormationでAWS上にデプロイする。

まずテンプレートの正当性を確認する。

$ sam validate
2019-10-21 01:14:36 Found credentials in shared credentials file: ~/.aws/credentials
/home/xxxxx/sam-python/template.yaml is a valid SAM Template

問題がなければ上記のように「valid」になるのでS3バケットにアップロードする。ここではバケット名を「l-w-i.lambda」とする。

$ aws s3 mb s3://l-w-i.lambda

$ sam package --template-file template.yaml --s3-bucket l-w-i.lambda --output-template-file packaged.yaml
Uploading to c0e3fc815ba0ece850ad243e5994a832  1897 / 1897.0  (100.00%)
Successfully packaged artifacts and wrote output template to file packaged.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /home/<user>/sam-python/packaged.yaml --stack-name <YOUR STACK NAME>

アップロードが終わったらデプロイを行う。

$ sam deploy --template-file packaged.yaml --stack-name sam-python --capabilities CAPABILITY_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - sam-python

これによりCloudFormationのスタックが作成され、Lambda関数がデプロイされる。

$ aws cloudformation describe-stacks --stack-name sam-python --query 'Stacks[].Outputs[1]'
[
    {
        "OutputKey": "HelloWorldApi",
        "OutputValue": "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/",
        "Description": "API Gateway endpoint URL for Prod stage for Hello World function"
    }
]

API Gatewayのエンドポイントも作成されるのでcurlでアクセスしてみる。

$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
{"message": "hello world"}

Lambdaが実行され、CloudWatch Logsにも実行ログが記録される。

デプロイしたLambda関数を削除する場合はCloudFormationのスタックを削除する。S3バケットとCloudWatch Logsのロググループは削除されないので別途削除する。

$ aws cloudformation delete-stack --stack-name sam-python

参考サイト

2018/12/30更新

対応バージョン: 1.10.33

S3にアクセスする際の署名バージョンには2と4があるが、AWS CLIの古いバージョンで署名バージョン4のみサポートするリージョンのS3バケットにアクセスすると以下のようなエラーになる。(この例ではフランクフルトリージョンのS3バケットにアクセス)

% aws --version
aws-cli/1.10.33 Python/2.7.14 Linux/4.14.72-68.55.amzn1.x86_64 botocore/1.4.23

% aws s3 cp test.txt s3://sample-eu-central-1/
upload failed: ./test.txt to s3://sample-eu-central-1/test.txt A
client error (PermanentRedirect) occurred when calling the PutObject
operation: The bucket you are attempting to access must be addressed
using the specified endpoint. Please send all future requests to this
endpoint: sample-eu-central-1.s3.eu-central-1.amazonaws.com

You can fix this issue by explicitly providing the correct region
location using the --region argument, the AWS_DEFAULT_REGION
environment variable, or the region variable in the AWS CLI
configuration file.  You can get the bucket's location by running "aws
s3api get-bucket-location --bucket BUCKET".

% aws s3api get-bucket-location --bucket sample-eu-central-1
{
    "LocationConstraint": "eu-central-1"
}

これは古いバージョンのAWS CLIがデフォルトで署名バージョン2でS3にアクセスするために起こる事象なので、CLI実行時にリージョンを明示的に指定するか、AWS CLIをアップデートすれば起こらなくなる。

CLI実行時にリージョンを指定する場合
% aws --region eu-central-1 s3 cp test.txt s3://sample-eu-central-1/
upload: ./test.txt to s3://sample-eu-central-1/test.txt
AWS CLIをアップデートする場合
% pip install --upgrade awscli

% aws --version
aws-cli/1.16.81 Python/2.7.14 Linux/4.14.72-68.55.amzn1.x86_64 botocore/1.12.71

% aws s3 cp test.txt s3://sample-eu-central-1/
upload: ./test.txt to s3://sample-eu-central-1/test.txt

参考サイト

2019/06/08更新

対応バージョン: 1.11.117

AWS CLIでLambda関数のソースコードをダウンロードする手順を示す。

まずget-functionコマンドにてLambdaのソースコードが収められたURL("Location": ...)とともにLambda関数の各種設定情報を取得する。

$ aws lambda get-function --function-name <Lambda関数名>
{
    "Code": {
        "RepositoryType": "S3",
        "Location": "https://awslambda-..."
    },
:

次にこのURLに対してcurl等でコンテンツを取り出す。以下のようにすればzip圧縮されたソースコード一式をダウンロードできる。

$ func="<Lambda関数名>"

$ url=$(aws lambda get-function --function-name ${func} | jq -r '.Code.Location')

$ curl -o lambda.zip $url

尚、Lambda関数がバージョン管理されている場合、最新のバージョンをダウンロードするのであれば上記の手順でよいが、特定のバージョンのソースコードをダウンロードするにはまずlist-versions-by-functionコマンドにて保存されているバージョンを確認した後、取得したいバージョンを--qualifierオプションで指定してget-functionコマンドを実行すればよい。

$ aws lambda list-versions-by-function --function-name <Lambda関数名>
{
    "Versions": [
        {
:
            "Version": "$LATEST",
:
            "LastModified": "2019-04-24T05:24:34.064+0000",
:
        },
        {
:            
            "Version": "1",
:
            "LastModified": "2019-03-14T02:08:41.267+0000",
:
        },
        {    
:            
            "Version": "2",
:
            "LastModified": "2019-03-12T02:18:20.732+0000",
        }
:
    ]
}

$ aws lambda get-function --function-name <Lambda関数名> --qualifier <バージョン番号>

2019/10/11更新

対応バージョン: ruby-mqtt 0.5.0

AWS IoTで証明書を作成し、これを使ってRubyのMQTTクライアントからAWS IoTに接続しようとするとエラーになる。

コード
$ vi mqtt.rb
require 'rubygems'
require 'mqtt'

client = MQTT::Client.connect(
           host: 'xxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com',
           port: 8883,
           ssl: true,
           cert_file: '<AWS IoTで作成した「このモノの証明書」>',
           key_file: '<AWS IoTで作成した「プライベートキー」>',
           ca_file: '<AWS IoTの「ルートCA ダウンロード」のページから取得したルートCA証明書>')
:
実行結果
$ ruby mqtt.rb
:
SSL_connect returned=1 errno=0 state=error: certificate verify failed (unable to get local issuer certificate) (OpenSSL::SSL::SSLError)

これは既にレガシー扱いになったVeriSign発行のルートCA証明書を使っているからであり、Amazon Trust Services発行のルートCA証明書を使えば問題なく動作する。

関連資料・記事

参考サイト

2017/06/11更新

以下の記事でAWS IoTを使ったセンシングデータのSNS通知手順を示したが、AWS IoTの設定をGUI(マネジメントコンソール)でなくCLIで行う手順を示す。AWS IoT以外の設定は変わらないので割愛する。

関連資料・記事

Amazon SNS

Topic作成
% aws sns create-topic --name "mqtt_test"
{
    "TopicArn": "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:mqtt_test"
}
通知先(Email)設定
% aws sns subscribe --topic-arn "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:mqtt_test" --protocol email --notification-endpoint "xxx@yyy.com"
{
    "SubscriptionArn": "pending confirmation"
}

確認用のメールが届くので「Confirm subscription」をクリックしてこのメールアドレスを有効にするか、「Confirm subscription」のURLからToken="******"部分を抜き出してCLIで有効化する。

% aws sns confirm-subscription --topic-arn "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:mqtt_test" --token "*******************************************"
{
    "SubscriptionArn": "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:mqtt_test:************************************"
}

AWS IoT

ArduinoをThingとして登録
aws iot create-thing --thing-name "Arduino"
{
    "thingArn": "arn:aws:iot:ap-northeast-1:xxxxxxxxxxxx:thing/Arduino",
    "thingName": "Arduino"
}
センシングデータを処理するルールを設定

最初にルールにひも付けるIAMロールを作成する。

% vi role.json
{
  "Version":"2012-10-17",
  "Statement":[{
    "Effect": "Allow",
    "Principal": {
      "Service": "iot.amazonaws.com"
    },
    "Action": "sts:AssumeRole"
  }]
}

% aws iam create-role --role-name "mqtt_test_role" --assume-role-policy-document file://role.json
{
    "Role": {
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": "sts:AssumeRole",
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "iot.amazonaws.com"
                    }
                }
            ]
        },
        "RoleId": "*********************",
        "CreateDate": "2017-06-10T04:29:40.209Z",
        "RoleName": "mqtt_test_role",
        "Path": "/",
        "Arn": "arn:aws:iam::xxxxxxxxxxxx:role/mqtt_test_role"
    }
}

ロールが作成できたらルールを作成してひも付ける。

% vi rule.json
{
  "sql": "SELECT * FROM 'topic/test'",
  "ruleDisabled": false,
  "actions": [{
    "sns": {
      "targetArn": "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:mqtt_test",
      "roleArn": "arn:aws:iam::xxxxxxxxxxxx:role/mqtt_test_role"
    }
  }]
}

% aws iot create-topic-rule --rule-name "sendEmail" --topic-rule-payload file://rule.json
ポリシーを作成
% vi policy.json
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action": ["iot:*"],
        "Resource": ["*"]
    }]
}

% aws iot create-policy --policy-name "mqtt_policy" --policy-document file://policy.json
{
    "policyName": "mqtt_policy",
    "policyArn": "arn:aws:iot:ap-northeast-1:xxxxxxxxxxxx:policy/mqtt_policy",
    "policyDocument": "{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [{\n        \"Effect\": \"Allow\",\n        \"Action\": [\"iot:*\"],\n        \"Resource\": [\"*\"]\n    }]\n}\n",
    "policyVersionId": "1"
}
デバイス証明書の作成と有効化
% aws iot create-keys-and-certificate --set-as-active --certificate-pem-outfile cert.pem --private-key-outfile thing-private-key.pem
{
    "certificateArn": "arn:aws:iot:ap-northeast-1:xxxxxxxxxxxx:cert/********************************",
    "certificatePem": "-----BEGIN CERTIFICATE-----\n******\n-----END CERTIFICATE-----\n",
    "keyPair": {
        "PublicKey": "-----BEGIN PUBLIC KEY-----\n******\n-----END PUBLIC KEY-----\n",
        "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\n******\n-----END RSA PRIVATE KEY-----\n"
    },
    "certificateId": "********************************************************"
}
ポリシーおよびThingをデバイス証明書にアタッチ
% aws iot attach-principal-policy --policy-name "mqtt_policy" --principal "arn:aws:iot:ap-northeast-1:xxxxxxxxxxxx:cert/****************************************************************"

% aws iot attach-thing-principal --thing-name "Arduino" --principal "arn:aws:iot:ap-northeast-1:xxxxxxxxxxxx:cert/****************************************************************"

参考サイト

2017/06/09更新

AWS IoTでMQTTブローカーを作り、ArduinoでセンシングしたデータをPub/Sub通信してAmazon SNSでメール通知する手順を示す。

データの流れは以下の通り。

作業の流れは以下のようになる。

Amazon SNS

Topic作成
通知先(Email)設定

AWS IoT

ArduinoをThingとして登録
センシングデータを処理するルールを設定
ポリシーを作成
デバイス証明書の作成と有効化
ポリシーおよびThingをデバイス証明書にアタッチ

ここではAWS IoTの設定はGUI(マネジメントコンソール)を使う。CLIを使った場合の手順は以下に示す。

関連資料・記事

Arduino

センシング用の回路を作成
Arduinoでセンシングを行いAWS IoT上のMQTTブローカーにPublishするプログラムを用意

テスト

尚ここで使用するArduinoはUNOとするが、直接IP通信ができないため母艦としてLinux(Ubuntu 16.04)を使用し、Pub/Sub通信やセンシングを行うプログラムはRubyで作成する。

Amazon SNS

まずセンサーデータを通知する通知先を作成する。手順としては最初にTopicを作成してそれに実際のメールアドレスをひも付ける。

Topic作成

Amazon SNSのダッシュボードから[Topics] > [Create new topic]をクリックし、Topic nameに任意の名前を付けてTopicを作成する。(ここでは「mqtt_test」とする)

メールアドレスのひも付け

作成したTopicにチェックを付け[Actions] > [Subscribe to topic]をクリックする。

Protocolを「Email」としてEndpointに通知先のメールアドレスを指定する。

確認用のメールが届くので「Confirm subscription」をクリックしてこのメールアドレスを有効にする。

ダッシュボードの[Subscriptions]をクリックすると通知先が作成されているのが確認できる。

AWS IoT

通知先が作成できたらAWS IoTの設定に移る。

流れとしてはまずAWS IoT上にArduinoをThingとして登録し、次にセンシングデータを処理するルールを設定したうえでデバイス証明書とポリシーを作成してArduinoにひも付ける。

Thing作成

AWS IoTのダッシュボードから[Registry] > [Things]をクリックして[Create]をクリックする。

Thingに任意の名前を付けて[Create thing]をクリックする。(ここでは「Arduino」とする)

センシングデータを処理するルールを設定

[Rules]をクリックして[Create]をクリックする。

ルールに任意の名前を付け(ここでは「sendEmail」とする)、Message source欄のAttributeに取得対象として全てのデータを表す「*」を、Topic filterに購読するトピック(ここでは「topic/test」とする)をそれぞれ指定して[Add action]をクリックする。

アクションを選択する画面が表示されるので「Send a message as an SNS push notification」を選択し[Configure action]をクリックする。

「SNS target」で先ほど作成したTopic(ここでは「mqtt_test」)を選択し、「IAM role name」で[Create a new role]をクリックする。

IAMロール名を入力できるようになるので任意の名前を付け(ここでは「mqtt_test_role」とする)、[Create a new role]をクリックする。

「Configure action」画面に戻るので[Add action]をクリックする。

「Create a rule」画面で[Create rule]をクリックするとアクションが作成され、ルールの作成が完了する。

ポリシー作成

[Security] > [policies]をクリックし、[Create]をクリックする。

Nameに任意のポリシー名を入力し(ここでは「mqtt_policy」とする)、Thingからの操作を全て許可することとして[Action]に「iot:*」、[Resource ARN]に「*」を入力して[Effect]の[Allow]をチェックして[Create]をクリックする。これでポリシーが作成される。

デバイス証明書作成

[Security] > [Certificates]をクリックし、[Create]をクリックする。

一番上の[Create certificate]をクリックする。

証明書と鍵ペアが作成されるのでこのうち証明書(A certificate for this thing: xxxxx.cert.pem)と秘密鍵(A private key: xxxxx.private.key)をダウンロードして[Activate]をクリックする。これで証明書が有効になる。

ルート証明書も必要になるので入手しておく。この作業はAWS IoTのコンソールでできないのでコマンドで実行する。

% wget https://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem -O rootCA.pem

ポリシーおよびThingをデバイス証明書にアタッチ

[Security] > [Certificates]をクリックし、先ほど作成した証明書の「・・・」をクリックして[Attach policy]を選択する。

作成済ポリシーをチェックして[Attach]をクリックする。

同様に先ほど作成した証明書の「・・・」をクリックして[Attach thing]を選択する。

作成済Thingをチェックして[Attach]をクリックする。

Arduino

AWS IoTの設定が終わったら残りはArduino側の設定となる。

Arduino IDEを動かす母艦はUbuntu 16.04とする。

最初にArduinoにセンサーを接続してセンシングを行う回路を作成する。ここでは温度センサーを使って室温を計測することとする。

またArduinoの母艦となるOS(ここではUbuntu 16.04)上にAWS IoTのMQTTブローカーに対してSubscribeを行うプログラムとセンサーからのデータを取得してPublishするプログラムをそれぞれ用意する。言語はここではRubyを使用する。

センシング用の回路を作成

温度センサーとして精度はあまり高くないものの安価に調達できるLM61BIZを使用する。

センサーのピンは3つで刻印面に向かって左から

+Vs(電源電圧)
Vout(出力電圧)
GND(グランド)

という配列になっているのでこれをArduinoの5V、A0、GNDにそれぞれ接続する。

結線イメージは以下のようになる。

関連資料・記事

Arduinoでセンシングを行いAWS IoT上のMQTTブローカーにPublishするプログラムを用意

まずRubyからArduinoを操作するためのarduino_firmataパッケージと、MQTTのPub/Sub通信を行うためのmqttパッケージをインストールする。

% sudo gem install arduino_firmata
% sudo gem install mqtt

次に温度センサーからの値を1秒おきに取得して値を温度に変換したうえで前回取得値と異なる場合はAWS IoT上のMQTTブローカーにPublishするプログラムを用意する。

% vi pub.rb
require 'rubygems'
require 'arduino_firmata'
require 'mqtt'

arduino = ArduinoFirmata.connect "/dev/ttyACM0"

client = MQTT::Client.connect(
           host: '<Thing ARN>',
           port: 8883,
           ssl: true,
           cert_file: '<証明書ファイル>',
           key_file: '<秘密鍵ファイル>',
           ca_file: 'rootCA.pem')

before_temp = 0

while true
  temp = arduino.analog_read 0
  temp = ((temp.to_f * 5 / 1024) - 0.6) * 100
  temp = temp.round
  if before_temp != temp
    puts temp
    before_temp = temp
    client.publish('topic/test', "temp = #{temp}")
  end
  sleep 1
end

arduino.close

関連資料・記事

テスト

上記で用意したプログラムを実行すると、温度が変わる度にAWS IoT上のMQTTブローカーに温度が送信(Publish)される。

% ruby pub.rb
22
22
22
23 <- 温度センサーに手を触れて温度を変化させる
23
23

AWS IoTはこのデータを受け取るとルール設定に従ってAmazon SNS経由でメール通知を行う。通知先には以下のようなメールが届く。

今回はデバイスから取得したデータをアウトプットするまでの一連の動きを見るためにシンプルな実装を行ったが、AWS IoTでは他にも目的に応じて様々な制御が可能である。

参考サイト

2019/10/22更新

Amazon FreeRTOS(以下FreeRTOS)は様々なデバイスにインストールすることができエッジ側にAWSの環境を拡張する手段を提供しているが、ここでは手に入りやすいEspressif ESP32-DevKitC(以下ESP32)にFreeRTOSをインストールする手順を示す。

動作環境

FreeRTOSを使うにあたりあらかじめ以下をインストールしておく。

Pytyoh 2.7.10以降 (3.xでよいが、2.xを使う場面あり)
pip
AWS SDK for Python (boto3)
AWS CLI
$ python --version
Python 3.6.8

$ python2 --version
Python 2.7.15+

$ pip --version
pip 19.3.1 from /usr/local/lib/python3.6/dist-packages/pip (python 3.6)

$ pip list | grep boto3
boto3               1.9.253  

$ aws --version
aws-cli/1.16.263 Python/3.6.8 Linux/4.15.0-65-generic botocore/1.12.253

ツールチェーンインストール

まずEspressif公式サイトからUbuntu用のツールチェーンをダウンロードしてインストールする。

以下をダウンロード & インストール

xtensa-esp32-elf-linux64-1.22.0-80-g6c4433a-5.2.0.tar.gz

$ mkdir ~/esp
$ cd ~/esp
$ tar zxvf ~/Downloads/xtensa-esp32-elf-linux64-1.22.0-80-g6c4433a-5.2.0.tar.gz
環境変数設定
export PATH="$PATH:$HOME/esp/xtensa-esp32-elf/bin"
バージョン確認
$ xtensa-esp32-elf-gcc --version
xtensa-esp32-elf-gcc (crosstool-NG crosstool-ng-1.22.0-80-g6c4433a) 5.2.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

CMakeインストール

次にFreeRTOSビルド用のCMakeをインストールする。

Amazon FreeRTOSではCMake 3.13以降がサポートされるが、Ubuntuのパッケージリポジトリには3.10しか用意されていないためCMake公式サイトからダウンロードしてインストールする。

以下をダウンロード & インストール

cmake-3.15.4-Linux-x86_64.tar.gz

$ mkdir ~/tmp
$ cd ~/tmp
$ tar zxvf ~/Downloads/cmake-3.15.4-Linux-x86_64.tar.gz
$ sudo cp -r cmake-3.15.4-Linux-x86_64/{bin,doc,share} /usr/local

manファイルについては格納場所/usr/local/manが/usr/local/share/manへのシンボリックリンクのためmanファイルのみ以下のようにしてコピーする。

$ sudo cp -r cmake-3.15.4-Linux-x86_64/man /usr/local/share
バージョン確認
$ cmake --version
cmake version 3.15.4

Amazon FreeRTOSダウンロード & 設定

ビルドの準備が整ったらAmazon FreeRTOS公式GitHubからソース一式をダウンロードする。

$ cd ~/tmp
$ git clone https://github.com/aws/amazon-freertos.git --recurse-submodules

次にセットアップスクリプト用の設定ファイルを編集する。

$ cd amazon-freertos/tools/aws_config_quick_start
$ vi configure.json
{
    "afr_source_dir":"../..", <----------- 上記ダウンロードディレクトリ(amazon-freertosディレクトリのフルパス)
    "thing_name":"$thing_name", <--------- ESP32に付けるThing Name
    "wifi_ssid":"$wifi_ssid", <----------- Wi-FiネットワークのSSID
    "wifi_password":"$wifi_password", <--- Wi-Fiネットワークの接続パスワード
    "wifi_security":"$wifi_security" <---- Wi-Fiネットワークのセキュリティタイプ
}

設定ファイルが編集できたらセットアップスクリプトを実行する。

$ python SetupAWS.py setup

これによりAWS IoT上のリソースが自動的に作られ、リソース間の関連付けも行われる。

モノ(Thing)作成
証明書作成
ポリシー作成
証明書にポリシーをアタッチ
モノに証明書をアタッチ
証明書関連のファイル作成
$ ls -l ESP32_01_*
-r--r--r-- 1 xxx xxx   64 10月 22 08:46 ESP32_01_cert_id_file <----------- 証明書ID
-r--r--r-- 1 xxx xxx 1220 10月 22 08:46 ESP32_01_cert_pem_file <---------- 公開鍵
-r--r--r-- 1 xxx xxx 1675 10月 22 08:46 ESP32_01_private_key_pem_file <--- 秘密鍵
MQTTエンドポイント、Wi-Fiネットワーク情報をデモ用aws_clientcredential.hファイルに追加
$ cat -n ../../demos/include/aws_clientcredential.h
:
    35	/*
    36	 * MQTT Broker endpoint.
    37	 */
    38	#define clientcredentialMQTT_BROKER_ENDPOINT "xxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com"
    39	
    40	
    41	/* Use of a "define" and not a "static const" here to be able to
    42	* use pre-compile concatenation on the string. */
    43	#define clientcredentialIOT_THING_NAME "ESP32_01"
:
    56	/*
    57	 * Wi-Fi network to join.
    58	 */
    59	#define clientcredentialWIFI_SSID       "xxxxxxxxxxxxxxxx"
    60	
    61	/*
    62	 * Password needed to join Wi-Fi network.
    63	 */
    64	#define clientcredentialWIFI_PASSWORD   "xxxxxxxxxxxxx"
    65	
    66	/**
    67	 * @brief Security type
    68	 * WPA2 Security, @see WIFISecurity_t
    69	 * Possible values are - eWiFiSecurityOpen, eWiFiSecurityWEP, eWiFiSecurityWPA,
    70	 * eWiFiSecurityWPA2
    71	 */
    72	#define clientcredentialWIFI_SECURITY   xxxxxxxxxxxxxxxxx
    73	
    74	#endif
証明書とプライベートキーをBase64エンコードしてデモ用aws_clientcredential_keys.hファイルに追加
$ cat -n ../../demos/include/aws_clientcredential_keys.h
:
    29	/*
    30	 * PEM-encoded client certificate
    31	 *
    32	 * Must include the PEM header and footer:
    33	 * "-----BEGIN CERTIFICATE-----\n"\
    34	 * "...base64 data...\n"\
    35	 * "-----END CERTIFICATE-----\n"
    36	 */
    37	#define keyCLIENT_CERTIFICATE_PEM \
    38	"-----BEGIN CERTIFICATE-----\n"\
:
    57	"-----END CERTIFICATE-----\n"
    77	/*
    78	 * PEM-encoded client private key.
    79	 *
    80	 * Must include the PEM header and footer:
    81	 * "-----BEGIN RSA PRIVATE KEY-----\n"\
    82	 * "...base64 data...\n"\
    83	 * "-----END RSA PRIVATE KEY-----\n"
    84	 */
    85	#define keyCLIENT_PRIVATE_KEY_PEM \
    86	"-----BEGIN RSA PRIVATE KEY-----\n"\
:
   112	"-----END RSA PRIVATE KEY-----\n"
:

FreeRTOSデモプロジェクトビルド & ESP32書き込み & 実行

設定が終わったのでCMakeでFreeRTOSをビルドし、ESP32に書き込んで実行する。

ビルド
$ cd ../..
$ cmake -DVENDOR=espressif -DBOARD=esp32_devkitc -DCOMPILER=xtensa-esp32 -S . -B build
$ cd build
$ make all -j4
:
[100%] Built target app
ESP32書き込み

ここでESP32をホストPCに接続し、まず以下を実行してESP32の中身を消去する。

$ cd ..
$ ./vendors/espressif/esp-idf/tools/idf.py erase_flash -B build
ESP-IDF currently only supports Python 2.7, and this is Python 3.6.8. Search for 'Setting the Python Interpreter' in the ESP-IDF docs for some tips to handle this.

ただ当該環境ではPython3がデフォルトなので上記のエラーになる。

idf.pyを書き換えてPython2を明示的に指定して再度実行する。

$ vi ./vendors/espressif/esp-idf/tools/idf.py
(変更前)
#!/usr/bin/env python

(変更後)
#!/usr/bin/env python2

$ ./vendors/espressif/esp-idf/tools/idf.py erase_flash -B build
Setting IDF_PATH environment variable: ...
:
Chip erase completed successfully in 3.8s
Hard resetting via RTS pin...
Done

続いてFreeRTOSをESP32に書き込む。

$ cd build
$ make flash
[  0%] Built target blank_ota_data
[  1%] Built target partition_table
[  1%] Built target idf_component_ulp
:
Wrote 953520 bytes (585067 compressed) at 0x00020000 in 14.5 seconds (effective 526.8 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...
[100%] Built target flash
AWS上のMQTTメッセージのモニタリング

AWS IoTコンソールでMQTTクライアントを使ってESP32がAWSに送信するメッセージをサブスクライブしておく。

[テスト]をクリックするとMQTTクライアントが開くので[トピックのサブスクリプション]フィールドに「iotdemo/#」と入力して[トピックへのサブスクラ...]をクリックする。

いまサブスクライブしたトピックの[iotdemo/#]をクリックしてメッセージを待ち受ける。

デモプログラム実行

この状態でデモプログラムを実行するとMQTTでパブリッシュが行われ、AWS IoTコンソール上でもメッセージが受信できていることが確認できる。

$ cd ..
$ ./vendors/espressif/esp-idf/tools/idf.py monitor -p /dev/ttyUSB0 -B build
:
157 914 [iot_thread] [INFO ][DEMO][9140] Demo completed successfully.
158 918 [iot_thread] [INFO ][INIT][9180] SDK cleanup done.
159 918 [iot_thread] [INFO ][DEMO][9180] -------DEMO FINISHED-------
-> Ctrl + ]で終了

参考サイト

2024/05/31更新

ALBのAnomalousHostCountメトリクスはターゲットホストからの異常応答(HTTP 5XXなど)が他のターゲットよりも多い場合の対象ターゲット数を表すが、これを簡単に出す環境を作る。(AnomalousHostCountをトリガーにアラームを設定する場合などにそのテスト環境として役に立つのではないかと)

環境作成とテスト

ALBにぶら下げるターゲットはAWSがAnomalousHostを判断するために3台以上必要なので、まず以下のEC2インスタンスを3台作成する。

Amazon Linux 2023 + nginx(インストールのみ)

次にこのうち1台だけnginxに流量制御を設定する。

$ sudo vi /etc/nginx/nginx.conf
:
http {
    limit_req_zone $binary_remote_addr zone=req:1m rate=1r/s;
    limit_req zone=req;
    :
}

$ sudo systemctl restart nginx

limit_req_zoneで流量制御のゾーン定義をし、limit_reqでそのゾーンを有効化する。

$binary_remote_addr

リモートアドレス毎にバッファを用意

zone=req:1m

バッファに「req」というゾーン名を付け、バッファ容量は1MBとする

rate=1r/s

1秒間に1リクエストまでの受け入れを許可

その他パラメータの詳細は公式ドキュメントを参照のこと。

この状態でALBを作成して上記3台のEC2をぶら下げ、ヘルスチェックの失敗回数(非正常のしきい値)を2から10に増やしてターゲットホストがUnHealthyになる条件を緩めた上で外部から例えば0.2秒毎にリクエストを送るとAnomalousHostCountメトリクスが出る。

$ while [ 1 ]
do
curl http://<ALB>のURL/
sleep 0.2
done

なお、流量制御を設定したnginxのログには以下のような内容が記録される。

/var/log/nginx/access.log
xx.xx.xx.xx - - [DD/MMM/YYYY:hh:mm:ss +0900] "GET / HTTP/1.1" 503 3693 "-" "curl/x.x.x" "xx.xx.xx.xx"
/var/log/nginx/error.log
YYYY/MM/DD hh:mm:ss [error] 2187#2187: *1774 limiting requests, excess: 0.195 by zone "req", client: xx.xx.xx.xx, server: _, request: "GET / HTTP/1.1", host: "xxxxxxxx.ap-northeast-1.elb.amazonaws.com"

参考ドキュメント

2019/12/24更新

対応バージョン: Amazon Linux 2

Amazon Linux 2上でcurlで任意のWebサイトにアクセスしようとすると以下のようなエラーが出ることがある。
$ curl https://foo.bar.com/
:
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

これはローカルに保存されているルート証明書の中にアクセス先サイトのSSL証明書を証明する者(issuer)がいないことが原因なので、証明者をシステムに組み込んであげればよい。

ここではアクセス先サイトのSSL証明書がLet's Encrypt発行のものだった場合の対応手順を示す。

まずSSL証明書の内容を調べる
$ openssl s_client -connect foo.bar.com:443 < /dev/null 2> /dev/null | openssl x509 -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            04:e3:91:24:eb:0e:28:a7:9e:e7:84:ac:b4:65:8d:4d:b9:c7
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
        Validity
            Not Before: Oct 26 13:34:11 2019 GMT
            Not After : Jan 24 13:34:11 2020 GMT
:

IssuerがLet's Encryptであることが確認できる。

次に以下からクロス署名されたLet’s Encryptの中間証明書を取得する

手順は以下の通り。

$ curl https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt \
-o /tmp/lets-encrypt-x3-cross-signed.pem

$ openssl verify /tmp/lets-encrypt-x3-cross-signed.pem
/tmp/lets-encrypt-x3-cross-signed.pem: OK
この中間証明書をシステムに組み込む

/etc/pki/ca-trust/extracted配下のファイルが更新されるので念のためバックアップを取っておく。

$ cd /etc/pki/ca-trust/extracted
$ sudo tar cvf crt.tar *

証明書をシステムに組み込む。

$ sudo cp /tmp/lets-encrypt-x3-cross-signed.pem /etc/pki/ca-trust/source/anchors
$ sudo update-ca-trust extract

これでcurlでエラーにならずにコンテンツが取得できるようになる。

$ curl https://foo.bar.com/
<!DOCTYPE html>
<html>
:

Ubuntuの場合

ちなみにUbuntu 18.04の場合は以下のようにすれば同様の対応が可能である。

$ sudo cp -p /etc/ssl/certs/ca-certificates.crt{,.org}

$ curl https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt \
-o /tmp/lets-encrypt-x3-cross-signed.pem

$ sudo sh -c "cat /tmp/lets-encrypt-x3-cross-signed.pem >> /etc/ssl/certs/ca-certificates.crt"

$ openssl verify /etc/ssl/certs/ca-certificates.crt
/etc/ssl/certs/ca-certificates.crt: OK

参考サイト

2024/11/07更新

対応バージョン: Amazon Linux 2

EC2のメトリクス値に異常が見受けられる場合、EC2自体(OSの挙動)に問題がなくても(AWSが自動で)取得しているメトリクスのサンプル数が普段と違うことにより異常値を示している場合がある。

以下はいくつかのメトリクスのサンプル数が普段と違う場合にCPUUtilizationとNetworkInが異常値を示す例である。(04:00〜23:00の時間帯)

通常サンプル数は一定であるため、サンプル数が普段と違う(上記の場合は1分あたりのサンプル数が5から10に倍増している)時点でAWS側になんらかの原因があると考えられ、自然に元に戻る場合もあるがEC2インスタンスを再起動(停止・起動)してEC2インスタンスが動作するホストを入れ替えると解消する可能性がある。(上記は自然に元に戻ったパターン)

次にCPUUtilizationとNetworkInの値を見てみると、状況的にはこの2つの値は本来一定であるはずだが、まず平均値を見ると以下のことがわかる。

CPUUtilization

50%前後を行き来している

NetworkIn

半分に下がっている

さらに最大値・最小値を見ると以下のことが分かり、通常ではあり得ないパターンを示している。

CPUUtilization

最大は100%に張り付き、最小は0%と3%を行き来している

NetworkIn

最大は普段と変わらないにもかかわらず、最小が0のまま

OS内部のプロセス動作状況やsyslog・sar情報などを確認しても上記のメトリクス値になる要素がないため、AWS側の問題であることがわかる。

2023/12/29更新

対応バージョン: Amazon Linux 2

今までIPv4でSSH接続していたEC2にIPv6で接続するための最小限を設定をまとめる。(IPv4を無効にする設定は行わない)

EC2はVPC内のサブネット内に配置するのでその両方の変更が必要になる。

なお、ここではパブリックサブネットに配置しているEC2を対象とする。(踏み台ホストへの接続を想定)

VPC

対象のVPCを選択して[アクション] > [CIDRの編集]をクリックする
「IPv6 CIDR」の[新しいIPv6 CIDRを追加]をクリックする
[Amazon提供のIPv6 CIDRブロック]を選択して[CIDRを選択]をクリックする

(結果) プレフィックス長 /56 のIPv6アドレスが割り当てられる

ルートテーブル

対象のVPCに紐付けているメインルートテーブルを選択して「ルート」の[ルートを編集]をクリックする
前述で割り当てられたIPv6アドレスのターゲットをインターネットゲートウェイに割り当てる

サブネット

対象のサブネットを選択して[アクション] > [IPv6 CIDRの編集]をクリックする
「サブネットのCIDRブロック」の[IPv6 CIDRを追加]をクリックする
「サブネットのCIDRブロック」の「Subnet CIDR block」を指定して[保存]をクリックする。通常一つのVPCの中に複数のサブネットを配置するので、プレフィックス長 /64 などで小分けにする
対象のサブネットを選択して[アクション] > [サブネットの設定を編集]をクリックする
「IPv6アドレスの自動割り当て設定」の[IPv6アドレスの自動割り当てを有効にする]をチェックして[保存]をクリックする

EC2

EC2は既存のインスタンスにIPv6アドレスを付与することができないため、一度AMIを取得してそのAMIから新規にEC2を作成する

(結果) IPv6アドレスが割り当てられる

セキュリティグループ

対象のEC2に割り当てているセキュリティグループの「インバウンドルール」にIPv6でSSH接続する接続元のIPv6アドレスを設定する

IPv6でSSH接続

これでAWS側の準備ができたのでIPv6でSSH接続する。

$ ssh -6 -i <pemファイル> ec2-user@<接続先EC2のIPv6アドレス>
Last login: xxx xxx xx xx:xx:xx xxxx from xxxx:xx:xxxx:xxxx:xxxx:xxxx:xxx:xxxx

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
[ec2-user@ip-xx-xx-xx-xx ~]$ 

2022/07/27更新

対応バージョン: Amazon Linux 1

Amazon Linux 1のOSを再起動すると特にsshd関連の設定変更をしたわけでもないのに/etc/pam.d/sshdが更新される。
$ uname -r
4.14.285-147.501.amzn1.x86_64

これはsshdの起動スクリプト(/etc/rc3.d/S55sshd)に以下のような処理が書かれているためである。

80行目でhandle_pam_broken_kernel()を呼び出し、同関数の中で「すでに/etc/pam.d/sshdが存在していたらsed -i にて/etc/pam.d/sshd自身を更新する」
:
 49 handle_pam_broken_kernel()
 50 {
 51     if [ -f /etc/pam.d/sshd ] ; then
 52         if [[ "$(uname -r)" =~ 3\.10\.34-3[78]\.137\.amzn1 ]] ; then
 53             # problematic kernel, disable pam_loginuid.so
 54             sed -r -i -e 's/^(session.*pam_loginuid.*$)/##sshd_autodisabled##\1/' /etc/pam.d/sshd
 55         else
 56             # should be a valid kernel, undo auto-disablement
 57             sed -r -i -e 's/^##sshd_autodisabled##//' /etc/pam.d/sshd
 58         fi
 59     fi
 60 }
:
 72 start()
 73 {
 74         [ -x $SSHD ] || exit 5
 75         [ -f /etc/ssh/sshd_config ] || exit 6
 76         # Create keys if necessary
 77         /usr/sbin/sshd-keygen
 78 
 79         # workaround a broken kernel build
 80         handle_pam_broken_kernel
 81 
 82         echo -n $"Starting $prog: "
 83         $SSHD $OPTIONS && success || failure
 84         RETVAL=$?
 85         [ $RETVAL -eq 0 ] && touch $lockfile
 86         echo
 87         return $RETVAL
 88 }
:

なおAmazon Linux 2においてはこの挙動は起こらない。

2021/05/28更新

対応バージョン: Amazon Linux 2

Amazon Linux 2上で動作するanacronによるログローテーションのデフォルトの挙動が複雑なので整理する。

また後半ではローテーション設定ファイルで指定可能なオプションを解説する。

なお、ここでは以下のカーネルバージョンを使用する。

$ uname -r
4.14.231-173.361.amzn2.x86_64

ログローテーションの実行タイミング

ログローテーションの実行タイミングは以下の通りである。

1日1回、3時〜22時の間の最初に訪れたxx時6分〜51分(起動がxx時1分 + ランダム遅延時間最大45分 + 固定遅延時間5分)

ログローテーション実行までの詳細な流れ

上記のタイミングを制御するために複数の設定ファイルにより以下のような流れでログローテーションが実行される。

/etc/cron.d/0hourlyの記述に従い毎時1分に/etc/cron.hourlyディレクトリ配下のスクリプトが起動する
$ ls -l /etc/cron.d
合計 16
-rw-r--r-- 1 root root 128  1月 16  2020 0hourly
-rw-r--r-- 1 root root 108  8月  2  2018 raid-check
-rw------- 1 root root 235 12月 17  2019 sysstat
-rw-r--r-- 1 root root 191 10月 16  2018 update-motd

$ cat /etc/cron.d/0hourly
# Run the hourly jobs
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
01 * * * * root run-parts /etc/cron.hourly
/etc/cron.hourlyディレクトリには0anacronスクリプトが格納されており、anacronが起動する(/var/spool/anacron/cron.dailyの中身は空である)
$ ls -l /etc/cron.hourly
-rwxr-xr-x 1 root root 392  1月 16  2020 0anacron

$ cat /etc/cron.hourly/0anacron
#!/bin/sh
# Check whether 0anacron was run today already
if test -r /var/spool/anacron/cron.daily; then
    day=`cat /var/spool/anacron/cron.daily`
fi
if [ `date +%Y%m%d` = "$day" ]; then
    exit 0;
fi

# Do not run jobs when on battery power
if test -x /usr/bin/on_ac_power; then
    /usr/bin/on_ac_power >/dev/null 2>&1
    if test $? -eq 1; then
    exit 0
    fi
fi
/usr/sbin/anacron -s
anacronの設定ファイルである/etc/anacrontabには以下の内容が記述されており、ログローテーションは/etc/cron.daily/logrotateスクリプトにて実行される

・起動すると最大遅延時間45分が設定される(RANDOM_DELAY=45)

・実行時間は3時〜22時の間(START_HOURS_RANGE=3-22)

・cron.dailyの実行は1日1回(period in daysが1)

・cron.dailyの開始時間はanacron起動から5分後(delay in minutesが5) + 上記RANDOM_DELAY

$ sudo cat /etc/anacrontab
# /etc/anacrontab: configuration file for anacron

# See anacron(8) and anacrontab(5) for details.

SHELL=/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
# the maximal random delay added to the base delay of the jobs
RANDOM_DELAY=45
# the jobs will be started during the following hours only
START_HOURS_RANGE=3-22

#period in days   delay in minutes   job-identifier   command
1	5	cron.daily		nice run-parts /etc/cron.daily
7	25	cron.weekly		nice run-parts /etc/cron.weekly
@monthly 45	cron.monthly		nice run-parts /etc/cron.monthly

$ ls -l /etc/cron.daily
合計 12
-rwx------ 1 root root 219  7月 27  2018 logrotate
-rwxr-xr-x 1 root root 618  4月 29  2019 man-db.cron
-rwx------ 1 root root 208  7月 27  2018 mlocate
/etc/cron.daily/logrotateスクリプトは以下のようになっており、ログローテーションは/etc/logrotate.confの設定に従って実行される
$ sudo cat /etc/cron.daily/logrotate
#!/bin/sh

/usr/sbin/logrotate -s /var/lib/logrotate/logrotate.status /etc/logrotate.conf
EXITVALUE=$?
if [ $EXITVALUE != 0 ]; then
    /usr/bin/logger -t logrotate "ALERT exited abnormally with [$EXITVALUE]"
fi
exit 0
/etc/logrotate.confは以下の内容になっており、ログローテーション全体の設定および以下のファイルのログローテーション設定が記述されている。それ以外のファイルのローテーション設定は/etc/logrotate.d配下に格納されている

・/var/log/wtmp

・/var/log/btmp

$ cat /etc/logrotate.conf
# see "man logrotate" for details
# rotate log files weekly
weekly

# keep 4 weeks worth of backlogs
rotate 4

# create new (empty) log files after rotating old ones
create

# use date as a suffix of the rotated file
dateext

# uncomment this if you want your log files compressed
#compress

# RPM packages drop log rotation information into this directory
include /etc/logrotate.d

# no packages own wtmp and btmp -- we'll rotate them here
/var/log/wtmp {
    monthly
    create 0664 root utmp
	minsize 1M
    rotate 1
}

/var/log/btmp {
    missingok
    monthly
    create 0600 root utmp
    rotate 1
}

# system-specific logs may be also be configured here.

$ ls -l /etc/logrotate.d
合計 20
-rw-r--r-- 1 root root  76  8月  2  2018 bootlog
-rw-r--r-- 1 root root 160  8月 19  2020 chrony
-rw-r--r-- 1 root root 408  8月  2  2018 psacct
-rw-r--r-- 1 root root 224  6月 16  2020 syslog
-rw-r--r-- 1 root root 100  2月 25 18:33 yum

例えばsyslogのログローテーション設定は以下のような内容になっている。

$ cat /etc/logrotate.d/syslog
/var/log/cron
/var/log/maillog
/var/log/messages
/var/log/secure
/var/log/spooler
{
    missingok
    sharedscripts
    postrotate
	/bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true
    endscript
}

これら既存の設定を必要に応じて修正したり、アプリケーションログなど任意のファイルを追加でログローテーションしたい場合はこの/etc/logrotate.d配下に新規にファイルを作成して目的に合った設定を行う。

なお、ログローテーションの状況は対象ファイル毎に/var/lib/logrotate/logrotate.statusに記録され、次回ローテーション実行時に前回実行分との差を確認するために使用される。

$ cat /var/lib/logrotate/logrotate.status
logrotate state -- version 2
"/var/log/yum.log" 2021-5-26-8:0:0
"/var/log/boot.log" 2021-5-26-8:0:0
"/var/log/wtmp" 2021-5-26-8:0:0
"/var/log/chrony/*.log" 2021-5-26-8:0:0
"/var/log/spooler" 2021-5-26-8:0:0
"/var/log/btmp" 2021-5-26-8:0:0
"/var/log/maillog" 2021-5-26-8:0:0
"/var/log/secure" 2021-5-26-8:0:0
"/var/log/messages" 2021-5-26-8:0:0
"/var/log/cron" 2021-5-26-8:0:0
"/var/account/pacct" 2021-5-26-8:0:0

ローテーション設定ファイルで指定可能なオプション

主なオプションをざっと掲載する。

(*)印があるコマンドは先頭に「no」を付けると同オプションと反対の動きをする。(ただしifemptyだけは先頭に「not」が付いてnotifemptyになる)

より詳しくは「man logrotate」を参照のこと。

ローテーション時の挙動

compress (*)

古いログファイルをgzip圧縮する

delaycompress (*)

前のログファイルの圧縮を次のローテーション時に行う

compresscmd

compressオプションを指定した場合に圧縮に使うコマンド(デフォルトはgzip)

uncompresscmd

圧縮ファイルの解凍時に使うコマンド(デフォルトはgunzip)

compressext

圧縮ファイルで使用する拡張子(デフォルトはcompresscmdで指定したコマンドのデフォルト拡張子)

compressoptions

圧縮コマンドに指定するコマンドラインオプション(gzipコマンドの場合、デフォルトは-6(速度を犠牲にして圧縮率を高める))

copy (*)

ログファイルのコピーを作成するが、オリジナルは変更しない

このオプションを使用すると古いログファイルがそのまま残るため、create(ファイル作成時の指定)オプションは効果がなくなる

copytruncate (*)

元のログファイルをコピーして新しいファイルを作成する代わりに、コピーを作成したあと元のログファイルをトランケートする(中身を空にする)

copyオプションと同様、このオプションを使用すると古いログファイルがそのまま残るため、create(ファイル作成時の指定)オプションは効果がなくなる

create <モード> <オーナー> <グループ> あるいは create <オーナー> <グループ> (*)

ログローテーションの直後(ローテーション後のスクリプトが実行される前)にログファイルが作成される

ファイルの属性を省略すると元のログファイルのものが使われる

su <ユーザ> <グループ>

ローテーションされたファイルに設定するユーザとグループを指定する(デフォルトはroot)

olddir <ディレクトリ> (*)

ローテーションしたファイルを格納するディレクトリ

createolddir <モード> <オーナー> <グループ> (*)

olddirオプションで指定したディレクトリが存在しない場合は作成する

dateext (*)

ローテーションしたファイルの末尾に数字を追加するのではなく「YYYYMMDD」のような日付拡張子を追加する

拡張子のフォーマットはdateformatおよびdateyesterdayオプションで指定できる

dateformat <フォーマット文字列>

strftime(3)と同様の表記にてdateextオプションの拡張子を指定する

使用可能な表記は「%Y」「%m」「%d」「%H」「%s」のみである

デフォルトは毎時の場合は「-%Y%m%d%H」、それ以外は「-%Y%m%d」

dateyesterday

今日の日付の代わりに昨日の日付を使用してdateext拡張子を作成し、ローテーションされたログファイルの名前にタイムスタンプと同じ日付が含まれるようにする

extension <拡張子>

<拡張子>で指定した拡張子のついたファイルがログローテーション後も保持されるようにする

圧縮を有効化すると拡張子の後に圧縮の拡張子(通常は.gz)が続くが、例えば「mylog.foo」というファイルをローテーションした場合に「mylog.foo.1.gz」ではなく「mylog.1.foo.gz」いうファイル名になるようにする

ifempty (*)

ログファイルが空でもローテーションを行う

missingok (*)

ログファイルが見つからない場合でもエラーにせずに次のログファイルを処理する

rotate <回数>

指定した回数分ローテーションを行う

<回数>が0の場合、古いファイルはローテーションされるのではなく削除される

start <番号>

ローテーション時のファイル末尾に付与する番号の起点番号

例えば0を指定すると、ログは元のログファイルからローテーションされるため拡張子は「.0」となる

9を指定すると、ログファイルの拡張子は「.9」で作成され、0〜8はスキップされる

ファイルはrotateオプションで指定された回数だけローテーションする

size <サイズ>

ログファイルが指定したサイズより大きくなるとローテーションする

サイズはバイト指定だが、k(KB)、M(MB)、G(GB)などの単位を付けて指定することもできる

サイズの指定方法は他のオプションも同一

maxsize <サイズ>

追加指定された時間間隔(日次、週次、月次、または年次)より前であっても、ログファイルが指定したサイズより大きくなった場合にローテーションする

minsize <サイズ>

ログファイルが指定したサイズより大きくなった場合にローテーションするが、追加指定された時間間隔(日次、週次、月次、または年次)より前にはローテーションしない

maxage <カウント>

<カウント>日よりも古いローテーションされたログを削除する

shred (*)

ログファイル削除時にunlink()ではなくshred -uを使用する(デフォルトは無効)

shredcycles <カウント>

ログファイルを削除する前にshredにログファイルを<カウント>回数上書きするように要求する

このオプションを指定しない場合、shredのデフォルト値が使用される

tabooext [+] <拡張子リスト>

ローテーションから除外される拡張子を指定する

「+」が拡張子リストの前にある場合、現在の拡張子リストが拡張され、「+」がない場合は現在の拡張子リストがここで指定したリストに上書きされる

デフォルトの拡張子リストは以下の通り

「.rpmsave」「.rpmorig」「~」「.disabled」「.dpkg-old」「.dpkg-dist」「.dpkg-new」「.cfsaved」「.ucf-old」「.ucf-dist」「.ucf-new」「.rpmnew」「.swp」「.cfsaved」「.rhn-cfg-tmp-*」

ローテーションタイミング

hourly

ローテーションを毎時行う

ただlogrotateはcronの設定で毎日行うように設定されているため、毎時ローテーションを行いたい場合はcronの設定を毎時実行するように変更する必要がある

daily

ローテーションを毎日行う

monthly

logrotateがその月に初めて実行された時にログローテーションを行う(通常それは月の初日になる)

yearly

ローテーションを毎年行う

weekly <曜日>

指定した曜日に1回ローテーションするか、最後のローテーションから少なくとも7日経ってローテーションする

曜日を表す数字は以下の通りで、特別な指定である7は7日毎を意味する(デフォルトは0)

0(日曜日)、1(月曜日)、2(火曜日)、3(水曜日)、4(木曜日)、5(金曜日)、6(土曜日)

スクリプト実行

prerotate 〜 endscript

ログファイルがローテーションされる前にこのオプションに囲まれたスクリプトが実行される

後述のsharedscriptsオプションが指定されている場合はパターン全体がスクリプトに渡される

postrotate 〜 endscript

ログファイルがローテーションされた後にこのオプションに囲まれたスクリプトが実行される

後述のsharedscriptsオプションが指定されている場合はパターン全体がスクリプトに渡される

firstaction 〜 endscript

ログファイルがローテーションされる前、かつprerotateスクリプトが実行される前に1回だけ実行される

ただし、少なくとも1つのログファイルがローテーションされる場合のみ実行される

スクリプトがエラーになった場合、以降の処理は行われない

lastaction 〜 endscript

ログファイルがローテーションされた後、かつpostrotateスクリプトが実行された後に1回だけ実行される

スクリプトがエラーになった場合、エラーメッセージのみが表示される(これが最後のアクションのため)

preremove 〜 endscript

ログファイルを削除する直前に1回だけ実行される

sharedscripts (*)

通常、prerotateとpostrotateで指定したスクリプトはローテーションされるログ毎に実行され、ログファイルへの絶対パスがスクリプトの最初の引数として渡される

このオプションが指定された場合、パターンマッチしたファイルの数に関係なくスクリプトは一回だけ実行される

ただしパターンマッチしたファイルのうちローテーションの必要なものが一つもない場合はスクリプトは実行されない

スクリプトがエラーになった場合、残りのアクションはどのログに対しても実行されない

メール通知

mail <メールアドレス> (*)

対象ログファイルが存在しない状態でローテーションされると指定したメールアドレスにメール通知を行う

mailfirst

メール送信時に期限切れ間近のファイルではなくローテーションしたばかりのファイルを送信する

maillast

メール送信時にローテーションしたばかりのファイルではなく期限切れ間近のファイルを送信する(これがデフォルト)

その他

include <ファイル> あるいは <ディレクトリ>

指定したファイルあるいはディレクトリ配下のファイルを設定ファイルとして追加使用する

2020/07/26更新

対応バージョン: Amazon Linux 2

一般ユーザのnproc値を変更しようと以下のソフトリミット設定を行ったが、スペックの低いEC2インスタンスだとある一定の値以上に増やせない。
$ cat /etc/security/limits.d/20-nproc.conf
:
*          soft    nproc     4096
root       soft    nproc     unlimited

$ ulimit -u
3791

またrootユーザもunlimitedに設定されているにもかかわらず、同様にulimit -uの値が一般ユーザと同じになってしまう。

$ sudo su
# ulimit -u
3791

これはnprocのハードリミットが設定されておらずカーネルによって設定された値がハードリミットとなり、この値がulimit -uの上限(上記の例では3791)になるためである。

$ man limits.conf
:
    <type>
        hard
            for enforcing hard resource limits. These limits are set by the superuser and enforced by
            the Kernel. The user cannot raise his requirement of system resources above such values.
:

この制限を外すには、以下のように設定してハードリミットを増やしてあげればよい。

$ cat /etc/security/limits.d/20-nproc.conf
:
*          hard    nproc     4096

同様に、rootユーザのulimit -uをunlimitedにするには以下のように設定すればよい。

$ cat /etc/security/limits.d/20-nproc.conf
:
root       hard    nproc     unlimited

2019/08/20更新

対応バージョン: Amazon Linux 2

ローカルのファイルをリモートのEC2にセキュアに同期(コピー)する場合、脆弱性のあるscpを使わずにsftpやrsyncを使ったほうがよいとされており、今後主流になっていくであろうOpenSSH-8.0でも同様の案内がされている。
OpenSSH-8.0リリースノートより
:
Security
========
:
The scp protocol is outdated, inflexible and not readily fixed. We
recommend the use of more modern protocols like sftp and rsync for
file transfer instead.
:

そこで、rsyncを使った同期の方法を示す。

尚、同期元と同期先の環境はそれぞれ以下とする。

同期元 (Ubuntu 18.04)
$ uname -r
4.15.0-58-generic

$ ssh -V
OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n  7 Dec 2017

$ rsync --version
rsync  version 3.1.2  protocol version 31
同期先 (Amazon Linux 2)
$ uname -r
4.14.123-111.109.amzn2.x86_64

$ ssh -V
OpenSSH_7.4p1, OpenSSL 1.0.2k-fips  26 Jan 2017

$ rsync --version
rsync  version 3.1.2  protocol version 31

rsync使用方法

以下のようにする。

rsync <オプション> -e 'ssh -i <EC2キーペアの秘密鍵>' <ローカルディレクトリ> <リモートディレクトリ>

使用例)

$ rsync -av --delete -e 'ssh -i ~/.ssh/ec2.pem' ~/files/ ec2-user@1.2.3.4:files/

オプションはここでは以下を指定しているが、他にもいろいろあるので用途に応じて指定する。

-a アーカイブモード(-rlptgoDと同義)

-r(ディレクトリを再帰的に処理)

-l(シンボリックリンクをそのままコピー)

-p(ファイルのパーミッションを保持)

-t(ファイルのタイムスタンプを保持)

-g(ファイルの所有グループを保持) ※

-o(ファイルの所有ユーザを保持) ※

-D(デバイスファイル/スペシャルファイルをそのままコピー) ※

※ root権限が必要

-v 処理内容の表示
--delete 同期元に存在しないファイルを同期先から削除

2019/10/19更新

Amazon DynamoDBはAWSクラウド上だけでなくローカルでも実行できるバージョン(DynamoDBローカル)が提供されている。ローカルで実行することにより課金を気にせず動作確認や開発が可能になる。

ここではUbuntu 18.04にDynamoDBローカルを導入してテーブルの作成やデータの操作を行ってみる。

DynamoDBローカル動作環境

DynamoDBローカルはjarファイルとDockerイメージの2種類が提供されているが、ここでは前者の方法で確認する。

jarファイルを利用する場合はあらかじめ以下をインストールしておく。

Java Runtime Environment (JRE) 6.x 以降

DynamoDBローカルインストール

まず以下からDynamoDBローカルのtar.gzをダウンロードする。

ダウンロードしたら任意の場所に展開する。インストールはこれだけである。

$ tar zxvf dynamodb_local_latest.tar.gz

DynamoDBローカルサーバ起動

サーバの起動も簡単で、以下のようにすればよい。Ctrl + cで停止する。

$ java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb
Initializing DynamoDB Local with the following configuration:
Port:	8000
InMemory:	false
DbPath:	null
SharedDb:	true
shouldDelayTransientStatuses:	false
CorsParams:	*

尚、デフォルトの待受ポートは8000なので必要に応じて変更する。(-port <n>で指定)

テーブル作成

サーバの起動ができたら実際にテーブルを作成してみる。

AWS CLIを使って以下のようにする。ここでは「test」というテーブルを作り、キーを「num」とする。

$ aws dynamodb create-table --table-name test \
--attribute-definitions AttributeName=num,AttributeType=N \
--key-schema AttributeName=num,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
--endpoint-url http://localhost:8000
{
    "TableDescription": {
        "TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/test", 
        "AttributeDefinitions": [
            {
                "AttributeName": "num", 
                "AttributeType": "N"
            }
        ], 
        "ProvisionedThroughput": {
            "NumberOfDecreasesToday": 0, 
            "WriteCapacityUnits": 1, 
            "LastIncreaseDateTime": 0.0, 
            "ReadCapacityUnits": 1, 
            "LastDecreaseDateTime": 0.0
        }, 
        "TableSizeBytes": 0, 
        "TableName": "test", 
        "BillingModeSummary": {
            "LastUpdateToPayPerRequestDateTime": 0.0, 
            "BillingMode": "PROVISIONED"
        }, 
        "TableStatus": "ACTIVE", 
        "KeySchema": [
            {
                "KeyType": "HASH", 
                "AttributeName": "num"
            }
        ], 
        "ItemCount": 0, 
        "CreationDateTime": 1571411233.367
    }
}

テーブルができていることを確認する。

$ aws dynamodb list-tables --endpoint-url http://localhost:8000
{
    "TableNames": [
        "test"
    ]
}

データ操作

テーブルが作成できたらデータを挿入/更新/削除してみる。

データ挿入
$ aws dynamodb put-item --table-name test \
--item '{"num":{"N":"1"},"data":{"S":"first"}}' \
--endpoint-url http://localhost:8000
データ取得
$ aws dynamodb get-item --table-name test \
--key '{"num":{"N":"1"}}' \
--endpoint-url http://localhost:8000
{
    "Item": {
        "num": {
            "N": "1"
        }, 
        "data": {
            "S": "first"
        }
    }
}

AWS CLIだけでなくブラウザからアクセスしてJavaScriptで操作することもできるので、以降はブラウザ上で実行する。

http://localhost:8000/shell/

データ挿入

左ペインに以下のコードを入力し、実行ボタン()をクリックすると右ペインに実行結果が表示される。

AWS.config.endpoint = new AWS.Endpoint('http://localhost:8000');
var dynamodb = new AWS.DynamoDB();
var params = {
    TableName: 'test',
    Item: {
        'num':{N: '2'},
        'data':{S: 'second'}
    }
};
dynamodb.putItem(params, function(err, data) {
    if(err) {
        console.log(err, err.stack);
    } else {
        console.log(data);
    }
});
データ取得
AWS.config.endpoint = new AWS.Endpoint('http://localhost:8000');
var dynamodb = new AWS.DynamoDB();
var params = {
    TableName: 'test',
    Key: {
        'num':{N: '2'}
    }
};
dynamodb.getItem(params, function(err, data) {
    if(err) {
        console.log(err, err.stack);
    } else {
        console.log(data);
    }
});
データ更新
AWS.config.endpoint = new AWS.Endpoint('http://localhost:8000');
var dynamodb = new AWS.DynamoDB();
var params = {
    TableName: 'test',
    Key: {
        'num':{N: '2'}
    },
    ExpressionAttributeNames: {
        '#d': 'data'
    },
    ExpressionAttributeValues: {
        ':newData':{S: '2nd'}
    },
    UpdateExpression: 'SET #d = :newData'
};
dynamodb.updateItem(params, function(err, data) {
    if(err) {
        console.log(err, err.stack);
    } else {
        console.log(data);
    }
});
データ削除
AWS.config.endpoint = new AWS.Endpoint('http://localhost:8000');
var dynamodb = new AWS.DynamoDB();
var params = { 
    TableName: 'test',
    Key: {
        'num':{N: '2'}
    }
};
dynamodb.deleteItem(params, function(err, data) {
    if(err) {
        console.log(err, err.stack);
    } else {
        console.log(data);
    }
});
テーブル削除

ひと通り確認が終わったのでテーブルを削除する。

$ aws dynamodb delete-table --table-name test \
--endpoint-url http://localhost:8000
{
    "TableDescription": {
        "TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/test", 
        "AttributeDefinitions": [
            {
                "AttributeName": "num", 
                "AttributeType": "N"
            }
        ], 
        "ProvisionedThroughput": {
            "NumberOfDecreasesToday": 0, 
            "WriteCapacityUnits": 1, 
            "LastIncreaseDateTime": 0.0, 
            "ReadCapacityUnits": 1, 
            "LastDecreaseDateTime": 0.0
        }, 
        "TableSizeBytes": 14, 
        "TableName": "test", 
        "BillingModeSummary": {
            "LastUpdateToPayPerRequestDateTime": 0.0, 
            "BillingMode": "PROVISIONED"
        }, 
        "TableStatus": "ACTIVE", 
        "KeySchema": [
            {
                "KeyType": "HASH", 
                "AttributeName": "num"
            }
        ], 
        "ItemCount": 1, 
        "CreationDateTime": 1571411233.367
    }
}

AWS CLIやJavaScriptを使って他にも様々な操作ができるので以下のドキュメントを参照のこと。

DynamoDBローカルのバックエンドはSQLite

DynamoDBローカルのバックエンドはSQLiteを使っているのでSQLiteの各種ツールで中身を参照することができる。

ファイルはDynamoDBローカルのサーバを起動したカレントディレクトリに「shared-local-instance.db」として作成される。

ファイルは一つだけなのでバックアップや別ホストへのコピーも容易に行える。

$ sqlite3 shared-local-instance.db 
SQLite version 3.22.0 2018-01-22 18:45:57
Enter ".help" for usage hints.

sqlite> .tables
cf    dm    sm    ss    test  tr    us  

テーブルはそれぞれ以下の用途に使われている。

cf

DynamoDBローカルのバージョン

sqlite> .schema cf
CREATE TABLE cf (version TEXT);

sqlite> select * from cf;
v2.4.0
dm

DynamoDBローカル内に定義したテーブルの情報

sqlite> .schema dm
CREATE TABLE dm (TableName TEXT, CreationDateTime INTEGER, LastDecreaseDate INTEGER, LastIncreaseDate INTEGER, NumberOfDecreasesToday INTEGER, ReadCapacityUnits INTEGER, WriteCapacityUnits INTEGER, TableInfo BLOB, BillingMode INTEGER DEFAULT 0, PayPerRequestDateTime INTEGER DEFAULT 0, PRIMARY KEY(TableName));

sqlite> select * from dm;
test|1571441645458|0|0|0|1|1|{"Attributes":[{"AttributeName":"num","AttributeType":"N"}],"GSIList":[],"GSIDescList":[],"SQLiteIndex":{"":[{"DynamoDBAttribute":{"AttributeName":"num","AttributeType":"N"},"KeyType":"HASH","SQLiteColumnName":"hashKey","SQLiteDataType":"BLOB"}]},"UniqueIndexes":[{"DynamoDBAttribute":{"AttributeName":"num","AttributeType":"N"},"KeyType":"HASH","SQLiteColumnName":"hashKey","SQLiteDataType":"BLOB"}],"UniqueGSIIndexes":[]}|0|0
sm / ss / us

DynamoDB Stream管理情報

sqlite> .schema sm
CREATE TABLE sm (StreamID TEXT, StreamStatus TEXT, TableName TEXT, StreamInfo BLOB, CreationDateTime INTEGER, DeletionDateTime INTEGER, PRIMARY KEY(StreamID));

sqlite> .schema ss
CREATE TABLE ss (StreamID TEXT, ShardID TEXT, CreationDateTime INTEGER, DeletionDateTime INTEGER, InitialSequenceNumberStart INTEGER, SequenceNumberEnd INTEGER, ParentShardID TEXT, PRIMARY KEY(ShardID));

sqlite> .schema us
CREATE TABLE us (StreamID TEXT, ShardID TEXT, SequenceNumber INTEGER, CreationDateTime INTEGER, StreamRecord BLOB, OperationType TEXT, PRIMARY KEY(SequenceNumber));

sqlite> select * from sm;

sqlite> select * from ss;

sqlite> select * from us;
test

今回の導入手順でテスト用に作成したテーブル

sqlite> .schema test
CREATE TABLE IF NOT EXISTS "test" (hashKey BLOB DEFAULT NULL, hashValue BLOB NOT NULL, itemSize INTEGER DEFAULT 0, ObjectJSON BLOB NOT NULL, PRIMARY KEY(hashKey));
CREATE INDEX "test*HVI" ON "test" (hashValue);

sqlite> select * from test;
>=E10000000000000000000000000000000000000|-��,q����	�T	�����|14|{"data":{"S":"first"},"num":{"N":"1"}}
tr

トランザクション管理情報

sqlite> .schema tr
CREATE TABLE tr (TransactionId TEXT, TransactionSignature BLOB, CreationDateTime INTEGER, PRIMARY KEY(TransactionId));

sqlite> select * from tr;

参考サイト

2019/03/06更新

セキュリティ要件などでCloudWatch Logsの特定のロググループを特定のIPからのみ参照可能にしたい場合は、IAMの以下の2つのポリシーを対象となるユーザやグループにアタッチすればよい。

※ ただしAWSの他のサービスからアクセスが必要な場合はこの方法は使えないので注意が必要

1. CloudWatchLogsFullAccessやCloudWatchLogsReadOnlyAccessのようなCloudWatch Logsにアクセスできるポリシー、あるいはそれに類するポリシー

2. 指定したロググループに対して特定のIPからのみアクセス可能とするポリシー(新規作成)

例) 「ng」というロググループに対してSourceIpで指定したIPからのみアクセス可能とする

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Action": "logs:DescribeLogStreams",
            "Resource": "arn:aws:logs:<リージョン>:<AWSアカウント>:log-group:ng:*",
            "Condition": {
                "NotIpAddress": {
                    "aws:SourceIp": "xxx.xxx.xxx.xxx"
                }
            }
        },
        {
            "Effect": "Deny",
            "Action": "logs:FilterLogEvents",
            "Resource": "arn:aws:logs:<リージョン>:<AWSアカウント>:log-group:ng:*",
            "Condition": {
                "NotIpAddress": {
                    "aws:SourceIp": "xxx.xxx.xxx.xxx"
                }
            }
        }
    ]
}

実行例

例えば以下の2つのロググループ/ログストリームが存在する状態で上記のポリシーを適用する。

ok/ok_test
ng/ng_test

この状態でロググループの一覧表示とログストリームの参照・検索を行うと以下のような挙動となる。

ロググループ一覧表示(両方表示される)
$ aws logs describe-log-groups
{
    "logGroups": [
        {
            "arn": "arn:aws:logs:xxxxxxxxxxxx:xxxxxxxxxxxx:log-group:ng:*", 
            "creationTime": 1551426635634, 
            "metricFilterCount": 0, 
            "logGroupName": "ng", 
            "storedBytes": 0
        }, 
        {
            "arn": "arn:aws:logs:xxxxxxxxxxxx:xxxxxxxxxxxx:log-group:ok:*", 
            "creationTime": 1551426626316, 
            "metricFilterCount": 0, 
            "logGroupName": "ok", 
            "storedBytes": 0
        }
    ]
}
ログストリーム参照(どのIPからでも参照可能)
$ aws logs describe-log-streams --log-group-name ok
{
    "logStreams": [
        {
            "creationTime": 1551426648336, 
            "arn": "arn:aws:logs:xxxxxxxxxxxx:xxxxxxxxxxxx:log-group:ok:log-stream:ok_test", 
            "logStreamName": "ok_test", 
            "storedBytes": 0
        }
    ]
}
ログストリーム参照(特定のIPからのみ参照可能)
$ aws logs describe-log-streams --log-group-name ng
An error occurred (AccessDeniedException) when calling the DescribeLogStreams operation: User: arn:aws:iam::xxxxxxxxxxxx:user/xxxx is not authorized to perform: logs:DescribeLogStreams on resource: arn:aws:logs:xxxxxxxxxxxx:xxxxxxxxxxxx:log-group:ng:log-stream: with an explicit deny
ログストリーム検索(どのIPからでも参照可能)
$ aws logs filter-log-events --log-group-name ok
{
    "searchedLogStreams": [], 
    "events": []
}
ログストリーム検索(特定のIPからのみ参照可能)
$ aws logs filter-log-events --log-group-name ng
An error occurred (AccessDeniedException) when calling the FilterLogEvents operation: User: arn:aws:iam::xxxxxxxxxxxx:user/xxxx is not authorized to perform: logs:FilterLogEvents on resource: arn:aws:logs:xxxxxxxxxxxx:xxxxxxxxxxxx:log-group:ng:log-stream: with an explicit deny

参考サイト

2019/12/19更新

Amazon CloudFront(以下CloudFront)とEC2を用いてWebサービスなどを提供したい場合に、EC2へのHTTP(S)アクセスをCloudFrontからのみに絞りたい場合がある。

ただCloudFrontからのアクセスを簡単に指定する方法はなく、以下で公開されているCloudFrontのIPアドレスを個別にセキュリティグループに設定する必要がある。(記事執筆時点で67個)

IPアドレス一つひとつをAWS CLIやマネジメントコンソール上で設定するのは大変なので、以下のワンライナーを使えば簡単にセキュリティグループに一括登録することができる。

なお、必要なツールとしてcurl、jq、awkはあらかじめインストールしておくこと。またここではEC2のTCP/80ポートへの接続許可とする。

$ sg="sg-xxxxxxxxx"
$ curl http://d7uri8nf7uskq.cloudfront.net/tools/list-cloudfront-ips \
| jq -r '.[][]' \
| awk '{printf("echo %s;aws ec2 authorize-security-group-ingress --group-id %s --protocol tcp --port 80 --cidr %s\n",$1,"'${sg}'",$1)}' \
| sh

以下、一つのコマンド毎に説明する。

CloudFrontのIPアドレス取得(JSONフォーマット)
$ curl http://d7uri8nf7uskq.cloudfront.net/tools/list-cloudfront-ips
{"CLOUDFRONT_GLOBAL_IP_LIST": ["144.220.0.0/16", ... , "34.232.163.208/29"]}
そこからjqでIPアドレスのみ取り出す
$ ... | jq -r '.[][]'
144.220.0.0/16
:
34.232.163.208/29
awkを使ってそのIPアドレスをAWS CLIでセキュリティグループに登録する構文を作る(時間がかかるので最初にIPアドレスをechoしておく)
$ ... | awk '{printf("echo %s;aws ec2 authorize-security-group-ingress --group-id %s --protocol tcp --port 80 --cidr %s\n",$1,"'${sg}'",$1)}'
echo 144.220.0.0/16;aws ec2 authorize-security-group-ingress --group-id sg-xxxxxxxxx --protocol tcp --port 80 --cidr 144.220.0.0/16
:
echo 34.232.163.208/29;aws ec2 authorize-security-group-ingress --group-id sg-xxxxxxxxx --protocol tcp --port 80 --cidr 34.232.163.208/29
これをshで実行する
$ ... | sh
144.220.0.0/16
:
34.232.163.208/29

なお、セキュリティグループには登録数の上限があるので気を付けること。

参考サイト

2017/7/10更新

対応バージョン: 10.12.5

macOS SierraにAWS CLI環境を構築する手順を示す。

準備

最初にMac App StoreからXcodeをインストールしておく。

AWS CLIインストール

AWS CLIのインストールにはPythonのパッケージマネージャであるpipコマンドを使用するのでpip -> AWS CLIの順でインストールする。

pipインストール
% sudo easy_install pip

% pip --version
pip 9.0.1 from /Library/Python/2.7/site-packages (python 2.7)
awscliインストール
% pip install awscli --upgrade --user

PATH環境変数に以下のパスを追加
$HOME/Library/Python/2.7/bin

% aws --version
aws-cli/1.11.117 Python/2.7.10 Darwin/16.6.0 botocore/1.5.80

JSON Lintインストール

AWS CLIにおけるJSONデータの表示・加工に便利なJSON Lintをインストールする。

JSON LintのインストールにはNode.js付属のnpmコマンドを使用するのでNode.js -> JSON Lintの順でインストールする。

nodebrewインストール
% curl https://raw.githubusercontent.com/hokaccha/nodebrew/master/nodebrew | perl - setup

PATH環境変数に以下のパスを追加
$HOME/.nodebrew/current/bin

% nodebrew selfupdate
Node.jsインストール
% nodebrew install latest

PATH環境変数に以下のパスを追加
$HOME/.nodebrew/node/v8.1.3/bin

% node -v
v8.1.3

% npm -v
5.0.3
JSON Lintインストール
% npm install jsonlint

PATH環境変数に以下のパスを追加
$HOME/node_modules/.bin

% jsonlint -v
1.6.2

% echo '{"key":"value"}' | jsonlint
{
  "key": "value"
}

初期設定

Access KeyとSecret Access Keyを設定してAWS CLIが使用できるようにする。

% aws configure list
      Name                 Value          Type  Location
      ----                 -----          ----  --------
   profile             <not set>          None  None
access_key             <not set>          None  None
secret_key             <not set>          None  None
    region             <not set>          None  None

% aws configure
AWS Access Key ID [None]: xxxxxxxxxxxxxxxxxx
AWS Secret Access Key [None]: xxxxxxxxxxxxxxxxxx
Default region name [None]: ap-northeast-1 <- 東京リージョンの場合
Default output format [None]: json <- 他にtable、textが指定可能。好みに応じて指定

% aws configure list
      Name                 Value          Type  Location
      ----                 -----          ----  --------
   profile             <not set>          None  None
access_key  ******************** shared-credentials-file
secret_key  ******************** shared-credentials-file
    region        ap-northeast-1   config-file  ~/.aws/config

設定は以下の2ファイルに格納されるが、それぞれのファイルの[default]の部分に一意の名前を付ければ複数の設定(プロファイル)を共存させられる。その場合、awsコマンドを実行する時に--profileに続けてプロファイル名を指定する。

$HOME/.aws/config

コマンド出力結果フォーマットとリージョン

[default]
output = json
region = ap-northeast-1
$HOME/.aws/credentials

認証情報

[default]
aws_access_key_id = xxxxxxxxxxxxxxxxxx
aws_secret_access_key = xxxxxxxxxxxxxxxxxx

動作確認

インスタンス一覧取得
% aws ec2 describe-instances | jq '.Reservations [] .Instances [] .InstanceId'
インスタンス起動
% aws ec2 start-instances --instance-ids i-xxxxxxxxxxxxxxxxx
インスタンス停止
% aws ec2 stop-instances --instance-ids i-xxxxxxxxxxxxxxxxx
インスタンス削除
% aws ec2 terminate-instances --instance-ids i-xxxxxxxxxxxxxxxxx
IAMユーザ取得
% aws iam list-users
{
    "Users": [
        {
            "UserName": "dev", 
            "Path": "/", 
            "CreateDate": "2017-07/10T04:34:38Z", 
            "UserId": "xxxxxxxxxxxxxxxxxxxxx", 
            "Arn": "arn:aws:iam::xxxxxxxxxxxx:user/dev"
        }
    ]
}

% aws iam get-user --query 'User.Arn'
"arn:aws:iam::xxxxxxxxxxxx:user/dev"

参考サイト

2019/12/01更新

AWSで見られる分かりにくい挙動やエラーなどについてメモとして記す。

このメモではAWSのストレージサービスを扱う。

マネジメントコンソールの表示は日本語/英語を併記する
記載内容は記事執筆時点の情報なのでタイミングやAWS CLI、AWS SDKなどのバージョンによっても変わっている可能性がある

Amazon S3

アクセス権の設定

バケットポリシーを設定しようとした際、ポリシー内で指定しているリソース(例: IAMロール)が事前に定義されていないと以下のエラーになるので事前に当該リソースを作成しておく必要がある。
$ aws s3api put-bucket-policy \
--bucket ${_BUCKET} \
--policy file://${_POLICY}
An error occurred (MalformedPolicy) when calling the PutBucketPolicy operation: Invalid principal in policy

2019/12/01更新

AWSで見られる分かりにくい挙動やエラーなどについてメモとして記す。

このメモではAWSのデータベースサービスを扱う。

マネジメントコンソールの表示は日本語/英語を併記する
記載内容は記事執筆時点の情報なのでタイミングやAWS CLI、AWS SDKなどのバージョンによっても変わっている可能性がある

Amazon Aurora

パラメータグループの変更

Aurora (MySQL)-5.6.10aにて、クラスタ起動後にdynamicタイプのパラメータグループの内容を変更すると本来はインスタンス再起動不要であるにも関わらず、パラメータグループのステータスが「同期中/in-sync」ではなく「再起動の保留中/pending-reboot」と表示されインスタンスの再起動を要求されることがある。

ただしこれは表示上だけの問題なので、実際の設定値が変わっていれば設定変更は完了しておりそのまま使い続けてよい。(AWSサポート回答より)

例) dynamicタイプのmax_connectionsを90から450に変更した場合

(変更前)
mysql> select @@max_connections;
+-------------------+
| @@max_connections |
+-------------------+
|                90 |
+-------------------+

(変更後)
mysql> select @@max_connections;
+-------------------+
| @@max_connections |
+-------------------+
|               450 |
+-------------------+
-> 実際に設定値が変わっていればよい

スロークエリログの記録タイミング

Aurora (MySQL)-5.6.10aにて、スロークエリログのタイムスタンプはSQL開始時刻ではなくSQL終了時刻となる。

例)

16:44:00にSQL開始

mysql> select now(); select sleep(10);
+---------------------+
| now()               |
+---------------------+
| 2017-08-15 07:44:00 |
+---------------------+

+-----------+
| sleep(10) |
+-----------+
|         0 |
+-----------+

スロークエリログの内容(タイムスタンプはSQL開始時刻ではなくSQL終了時刻)

# Time: 170815 7:44:10
# User@Host: awsuser[awsuser] @ [172.31.29.5] Id: 941
# Query_time: 10.000223 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0
SET timestamp=1502783050;
select sleep(10);

2017/08/12更新

対応バージョン: Aurora(MySQL-Compatible) 5.6.10a

Amazon Auroraで出力されるログはデフォルトで以下の3種類がある。
error/mysql-error-running.log{,.YYYY-MM-DD.nn}
error/mysql-error.log
external/mysql-external.log

Auroraではこれに加え、クエリログ(general_log)とスロークエリログ(slow_query_log)も出力することができる。

手順は以下の通りで、パラメータグループで設定することができる。尚、これら設定はAuroraインスタンスを再起動することなく動的に反映可能である。(反映にかかる時間は10秒程度)

出力先

まずログの出力先(log_output)がデフォルトで"TABLE"になっているので"FILE"に変更する。

続いて各ログの出力設定を行う。片方だけの設定も両方の設定もどちらも可能である。

クエリログ(general_log)

general_logの値を1(ON)に設定する。デフォルトは0(OFF)。

これにより以下のログが新たに出力されるようになる。

general/mysql-general.log

スロークエリログ(slow_query_log)

slow_query_logを1(ON)に設定する。デフォルトは0(OFF)。

さらにクエリが何秒以上かかった時にログを出力させるかをlong_query_timeで設定する。デフォルトは10(秒)で、0(秒)にすると遅延ありなしに関係なく全てのクエリがログに出力されるようになる。

これにより以下のログが新たに出力されるようになる。

slowquery/mysql-slowquery.log

参考サイト