はじめに
今年も始まりましたACCESSアドベントカレンダー2020。最初の投稿をさせていただきます、@ufoo68です。
今回は、普段あまり使わないAWS IoTのことについて書いてみようと思います。
今回のネタ
AWS IoTを用いるとデバイスの認証を簡単に実現できますが、例えばスマホやPCなどのUI画面からデバイスを制御する場合のそれぞれの認証ってどう実装するのだろう?と思ったので調べてみました。こういう公式ページが見つかりましたが具体的な実現方法がわからなかったので、実装してみることにしました。
ちなみに今回もAWS CDKを使ってインフラをコードで管理したいと思います。
システム構成など
実現にしたいものはこんな感じのものです↓。
- メッセージ送信のためのUIアプリがあり、Cognitoで認証されたユーザーのみが送信可能
- 証明書で認証されたデバイスのみがメッセージの受信可能
IoTの構築
まずはIoTに関するシステムを構築してみます。事前にマネジメントコンソールで証明書を発行します。このとき証明書のARNをCDKのコードに直書きはしたくなかったので、AWS Systems Manager パラメータストアを使ってやり取りを行いました。以下のようにAWS CLIを実行します。
aws ssm put-parameter --name "iot-cert" --type String --value {{arn}}
IoT側はこのように実装しました。
import * as cdk from '@aws-cdk/core'
import * as iot from '@aws-cdk/aws-iot'
import * as ssm from '@aws-cdk/aws-ssm'
export class IotStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const projectName = 'iot_cognito'
const policyName = `${projectName}_policy`
const iotPolicy = new iot.CfnPolicy(this, 'IotPolicy', {
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: 'iot:*',
Resource: '*',
},
],
},
policyName,
})
const thingName = `${projectName}_thing`
const iotThing = new iot.CfnThing(this, 'IotThing', { thingName })
const ioTCertificateArn = ssm.StringParameter.fromStringParameterAttributes(this, 'IotCertArn', {
parameterName: 'iot-cert-arn',
}).stringValue
const iotPolicyPrincipalAttachment = new iot.CfnPolicyPrincipalAttachment(
this,
'IotPolicyPrincipalAttachment',
{
policyName,
principal: ioTCertificateArn,
}
)
iotPolicyPrincipalAttachment.addDependsOn(iotPolicy)
const iotThingPrincipalAttachment = new iot.CfnThingPrincipalAttachment(
this,
'IotThingPrincipalAttachment',
{
thingName,
principal: ioTCertificateArn,
}
)
iotThingPrincipalAttachment.addDependsOn(iotThing)
}
}
やっていることは単純で、事前に発行した証明書にThingとPolicyを紐付けているだけです。
Cognitoの構築
これが若干面倒でした。
import * as cdk from '@aws-cdk/core'
import * as cognito from '@aws-cdk/aws-cognito'
import * as iam from '@aws-cdk/aws-iam'
export class CognitoStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const userPool = new cognito.UserPool(this, 'UserPool', {
signInAliases: {
email: true,
},
selfSignUpEnabled: true,
})
const userPoolClient = new cognito.UserPoolClient(this, 'UserPoolClient', {
userPool,
authFlows: {
userPassword: true,
adminUserPassword: true,
userSrp: true,
}
})
const identityPool = new cognito.CfnIdentityPool(this, 'IdentityPool', {
allowUnauthenticatedIdentities: true,
cognitoIdentityProviders: [{
clientId: userPoolClient.userPoolClientId,
serverSideTokenCheck: false,
providerName: userPool.userPoolProviderName,
}]
})
const unAuthRole = new iam.Role(this, 'UnAuthRole', {
assumedBy: new iam.FederatedPrincipal('cognito-identity.amazonaws.com', {
StringEquals: {
['cognito-identity.amazonaws.com:aud']: identityPool.ref,
},
['ForAnyValue:StringLike']: {
['cognito-identity.amazonaws.com:amr']: 'unauthenticated'
},
}, 'sts:AssumeRoleWithWebIdentity'),
})
const authRole = new iam.Role(this, 'AuthRole', {
assumedBy: new iam.FederatedPrincipal('cognito-identity.amazonaws.com', {
StringEquals: {
['cognito-identity.amazonaws.com:aud']: identityPool.ref,
},
['ForAnyValue:StringLike']: {
['cognito-identity.amazonaws.com:amr']: 'authenticated'
},
}, 'sts:AssumeRoleWithWebIdentity'),
managedPolicies: [iam.ManagedPolicy.fromManagedPolicyArn(this, 'AWSIoTFullAccess', 'arn:aws:iam::aws:policy/AWSIoTFullAccess')]
})
new cognito.CfnIdentityPoolRoleAttachment(this, 'IdentitiyRole', {
identityPoolId: identityPool.ref,
roles: {
authenticated: authRole.roleArn,
unauthenticated: unAuthRole.roleArn,
},
})
}
}
まずはユーザープールをつくり、ユーザープールクライアント、IDプールを作成します。このIDプールがIoT CoreとCognitoを連携させるために必要なものになります。
unAuthRole
とauthRole
を定義して、認証されたユーザーにauthRole
を付与してAWS IoTの操作権限を渡します。
ここまでのコード全般はこちらで公開しています。
AWS IoTとCognitoの紐付け
この作業は、Cognitoで作成したユーザーのアイデンティティIDをAWS IoT Coreのポリシーに紐付けることをします。なのでどうしても手作業が生じるのでCDKでの実装は難しそうでした。
ここでユーザー作成とメッセージ送信用のUIを作ってしまったほうが楽だったので、こちらを参考に先にUIを作りました。さっと作りたかったのでcreate-react-app
を使いました。
create-react-app iot-cognito-ui
Cognito認証とデータのパブリッシュを簡単に使うためのaws-amplifyライブラリをインストールします。
yarn add aws-amplify aws-amplify-react
コード全般はこちらで公開しています。Amplifyを使いましたが今回はライブラリの使用のみで特にデプロイはせずにローカルでの実行を行いました。withAuthenticator()
を使うとこんな感じ↓のサインアップ・サインイン画面を勝手に作ってくれるので今回はこれでユーザー作成をしてみます。
ユーザー作成を行うとアイデンティティIDとうものがIDプールで作成されたことが確認できるので、それをIoT Coreのポリシーに紐付けます。以下のコマンドを実行します。
aws iot attach-policy --policy-name {{policy name}} --target {{cognito identity id}}
{{policy name}}
はCDKで作成したものを、{{cognito identity id}}
はIDプールで作成されたものを当てはめます。
マネジメントコンソール上で、作成したポリシーに新たにCOGNITO ID
と書かれた証明書が追加されていたら成功です。
動作確認
サーバー側の実装が完了したので、あとはクライアントアプリを作成して動作確認をしてみようと思います。本当は実デバイスを用意してやってみようと思ったのですが、このサーバー側の構築に手間取って気力を奪われてしまったので、PC上で疑似デバイス用のプログラムを作成してすべてPC上で動きを確認してみようと思います。
まずメッセージを送るためのUIアプリを、先程のcreate-react-appのテンプレートからApp.js
を変更して適当なUIを作成します。
import React, { useState } from 'react'
import './App.css'
import { withAuthenticator } from 'aws-amplify-react'
import Amplify, { PubSub } from 'aws-amplify'
import { AWSIoTProvider } from '@aws-amplify/pubsub'
import cryptoRandomString from 'crypto-random-string'
Amplify.configure({
Auth: {
identityPoolId: process.env.REACT_APP_IDENTITY_POOL_ID,
region: process.env.REACT_APP_REGION,
userPoolId: process.env.REACT_APP_USER_POOL_ID,
userPoolWebClientId: process.env.REACT_APP_USER_CLIENT_ID,
}
})
Amplify.addPluggable(new AWSIoTProvider({
aws_pubsub_region: process.env.REACT_APP_REGION,
aws_pubsub_endpoint: `wss://${process.env.REACT_APP_PUBSUB_ENDPOINT}/mqtt`,
clientId: cryptoRandomString({length: 10}),
}))
const App = () => {
const [message, setMessage] = useState('')
const [topic, setTopic] = useState('')
const handleChangeTopic = (event) => {
setTopic(event.target.value)
}
const handleChangeMessage = (event) => {
setMessage(event.target.value)
}
const handlePublish = () => {
PubSub.publish(topic, message)
console.log('send')
}
return (
<div className="App">
<div>AWS IoT test UI</div>
<div>
<label>TOPIC:
<input type="text" onChange={handleChangeTopic}></input>
</label>
</div>
<div>
<label>MESSAGE:
<input type="text" onChange={handleChangeMessage}></input>
</label>
</div>
<button onClick={handlePublish}>publish</button>
</div>
)
}
export default withAuthenticator(App)
とりあえずtopic名とメッセージ内容をtestで送信してみます。
受信用の疑似デバイスのコードも作成します(githubはこちら)。このプログラムでは、マネジメントコンソールで発行した証明書を用いて認証を行っています。
import awsIot from 'aws-iot-device-sdk'
import dotenv from 'dotenv'
dotenv.config()
const device = new awsIot.device({
keyPath: process.env.KEY_PATH,
certPath: process.env.CERT_PATH,
caPath: process.env.CA_PATH,
clientId: process.env.CLIENT_ID,
host: process.env.HOST,
})
device
.on('connect', () => {
console.log('connect')
device.subscribe(process.argv[2])
})
device
.on('message', (topic, payload) => {
console.log('message', topic, payload.toString())
})
こちらも同様にtestというトピック名をSubscribeするように実行します(process.argv[2]
でコマンド引数でトピック名を渡します)。
node index test
するとちゃんとUIから送ったメッセージを受信することができました。
message test "test"
さいごに
最終的な成果物としては、単純にウェブアプリから送ったメッセージを受信するだけのものになりましたが、お互いに別々の形での認証を行った上でAWS IoT Coreを介したメッセージのやり取りを実現することができました。
正直手作業が多々あったのでCDKでのインフラ管理が完全にできていないのが残念ではありますが、それは今後の課題ということで今回の投稿を終了したいと思います。
明日は@KensukeTakaharaさんの投稿です。お楽しみに!