[AWS CDK] GreengrassとLambdaを作成して、Raspberry Piにデプロイしてみた

AWS CDKで、GreengrassとLambdaを作成して、Raspberry Piにデプロイしてみました。
2019.09.09

概要

CX事業本部の佐藤です。

最近弊社でAWS CDKの人気が高まり始めています。私自身あまり触ったことがなかったため、今回勉強も兼ねて、AWS CDKでGreengrassとLambdaを作成してみました。ちょうど使ってないRaspberry Piがあったため、実際にデプロイしてRaspberry Pi上でLambdaを実行させてみたいと思います。

下記のリポジトリをベースに説明していきます。

aws-cdk-greengrass-template

環境

項目 バージョン
macOS Mojave 10.14.5
AWS CDK 1.6.1 build a09203a
node v10.16.0
npm 6.9.0
Raspberry Pi Raspberry Pi 3 Model B+(SSHで接続できる)
Raspbian OS Raspbian Stretch, 2018-06-29
Greengrass Core v1.9.2
Lambdaランタイム Python 3.7.2

Raspberry PiでGreengrass Lambdaを実行する

Greengrassを使用して、Raspberry PiでLambdaを実行させてみます。以下の手順で行なっていきます。

事前にAWS IoTで証明書を作成する

AWS IoTで証明書を作成しておきます。証明書の作成はマネージメントコンソール上から行います。

  1. Iot Coreコンソールに移動します。
  2. 安全性メニューの証明書メニューを選択します。
  3. 作成ボタンを押します。
  4. 1-Click証明書作成(推奨)の証明書の作成を押します。
  5. モノの証明書、パブリックキー、プライベートキーがダウンロードできるようになるため、すべてダウンロードして自PC上に保管します。
  6. 有効化を押します。
  7. 完了を押します。

これで、デバイス証明書の作成ができました。この証明書を後ほど、Raspberry Piに設定します。

AWS CDKのインストール

npmでインストールします。

npm install -g aws-cdk

AWS CDKプロジェクトの作成とライブラリのインストール

mkdir aws-cdk-greengrass-sample
cd aws-cdk-greengrass-sample
cdk init --language typescript
npm install --save @aws-cdk/aws-lambda
npm install --save @aws-cdk/aws-greengrass
npm install --save @aws-cdk/aws-iot

Greengrass、LambdaをCDKで作成する

Greengrassを使用するため、AWSにLambda関数とGreengrassグループを作成します。今回はタイトル通り、AWS CDKを使用してデプロイしてみたいと思います。GreengrassがLambda関数に依存している構成のため、スタックを2つに分けて、クロススタック参照を使ってデプロイしています。

Lambda用スタック

lib/greengrass-lambda-stack.ts

import cdk = require('@aws-cdk/core');
import lambda = require('@aws-cdk/aws-lambda');

export class GreengrassLambdaStack extends cdk.Stack {

    public readonly greengrassLambdaAlias: lambda.Alias;

    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        // GreengrassにデプロイするLambda関数の作成
        const greengrassLambda = new lambda.Function(this, 'GreengrassSampleHandler', {
            runtime: lambda.Runtime.PYTHON_3_7,
            code: lambda.Code.asset('handlers'),
            handler: 'handler.handler',
        });
        const version = greengrassLambda.addVersion('GreengrassSampleVersion');

        // Greengrass Lambdaとして使用する場合、エイリアスを指定する必要がある
        this.greengrassLambdaAlias = new lambda.Alias(this, 'GreengrassSampleAlias', {
            aliasName: 'rasberrypi',
            version: version
        })
    }
}

Greengrass用スタック

lib/greengrass-raspberry-pi-stack.ts

import cdk = require('@aws-cdk/core');
import iot = require('@aws-cdk/aws-iot');
import lambda = require('@aws-cdk/aws-lambda');
import greengrass = require('@aws-cdk/aws-greengrass');

interface GreengrassRaspberryPiStackProps extends cdk.StackProps {
  greengrassLambdaAlias: lambda.Alias
}

export class GreengrassRaspberryPiStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props: GreengrassRaspberryPiStackProps) {
    super(scope, id, props);

    const certArn: string = '先ほど作った証明書のARNを設定'
    const region: string = cdk.Stack.of(this).region;
    const accountId: string = cdk.Stack.of(this).account;

    // AWS IoTのモノの作成
    const iotThing = new iot.CfnThing(this, 'Thing', {
      thingName: 'Raspberry_Pi_Thing'
    });

    if (iotThing.thingName !== undefined) {
      
      const thingArn = `arn:aws:iot:${region}:${accountId}:thing/${iotThing.thingName}`;

      // ポリシーを作成
      const iotPolicy = new iot.CfnPolicy(this, 'Policy', {
        policyName: 'Raspberry_Pi_Policy',
        policyDocument: {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "iot:*",
                "greengrass:*",
              ],
              "Resource": [
                "*"
              ]
            }
          ]
        }
      });
      iotPolicy.addDependsOn(iotThing);

      // 証明書にポリシーをアタッチ
      if (iotPolicy.policyName !== undefined) {
        const policyPrincipalAttachment = new iot.CfnPolicyPrincipalAttachment(this, 'PolicyPrincipalAttachment', {
          policyName: iotPolicy.policyName,
          principal: certArn
        })
        policyPrincipalAttachment.addDependsOn(iotPolicy)
      }

      // モノに証明書をアタッチ
      const thingPrincipalAttachment = new iot.CfnThingPrincipalAttachment(this, 'ThingPrincipalAttachment', {
        thingName: iotThing.thingName,
        principal: certArn
      });
      thingPrincipalAttachment.addDependsOn(iotThing)

      // Greengrass Coreの作成
      const coreDefinition = new greengrass.CfnCoreDefinition(this, 'CoreDefinition', {
        name: 'Raspberry_Pi_Core',
        initialVersion: {
          cores: [
            {
              certificateArn: certArn,
              id: '1',
              thingArn: thingArn
            }
          ]
        }
      });
      coreDefinition.addDependsOn(iotThing)

      // Greengrassリソースの作成
      const resourceDefinition = new greengrass.CfnResourceDefinition(this, 'ResourceDefinition', {
        name: 'Raspberry_Pi_Resource',
        initialVersion: {
          resources: [
            {
              id: '1',
              name: 'log_file_resource',
              resourceDataContainer: {
                localVolumeResourceData: {
                  sourcePath: '/log',
                  destinationPath: '/log'
                }
              }
            }
          ]
        }
      });

      // Greengrass Lambdaの作成
      const functionDefinition = new greengrass.CfnFunctionDefinition(this, 'FunctionDefinition', {
        name: 'Raspberry_Pi_Function',
        initialVersion: {
          functions: [
            {
              id: '1',
              functionArn: props.greengrassLambdaAlias.functionArn,
              functionConfiguration: {
                encodingType: 'binary',
                memorySize: 65536,
                pinned: true,
                timeout: 3,
                environment: {
                  // ログファイルを書き出すため、リソースの書き込み権限を与える
                  resourceAccessPolicies: [
                    {
                      resourceId: '1',
                      permission: 'rw'
                    }
                  ]
                }
              }
            }
          ]
        }
      });

      // Greengrassグループの作成
      const group = new greengrass.CfnGroup(this, 'Group', {
        name: 'Raspberry_Pi',
        initialVersion: {
          coreDefinitionVersionArn: coreDefinition.attrLatestVersionArn,
          resourceDefinitionVersionArn: resourceDefinition.attrLatestVersionArn,
          functionDefinitionVersionArn: functionDefinition.attrLatestVersionArn
        }
      });

      // 一連のDefinitionの作成が終わったらグループを作成
      group.addDependsOn(coreDefinition)
      group.addDependsOn(resourceDefinition)
      group.addDependsOn(functionDefinition)
    }
  }
}

上記2つのスタックを、クロススタック参照で作成します。

bin/greengrass-raspberry-pi.ts

#!/usr/bin/env node
import 'source-map-support/register';
import cdk = require('@aws-cdk/core');
import { GreengrassRaspberryPiStack } from '../lib/greengrass-raspberry-pi-stack';
import { GreengrassLambdaStack } from '../lib/greengrass-lambda-stack';

const app = new cdk.App();

const lambdaStack = new GreengrassLambdaStack(app, 'GreengrassLambdaStack');
new GreengrassRaspberryPiStack(app, 'GreengrassRasberryPiStack', {
    greengrassLambdaAlias: lambdaStack.greengrassLambdaAlias
});

Lambdaコード

5秒ごとに、hello!! の文字列をログファイルに書き込む処理です。

handlers/handler.py

from logging import config, getLogger
import time
import os

config.dictConfig({
  "version": 1,
  "disable_existing_loggers": False,
  "root": {
    "level": "INFO",
    "handlers": [
      "logFileHandler"
    ]
  },
  "handlers": {
    "logFileHandler": {
      "class": "logging.FileHandler",
      "level": "INFO",
      "formatter": "logFileFormatter",
      "filename": "/log/test.log",
      "mode": "w",
      "encoding": "utf-8"
    }
  },
  "formatters": {
    "logFileFormatter": {
      "format": "%(asctime)s|%(levelname)-8s|%(name)s|%(funcName)s|%(message)s"
    }
  }
})

logger = getLogger(__name__)

while True:
    logger.info('hello!!')
    time.sleep(5)
    
def handler(event, context):
  return

デプロイ

AWSにデプロイします。デプロイするためにはS3バケットが必要なため、CDKのコマンドで作成します。

cdk bootstrap

TypeScriptのためビルドします。

npm run build

デプロイします。

cdk GreengrassRaspberryPiStack deploy
Including dependency stacks: GreengrassLambdaStack
GreengrassLambdaStack
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬────────────────────────────────────────────┬────────┬────────────────┬──────────────────────────────┬───────────┐
│   │ Resource                                   │ Effect │ Action         │ Principal                    │ Condition │
├───┼────────────────────────────────────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤
│ + │ ${GreengrassSampleHandler/ServiceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:lambda.amazonaws.com │           │
└───┴────────────────────────────────────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                               │ Managed Policy ARN                                                             │
├───┼────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${GreengrassSampleHandler/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)

Do you wish to deploy these changes (y/n)? y
GreengrassLambdaStack: deploying...
GreengrassLambdaStack: creating CloudFormation changeset...
 0/6 | 10:16:55 PM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata
 0/6 | 10:16:56 PM | CREATE_IN_PROGRESS   | AWS::IAM::Role        | GreengrassSampleHandler/ServiceRole (GreengrassSampleHandlerServiceRole33F33CB8)
 0/6 | 10:16:56 PM | CREATE_IN_PROGRESS   | AWS::IAM::Role        | GreengrassSampleHandler/ServiceRole (GreengrassSampleHandlerServiceRole33F33CB8) Resource creation Initiated
 0/6 | 10:16:57 PM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata Resource creation Initiated
 1/6 | 10:16:58 PM | CREATE_COMPLETE      | AWS::CDK::Metadata    | CDKMetadata
 2/6 | 10:17:15 PM | CREATE_COMPLETE      | AWS::IAM::Role        | GreengrassSampleHandler/ServiceRole (GreengrassSampleHandlerServiceRole33F33CB8)
 2/6 | 10:17:18 PM | CREATE_IN_PROGRESS   | AWS::Lambda::Function | GreengrassSampleHandler (GreengrassSampleHandler0B069A6B)
 2/6 | 10:17:19 PM | CREATE_IN_PROGRESS   | AWS::Lambda::Function | GreengrassSampleHandler (GreengrassSampleHandler0B069A6B) Resource creation Initiated
 3/6 | 10:17:19 PM | CREATE_COMPLETE      | AWS::Lambda::Function | GreengrassSampleHandler (GreengrassSampleHandler0B069A6B)
 3/6 | 10:17:22 PM | CREATE_IN_PROGRESS   | AWS::Lambda::Version  | GreengrassSampleHandler/VersionGreengrassSampleVersion (GreengrassSampleHandlerVersionGreengrassSampleVersion357ED95A)
 3/6 | 10:17:22 PM | CREATE_IN_PROGRESS   | AWS::Lambda::Version  | GreengrassSampleHandler/VersionGreengrassSampleVersion (GreengrassSampleHandlerVersionGreengrassSampleVersion357ED95A) Resource creation Initiated
 4/6 | 10:17:23 PM | CREATE_COMPLETE      | AWS::Lambda::Version  | GreengrassSampleHandler/VersionGreengrassSampleVersion (GreengrassSampleHandlerVersionGreengrassSampleVersion357ED95A)
 4/6 | 10:17:26 PM | CREATE_IN_PROGRESS   | AWS::Lambda::Alias    | GreengrassSampleAlias (GreengrassSampleAlias7D31FBCD)
 4/6 | 10:17:26 PM | CREATE_IN_PROGRESS   | AWS::Lambda::Alias    | GreengrassSampleAlias (GreengrassSampleAlias7D31FBCD) Resource creation Initiated
 5/6 | 10:17:26 PM | CREATE_COMPLETE      | AWS::Lambda::Alias    | GreengrassSampleAlias (GreengrassSampleAlias7D31FBCD)
 6/6 | 10:17:28 PM | CREATE_COMPLETE      | AWS::CloudFormation::Stack | GreengrassLambdaStack

 ✅  GreengrassLambdaStack

Outputs:
GreengrassLambdaStack.ExportsOutputRefGreengrassSampleAlias7D31FBCDFD224BE2 = arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:GreengrassLambdaStack-GreengrassSampleHandlerXXXXXXXXXXXXXXXXXXXX:rasberrypi

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/GreengrassLambdaStack/d5739a10-XXXXXXXXXXXXXXXXXX
GreengrassRaspberryPiStack
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬──────────┬────────┬───────────────────┬───────────┬───────────┐
│   │ Resource │ Effect │ Action            │ Principal │ Condition │
├───┼──────────┼────────┼───────────────────┼───────────┼───────────┤
│ + │ ???      │ Allow  │ greengrass:*      │           │           │
│   │          │        │ iot:*             │           │           │
└───┴──────────┴────────┴───────────────────┴───────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)

Do you wish to deploy these changes (y/n)? y
GreengrassRaspberryPiStack: deploying...
GreengrassRaspberryPiStack: creating CloudFormation changeset...
 0/10 | 10:17:48 PM | CREATE_IN_PROGRESS   | AWS::IoT::Thing                     | Thing
 0/10 | 10:17:48 PM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata                  | CDKMetadata
 0/10 | 10:17:48 PM | CREATE_IN_PROGRESS   | AWS::Greengrass::ResourceDefinition | ResourceDefinition
 0/10 | 10:17:48 PM | CREATE_IN_PROGRESS   | AWS::Greengrass::FunctionDefinition | FunctionDefinition
 0/10 | 10:17:50 PM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata                  | CDKMetadata Resource creation Initiated
 1/10 | 10:17:50 PM | CREATE_COMPLETE      | AWS::CDK::Metadata                  | CDKMetadata
 1/10 | 10:17:51 PM | CREATE_IN_PROGRESS   | AWS::Greengrass::FunctionDefinition | FunctionDefinition Resource creation Initiated
 2/10 | 10:17:51 PM | CREATE_COMPLETE      | AWS::Greengrass::FunctionDefinition | FunctionDefinition
 2/10 | 10:17:52 PM | CREATE_IN_PROGRESS   | AWS::Greengrass::ResourceDefinition | ResourceDefinition Resource creation Initiated
 3/10 | 10:17:52 PM | CREATE_COMPLETE      | AWS::Greengrass::ResourceDefinition | ResourceDefinition
3/10 Currently in progress: Thing
 3/10 | 10:18:49 PM | CREATE_IN_PROGRESS   | AWS::IoT::Thing                     | Thing Resource creation Initiated
 4/10 | 10:18:49 PM | CREATE_COMPLETE      | AWS::IoT::Thing                     | Thing
 4/10 | 10:18:51 PM | CREATE_IN_PROGRESS   | AWS::IoT::ThingPrincipalAttachment  | ThingPrincipalAttachment
 4/10 | 10:18:51 PM | CREATE_IN_PROGRESS   | AWS::Greengrass::CoreDefinition     | CoreDefinition
 4/10 | 10:18:52 PM | CREATE_IN_PROGRESS   | AWS::IoT::Policy                    | Policy
 4/10 | 10:18:53 PM | CREATE_IN_PROGRESS   | AWS::Greengrass::CoreDefinition     | CoreDefinition Resource creation Initiated
 5/10 | 10:18:54 PM | CREATE_COMPLETE      | AWS::Greengrass::CoreDefinition     | CoreDefinition
 5/10 | 10:18:58 PM | CREATE_IN_PROGRESS   | AWS::Greengrass::Group              | Group
 5/10 | 10:19:00 PM | CREATE_IN_PROGRESS   | AWS::Greengrass::Group              | Group Resource creation Initiated
 6/10 | 10:19:00 PM | CREATE_COMPLETE      | AWS::Greengrass::Group              | Group
6/10 Currently in progress: ThingPrincipalAttachment, Policy
 6/10 | 10:19:52 PM | CREATE_IN_PROGRESS   | AWS::IoT::ThingPrincipalAttachment  | ThingPrincipalAttachment Resource creation Initiated
 7/10 | 10:19:52 PM | CREATE_COMPLETE      | AWS::IoT::ThingPrincipalAttachment  | ThingPrincipalAttachment
 7/10 | 10:19:52 PM | CREATE_IN_PROGRESS   | AWS::IoT::Policy                    | Policy Resource creation Initiated
 8/10 | 10:19:52 PM | CREATE_COMPLETE      | AWS::IoT::Policy                    | Policy
 8/10 | 10:19:54 PM | CREATE_IN_PROGRESS   | AWS::IoT::PolicyPrincipalAttachment | PolicyPrincipalAttachment
8/10 Currently in progress: PolicyPrincipalAttachment

 ✅  GreengrassRaspberryPiStack

スタックが2つ作成されて、デプロイできました。

Greengrass Coreが動作できるように、Raspberry Piの環境設定を行う

Raspberry PiでGreengrassを動作させるために、各種設定を行います。以降の作業はRaspberry PiにSSHで接続して作業します。

userとgroupの作成

sudo adduser --system ggc_user
sudo addgroup --system ggc_group

ハードリンクとソフトリンクの保護を有効

viなどを使用して、/etc/sysctl.d/98-rpi.confファイルに以下を追加します。

fs.protected_hardlinks = 1
fs.protected_symlinks = 1

再起動します。

sudo reboot

再起動後、設定が反映されているか確認します。以下のように表示されていれば、OKです。

pi@raspberrypi~ $ sudo sysctl -a 2> /dev/null | grep fs.protected
fs.protected_hardlinks = 1
fs.protected_symlinks = 1

メモリ cgroups を有効

viなどを使用して、/boot/cmdline.txt ファイルの最初の行に以下の内容を追加します。

cgroup_enable=memory cgroup_memory=1

再起動します。

sudo reboot

Raspberry PiにGreengrass Coreのインストール

Raspberry Piに、Greengrass Coreをインストールします。以下のコマンドを実行します。実行すると、/greengrass フォルダが作成されます。

wget https://d1onfpft10uf5o.cloudfront.net/greengrass-core/downloads/1.9.2/greengrass-linux-armv7l-1.9.2.tar.gz
sudo tar -xzvf greengrass-linux-armv7l-1.9.2.tar.gz -C /

Raspberry Piに証明書を組み込む

AWS IoTと接続するために、デバイス証明書を設定します。

  1. 先ほど作成してダウンロードしてある証明書や秘密鍵、公開鍵をRaspberry Pi上の /greengrass/certs 配下に保存します。SCPなどを使用して、Raspberry Pi上にアップロードします。
    • XXXXXXXX-certificate.pem.crt
    • XXXXXXXX-private.pem.key
    • XXXXXXXX-public.pem.key
  2. ルートCA証明書をダウンロードします。
    cd /greengrass/certs
    sudo wget -O root.ca.pem https://www.amazontrust.com/repository/AmazonRootCA1.pem
    
  3. /greengrass/config/config.json を以下の内容で作成します。証明書の保存先の指定や、AWS IoTのエンドポイントの指定、対象のモノの指定などを行なっています。
    {
     "coreThing" : {
       "caPath" : "root.ca.pem",
       "certPath" : "XXXXXXXX-certificate.pem.crt",
       "keyPath" : "XXXXXXXX-private.pem.key",
       "thingArn" : "Greengrass Coreに設定されているモノのARNを設定",
       "iotHost" : "AWS IoTのエンドポイントを設定",
       "ggHost" : "greengrass-ats.iot.ap-northeast-1.amazonaws.com",
       "keepAlive" : 600
     },
     "runtime" : {
       "cgroup" : {
         "useSystemd" : "yes"
       }
     },
     "managedRespawn" : false,
     "crypto" : {
       "principals" : {
         "SecretsManager" : {
           "privateKeyPath" : "file:///greengrass/certs/XXXXXXXX-private.pem.key"
         },
         "IoTCertificate" : {
           "privateKeyPath" : "file:///greengrass/certs/XXXXXXXX-private.pem.key",
           "certificatePath" : "file:///greengrass/certs/XXXXXXXX-certificate.pem.crt"
         } 
       },
       "caPath" : "file:///greengrass/certs/root.ca.pem"
     }
    }
    
  4. 証明書の組み込みが終わったので、Greengrass Coreを実行します。
    sudo /greengrass/ggc/core/greengrassd start

Raspberry PiにGreengrassをデプロイする

Raspberry PiにGreengrassグループをデプロイします。

  1. マネージメントコンソールから、IoT Greengrassを選択します。
  2. グループメニューを選択します。
  3. Rasberry_PiというGreengrassグループがあるので、それを選択します。
  4. 右上のアクション→デプロイを押します。

ステータスが「正常に完了しました」になれば成功です。

Raspberry Pi上のLambdaの動作確認

実際にログファイルが書き込まれているか確認します。確認すると以下のように、5秒ごとにログが書き込まれていました。うまく動いてそうです。

cat /log/test.log
2019-09-08 19:21:53,760|INFO    |handler|<module>|hello!!
2019-09-08 19:21:58,766|INFO    |handler|<module>|hello!!
2019-09-08 19:22:03,772|INFO    |handler|<module>|hello!!
2019-09-08 19:22:08,778|INFO    |handler|<module>|hello!!
2019-09-08 19:22:13,784|INFO    |handler|<module>|hello!!

まとめ

AWS CDKを使って、Greengrassを動作させることができました。Lambdaをデバイス上で動作させるほかにも、コネクタという機能を使って、Raspberry PiのGPIOを簡単に制御できたりもするので、興味のある方は試してみてはいかがでしょうか。