亚马逊AWS官方博客

Step-by-Step 快速上手 AWS IoT OTA 固件升级

为了保证物联网设备能够保持在功能上随时更新,并且在出现问题的时候及时得到修复。小到智能手环,空气净化器,大到家用汽车,设备厂商无不是通过提供 OTA(Over-The-Air)功能来提高用户满意度。而利用 AWS IoT Device Management 中的 Jobs 组件,可以帮助客户非常快速的开发出物联网设备的 OTA 功能。本文旨在帮助读者一步步的快速上手并理解 OTA 升级流程,其中会使用到 AWS IoT Core,IoT Device Management,EC2 以及 S3 的相关功能。关于在开发过程中的具体流程可以配合参考 AWS IoT Device SDK 文档。

准备工作

  1. 用具有 admin 权限的用户登陆 AWS Console。
  2. 本文中的 AWS IoT 设备会使用一台 Amazon Linux EC2 实例模拟,Amazon Linux EC2 实例上默认安装了 AWS 命令行工具 AWSCLI,接下来的所有操作都是以 AWS us-east-1 区为示例。启动一台 Amazon Linux EC2 实例作为模拟的 IoT 设备,由于后面安装要安装的 rpm 包有依赖关系,这里要确保使用的是 Amazon Linux 2023 AMI (HVM)。
  3. 为了保证网络畅通,在实验环节 Security Group 建议开放全部的 IP 和端口。如在第四步最后遇到 Connect Closed,可在用于测试的 EC2 Instance 所关联的 Security Groups 的 Inbound rules 中添加一条新的规则,允许所有 IP 地址 (0.0.0.0/0) 访问所有端口 (0-65535)。
  4. 在 AWS Console 上赋予这台 EC2 实例一个具有足够权限的 Role,测试中可以直接用 admin 权限。
  5. 在 IAM 服务中,生成 Access Key 和 Secret Access Key 并记录下来。
  6. 登陆到 EC2 实例上使用 aws configure 命令配置好上一步生成的 Access Key 和 Secret Access Key 并将 Region 设置为 us-east-1。

操作流程

  1. 环境准备
  2. 在 AWS 上创建 IoT Thing
  3. 编写 AWS IoT Jobs 文档
  4. 运行 IoT 设备端程序
  5. 创建 AWS IoT Jobs 进行固件升级
  6. 验证固件升级是否成功

第一步 – 环境准备

  1. 登陆 EC2 实例,安装 git
    $ sudo yum install git
  1. 安装 node.js
    $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
    $ . ~/.nvm/nvm.sh
    $ nvm install node
  1. 安装 AWS IoT Device SDK – Javascript
    $ git clone https://github.com/aws/aws-iot-device-sdk-js.git
    $ cd aws-iot-device-sdk-js
    $ npm install
  1. 下载两个不同版本的 telnet 程序包,后续模拟固件升级时使用
    $ cd ~/aws-iot-device-sdk-js/examples/
    $ wget https://www.rpmfind.net/linux/centos/8-stream/AppStream/x86_64/os/Packages/telnet-0.17-76.el8.x86_64.rpm
    $ wget https://www.rpmfind.net/linux/centos-stream/9-stream/AppStream/x86_64/os/Packages/telnet-0.17-85.el9.x86_64.rpm
  1. 安装旧版本 telnet 程序,后续我们会通过 OTA 完成这个程序从 telnet-0.17-76.el8.x86_64 版本到 telnet-0.17-85.el9.x86_64 版本的升级
    $ sudo rpm -ivh telnet-0.17-76.el8.x86_64.rpm
  1. 创建一个 S3 bucket 作为新固件的存储位置,并上传新版本的 telnet 程序
    $ aws s3 mb s3://example-bucket-202401 # 桶名请替换成自己定义的名称
    $ aws s3 cp telnet-0.17-85.el9.x86_64.rpm s3://example-bucket-202401

第二步 – 在 AWS 上创建 IOT thing

  1. 创建 IoT thing,记录下输出中的 thingArn
    $ aws iot create-thing --thing-name aws-iot-device-sdk-js
    # 输出结果
    {  
        "thingName": "aws-iot-device-sdk-js",  
        "thingArn": "arn:aws:iot:us-east-1:378245234483:thing/aws-iot-device-sdk-js",  
        "thingId": "a16b5c76-e109-483a-9801-550c07c63e2b"  
    }
  1. 下载 AWS IoT 根证书,创建 IoT 设备证书和密钥,记录下生成的 certificateArn
    $ pwd
    # 输出结果
    /home/ec2-user/aws-iot-device-sdk-js/examples
    $ mkdir certs
    $ cd certs
    $ wget https://www.amazontrust.com/repository/AmazonRootCA1.pem
    $ mv AmazonRootCA1.pem root-CA.crt
    $ aws iot create-keys-and-certificate \
          --certificate-pem-outfile "certificate.pem.crt" \
          --public-key-outfile "public.pem.key" \
          --private-key-outfile "private.pem.key"  
  • 从上一步的命令输出中记录下自己的 certificateArn,后面的命令中会用到,例如
    # 输出结果
    "certificateArn": "arn:aws:iot:us-east-1:378245234483:cert/fdb474e9d41e49ecd7054c18c5a8720d2735f8a67ad03ff38a9d888b9a15513a",
    "certificateId": "fdb474e9d41e49ecd7054c18c5a8720d2735f8a67ad03ff38a9d888b9a15513a",
  1. 创建一个 IoT Policy,挂载给证书并激活证书
    $ cd .. 
    $ pwd
    # 输出结果
    >/home/ec2-user/aws-iot-device-sdk-js/examples
  • 编写一个 policy 文档,复制以下 JSON 格式的策略并保存为 iot-policy.json 文件
    $ vi iot-policy.json
    {
      "Version": "2012-10-17",
      "Statement": 
      [
        {
          "Effect": "Allow",
          "Action": 
          [
            "iot:Publish",
            "iot:Subscribe",
            "iot:Connect",
            "iot:Receive"
          ],
          "Resource": 
          [
            "*"
          ]
        }
      ]
    }
  • 创建 iot policy
    $ aws iot create-policy --policy-name ota-policy --policy-document file://iot-policy.json
  • 挂载 policy 到之前创建的 IoT 设备证书上,注意这里的 –target 替换成自己的证书 Arn
    $ aws iot attach-policy \
        --policy-name ota-policy \
        --target "arn:aws:iot:us-east-1:378245234483:cert/fdb474e9d41e49ecd7054c18c5a8720d2735f8a67ad03ff38a9d888b9a15513a"
  • 激活证书,注意 –certificate-id 替换成自己证书的 id
    $ aws iot update-certificate --certificate-id fdb474e9d41e49ecd7054c18c5a8720d2735f8a67ad03ff38a9d888b9a15513a --new-status ACTIVE
  • Attach thing 到证书,其中 –principal 是自己证书的 Arn
    $ aws iot attach-thing-principal --thing-name aws-iot-device-sdk-js --principal arn:aws:iot:us-east-1:378245234483:cert/fdb474e9d41e49ecd7054c18c5a8720d2735f8a67ad03ff38a9d888b9a15513a

第三步 – 编写 AWS IoT Jobs 文档

  1. 编写一个 IoT Jobs 文档。关于文档编写的格式,请参考 https://github.com/aws/aws-iot-device-sdk-js#jobsAgent。当 IoT 设备请求 IoT Jobs 文档时,AWS IoT 会生成预签名 URL 并使用预签名 URL 替换占位符 URL。然后将 IoT Jobs 文档发送到设备,设备会通过这个预签名 URL 取得访问 S3 bucket 中固件的权限
    $ pwd
    /home/ec2-user/aws-iot-device-sdk-js/examples
  • 编写一个 jobs 文档,复制以下 JSON 格式文档并保存为 jobs-document.json 文件。其中 url 地址可以从 S3 bucket 控制台界面直接 Copy URL
    $ vi jobs-document.json
    {
      "operation": "install",
      "packageName": "new-firmware",
      "workingDirectory": "../examples",
      "launchCommand": "sudo rpm -Uvh new-firmware.rpm",
      "autoStart": "true",
      "files": 
      [
        {
          "fileName": "new-firmware.rpm",
          "fileVersion": "1.0",
          "fileSource": 
          {
            "url": "${aws:iot:s3-presigned-url:https://example-bucket-202401.s3.amazonaws.com/telnet-0.17-85.el9.x86_64.rpm}"
          }
        }
      ]
    }
  1. 上传 IoT Jobs 文档到 S3 bucket,桶名称 example-bucket-202401 替换成自己的桶名称
    $ aws s3 cp jobs-document.json s3://example-bucket-202401
  1. 在创建使用预签名 Amazon S3 URL 的 Job 时,您必须提供一个 IAM 角色,该角色可授予 AWS IoT 服务从 Amazon S3 存储桶中下载文件的权限。该角色还必须向 AWS IoT 授予 assumeRole 的权限,也就是让 AWS IoT 具有代表设备去 S3 上面下载固件的权限
  • 编写一个 assumeRole 的 policy 文档,复制以下 JSON 格式的策略并保存为 trust-policy.json 文件
    $ vi trust-policy.json
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "",
          "Effect": "Allow",
          "Principal": 
          {
            "Service": 
            [
              "iot.amazonaws.com"
            ]
          },
          "Action": "sts:AssumeRole"
        }
      ]
    }
  • 创建 IAM Role,记录下 Arn
    $ aws iam create-role --role-name iot-access-s3 --assume-role-policy-document file://trust-policy.json
    # 输出结果
    {
        "Role": {
            "Path": "/",
            "RoleName": "iot-access-s3",
            "RoleId": "AROAVQEJMH4ZH2DLDZHC4",
            "Arn": "arn:aws:iam::378245234483:role/iot-access-s3",
            "CreateDate": "2024-01-01T11:48:24+00:00",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Sid": "",
                        "Effect": "Allow",
                        "Principal": {
                            "Service": [
                                "iot.amazonaws.com"
                            ]
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            }
        }
    }
  • 编写一个从 S3 存储桶下载文件的 policy 文档,复制以下 JSON 格式的策略并保存为 s3-policy.json 文件,Arn 中替换成自己的 bucket name
    $ vi s3-policy.json
    {
        "Version": "2012-10-17",
        "Statement": 
        [
            {
                "Effect": "Allow",
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::example-bucket-202401/*"
            }
        ]
    }
  • 创建 policy,记录下 Arn
    $ aws iam create-policy --policy-name iot-access-s3 --policy-document file://s3-policy.json
    # 输出结果
    {
        "Policy": {
            "PolicyName": "iot-access-s3",
            "PolicyId": "ANPAVQEJMH4ZELOU6YRKJ",
            "Arn": "arn:aws:iam::378245234483:policy/iot-access-s3",
            "Path": "/",
            "DefaultVersionId": "v1",
            "AttachmentCount": 0,
            "PermissionsBoundaryUsageCount": 0,
            "IsAttachable": true,
            "CreateDate": "2024-01-01T11:49:34+00:00",
            "UpdateDate": "2024-01-01T11:49:34+00:00"
        }
    }
  • 挂载 policy iot-access-s3 到 role iot-access-s3,替换 arn 为上一步的 arn
    $ aws iam attach-role-policy --role-name iot-access-s3 --policy-arn arn:aws:iam::378245234483:policy/iot-access-s3

第四步 – 运行 IoT 设备端程序

$ pwd
# 输出结果
/home/ec2-user/aws-iot-device-sdk-js/examples
  1. 查看自己的 AWS IoT Endpoint
    $ aws iot describe-endpoint --endpoint-type iot:Data-ATS
    # 输出结果
    {
        "endpointAddress": "a1oqv3t6tjqroz-ats.iot.us-east-1.amazonaws.com"
    }
  1. 运行客户端程序  jobs-agent.js,并等待 jobs 的提交,注意将 endpoint 改为上一步获取的自己的 endpoint 地址
    $ node jobs-agent.js -f ~/aws-iot-device-sdk-js/examples/certs -H a1oqv3t6tjqroz-ats.iot.us-east-1.amazonaws.com -T aws-iot-device-sdk-js -D
    # 输出结果
    {
      keyPath: '/home/ec2-user/aws-iot-device-sdk-js/examples/certs/private.pem.key',
      certPath: '/home/ec2-user/aws-iot-device-sdk-js/examples/certs/certificate.pem.crt',
      caPath: '/home/ec2-user/aws-iot-device-sdk-js/examples/certs/root-CA.crt',
      clientId: 'ec2-user23616',
      region: undefined,
      baseReconnectTimeMs: 4000,
      keepalive: 300,
      protocol: 'mqtts',
      port: 8883,
      host: 'a1oqv3t6tjqroz-ats.iot.us-east-1.amazonaws.com',
      thingName: 'aws-iot-device-sdk-js',
      debug: true,
      username: '?SDK=JavaScript&Version=2.0.0-dev',
      reconnectPeriod: 4000,
      fastDisconnectDetection: true,
      resubscribe: false,
      servername: 'a1oqv3t6tjqroz-ats.iot.us-east-1.amazonaws.com',
      key: <Buffer 2d 2d 2d 2d 2d 42 45 47 49 4e 20 52 53 41 20 50 52 49 56 41 54 45 20 4b 45 59 2d 2d 2d 2d 2d 0a 4d 49 49 45 70 41 49 42 41 41 4b 43 41 51 45 41 76 4b ... 1629 more bytes>,
      cert: <Buffer 2d 2d 2d 2d 2d 42 45 47 49 4e 20 43 45 52 54 49 46 49 43 41 54 45 2d 2d 2d 2d 2d 0a 4d 49 49 44 57 54 43 43 41 6b 47 67 41 77 49 42 41 67 49 55 55 4a ... 1170 more bytes>,
      ca: <Buffer 2d 2d 2d 2d 2d 42 45 47 49 4e 20 43 45 52 54 49 46 49 43 41 54 45 2d 2d 2d 2d 2d 0a 4d 49 49 44 51 54 43 43 41 69 6d 67 41 77 49 42 41 67 49 54 42 6d ... 1138 more bytes>,
      requestCert: true,
      rejectUnauthorized: true
    }
    attempting new mqtt connection...
    subscribeToJobs: { thingName: 'aws-iot-device-sdk-js', operationName: 'shutdown' }
    subscribeToJobs: { thingName: 'aws-iot-device-sdk-js', operationName: 'reboot' }
    subscribeToJobs: { thingName: 'aws-iot-device-sdk-js', operationName: 'install' }
    subscribeToJobs: { thingName: 'aws-iot-device-sdk-js', operationName: 'systemStatus' }
    subscribeToJobs: { thingName: 'aws-iot-device-sdk-js', operationName: 'stop' }
    subscribeToJobs: { thingName: 'aws-iot-device-sdk-js', operationName: 'start' }
    subscribeToJobs: { thingName: 'aws-iot-device-sdk-js', operationName: 'restart' }
    startJobNotifications: { thingName: 'aws-iot-device-sdk-js' }
    agent connected
    startJobNotifications completed for thing: aws-iot-device-sdk-js

第五步 – 创建 AWS IoT Jobs 进行固件升级

  1. 新开一个命令行窗口到 EC2 实例,查看当前固件版本
    $ rpm -qa |grep telnet
    # 输出结果
    telnet-0.17-76.el8.x86_64
  1. 创建 AWS IoT Jobs,#注意这里的 –targets,example-bucket-123 和 roleArn 要替换成自己的
    $ cd /home/ec2-user/aws-iot-device-sdk-js/examples
    $ aws iot create-job --job-id 1 --targets arn:aws:iot:us-east-1:378245234483:thing/aws-iot-device-sdk-js --document-source https://example-bucket-202401.s3.amazonaws.com/jobs-document.json --presigned-url-config "{\"roleArn\":\"arn:aws:iam::378245234483:role/iot-access-s3\", \"expiresInSec\":3600}" --target-selection SNAPSHOT
    {
        "jobArn": "arn:aws:iot:us-east-1:378245234483:job/1", 
        "jobId": "1"
    }

第六步 – 验证固件升级是否成功

  1. 查看之前 IoT 设备端程序输出
    # 输出结果
    agent connected
    startJobNotifications completed for thing: aws-iot-device-sdk-js
    job execution handler invoked: { thingName: 'aws-iot-device-sdk-js', operationName: 'install' }
    updateJobStatus: {
      thingName: 'aws-iot-device-sdk-js',
      jobId: '1',
      status: 'IN_PROGRESS',
      statusDetails: { step: 'downloading', fileName: 'new-firmware.rpm' }
    }
    updateJobStatus: {
      thingName: 'aws-iot-device-sdk-js',
      jobId: '1',
      status: 'IN_PROGRESS',
      statusDetails: { operation: 'install', step: 'restarting package' }
    }
    updateJobStatus: {
      thingName: 'aws-iot-device-sdk-js',
      jobId: '1',
      status: 'SUCCEEDED',
      statusDetails: { operation: 'install', state: 'package installed and started' }
    }
  1. 查看 IoT Job 状态
    $ aws iot describe-job --job-id 1
    # 输出结果
    {
        "documentSource": "https://example-bucket-202401.s3.amazonaws.com/jobs-document.json",
        "job": {
            "jobArn": "arn:aws:iot:us-east-1:378245234483:job/1",
            "jobId": "1",
            "targetSelection": "SNAPSHOT",
            "status": "COMPLETED",
            "targets": [
                "arn:aws:iot:us-east-1:378245234483:thing/aws-iot-device-sdk-js"
            ],
            "presignedUrlConfig": {
                "roleArn": "arn:aws:iam::378245234483:role/iot-access-s3",
                "expiresInSec": 3600
            },
            "jobExecutionsRolloutConfig": {},
            "createdAt": "2024-01-01T11:54:55.659000+00:00",
            "lastUpdatedAt": "2024-01-01T11:55:00.443000+00:00",
            "completedAt": "2024-01-01T11:55:00.443000+00:00",
            "jobProcessDetails": {
                "numberOfCanceledThings": 0,
                "numberOfSucceededThings": 1,
                "numberOfFailedThings": 0,
                "numberOfRejectedThings": 0,
                "numberOfQueuedThings": 0,
                "numberOfInProgressThings": 0,
                "numberOfRemovedThings": 0,
                "numberOfTimedOutThings": 0
            },
            "timeoutConfig": {},
            "schedulingConfig": {}
        }
    }
  1. 查看固件版本号
  2. 程序版本已由 telnet-0.17-76.el8.x86_64 升级到 telnet-0.17-85.el9.x86_64
    $ rpm -qa |grep telnet
    # 输出结果
    telnet-0.17-85.el9.x86_64

到此为止,通过以上几步简单的动手环节,您已成功地完成了 OTA 升级。

本篇作者

郭松

亚马逊云科技解决方案架构师,负责企业级客户的架构咨询及设计优化,同时致力于 AWS IoT 和存储服务在国内和全球企业客户的应用和推广。加入亚马逊云科技之前在 EMC 研发中心担任系统工程师,对企业级存储应用的高可用架构,方案及性能调优有深入研究。