Amazon Alexa
AWS
SAM

Alexa SkillをCloudFormation(SAM)で管理する

re:inventでしれっと発表された、Alexa::Ask::Skillが気になって仕方なかったので挑戦して来ました。

事前準備

まずS3にSkill PackageのZIPファイルを用意する必要があります。作り方については以前記事を書いていますので、そちらを参考にしてください。

これをS3にアップロードしておきましょう。

Lambdaのソースコードを用意する

今回のディレクトリ構成です。Lambdaのコードをルートディレクトリに置いていますが、特に理由はありませんので好みで変えてください。

$ tree -I node_modules
.
├── .envrc
├── deploy.sh
├── index.js
├── package-lock.json
├── package.json
└── template.yml

0 directories, 6 files

環境変数で以下を使います。実運用時にはSSMへ置いておくとよいでしょう。

export S3_BUCKET=YOUR_S3_BUCKET_NAME
export ClientId=LWA_CLIENT_ID
export ClientSecret=LWA_CLIENT_SECRET
export RefreshToken="Atzr|LWA_REFRESH_TOKEN"
export VendorId=VENDOR_ID
export SkillPackageSourceBucketKey=skillpackage.zip
export SkillPackageSourceBucketName=YOUR_S3_BUCKET_NAME

Lambdaのコードは以下のようにしました。最小構成です。

/* eslint-disable  func-names */
/* eslint-disable  no-console */
const Alexa = require('ask-sdk-core');

const LaunchRequestHandler = {
    canHandle: (handlerInput) => true,
    handle(handlerInput) {
        const speechText = 'Welcome to the Alexa Skills Kit, you can say hello!';
        return handlerInput.responseBuilder.speak(speechText)
            .reprompt(speechText)
            .withSimpleCard('Hello World', speechText)
            .getResponse();
    },
};

const ErrorHandler = {
    canHandle: (handlerInput) => true,
    handle(handlerInput, error) {
        console.log(`Error handled: $ {
            error.message
        }`);

        return handlerInput.responseBuilder.speak('Sorry, I can\'t understand the command. Please say again.')
            .reprompt('Sorry, I can\'t understand the command. Please say again.')
            .getResponse();
    },
};

const skillBuilder = Alexa.SkillBuilders.custom();

exports.handler = skillBuilder.addRequestHandlers(LaunchRequestHandler)
    .addErrorHandlers(ErrorHandler)
    .lambda();

CloudFormation Templateの作成

いよいよテンプレートを作りましょう。Lambdaをデプロイするので、SAMを使います。

---
AWSTemplateFormatVersion: '2010-09-09'
Description: example alexa skills
Transform: 'AWS::Serverless-2016-10-31'
Parameters:
  ClientId:
    Description: The client ID of the application registered for Login with Amazon (LWA). This information can be found on the Amazon developer portal?s LWA page.
    Type: String
  ClientSecret:
    Description: The client secret of the application registered for Login with Amazon (LWA). This information can be found on the Amazon developer portal?s LWA page.
    NoEcho: true
    Type: String
  RefreshToken:
    Description: The refresh token used to request new access tokens from LWA.
    NoEcho: true
    Type: String
  SkillPackageSourceBucketKey:
    Description: The location and name of the .zip file that contains the skill package.
    Type: String
  SkillPackageSourceBucketName:
    Description: The name of the Amazon S3 bucket where the .zip file that contains the skill package is stored.
    Type: String
  VendorId:
    Description: The unique identifier of an Amazon digital vendor account.
    Type: String
  TestingInstructions:
    Description: Testing instructions about your skill for review
    Type: String
  SkillSummary:
    Type: String
  SkillDescription:
    Type: String
  SkillName:
    Type: String
Resources:
  AlexaSkillsKitServiceRole:
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action:
          - sts:AssumeRole
          Condition:
            StringEquals:
              sts:ExternalId: !Ref 'VendorId'
          Effect: Allow
          Principal:
            Service: alexa-appkit.amazon.com
        Version: 2012-10-17
      Policies:
      - PolicyDocument:
          Statement:
          - Action:
            - s3:GetObject
            Effect: Allow
            Resource: !Sub 'arn:aws:s3:::${SkillPackageSourceBucketName}/${SkillPackageSourceBucketKey}'
          Version: 2012-10-17
        PolicyName: !Sub 'MyExample-AlexaSkillsKitServiceRolePolicy'
      RoleName: !Sub 'MyExample-AlexaSkillsKit'
    Type: AWS::IAM::Role
  SkillFunction:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: index.handler
      Runtime: nodejs8.10
      Events:
        AlexaSkillEvent:
          Type: AlexaSkill
  AlexaSkill:
    DependsOn:
      - AlexaSkillsKitServiceRole
    Type: "Alexa::ASK::Skill"   
    Properties:
      SkillPackage:
        S3Bucket: !Ref 'SkillPackageSourceBucketName'
        S3BucketRole: !GetAtt 'AlexaSkillsKitServiceRole.Arn'
        S3Key: !Ref 'SkillPackageSourceBucketKey'
        Overrides:
          Manifest:
            publishingInformation:
              locales:
                ja-JP:
                  name: !Ref SkillName
                  description: !Ref SkillDescription
                  summary: !Ref SkillSummary
              testingInstructions: !Ref TestingInstructions
            apis:
              custom:
                endpoint:
                  uri: !GetAtt SkillFunction.Arn
      AuthenticationConfiguration:
        ClientId: !Ref 'ClientId'
        ClientSecret: !Ref 'ClientSecret'
        RefreshToken: !Ref 'RefreshToken'
      VendorId: !Ref 'VendorId'

Outputs:
  skillId:
    Description: Your Alexa Skill ID
    Value: !Ref AlexaSkill
  LambdaFunctionArn:
    Description: ARN of your Lambda Function
    Value: !GetAtt SkillFunction.Arn

こいつで作成しているのは以下の3つです。

  • Alexa側でS3のSkill PackageファイルをDLするためSTSを提供するIAMロール
  • Alexaが使用するLambdaファンクション
  • Alexaスキルそのもの

基本的にはSkill PackageのZIPファイルにある内容で対話モデルとスキル情報が作成されますが、SkillPackage.Overrides:でmanifestのみ上書き可能です。上記サンプルではスキル名や説明文をCloudFormationのパラメーターで上書きしようとしています。

CloudFormationでデプロイする

最後にデプロイしましょう。build.shに以下のようなコマンドを入れておくと便利です。

#!/usr/bin/env bash
# ビルドパッケージ作成
aws cloudformation package --template-file ./template.yml --output-template-file template-output.yml --s3-bucket $S3_BUCKET
# デプロイ
aws cloudformation deploy \
 --template-file ./template-output.yml --stack-name alexa-cfn --capabilities CAPABILITY_NAMED_IAM --region $AWS_REGION \
 --parameter-overrides \
  ClientId=$ClientId \
  ClientSecret=$ClientSecret \
  RefreshToken=$RefreshToken \
  SkillPackageSourceBucketKey=$SkillPackageSourceBucketKey \
  SkillPackageSourceBucketName=$SkillPackageSourceBucketName \
  VendorId=$VendorId \
  TestingInstructions='テスト' \
  SkillSummary='これはCloudFormationテストです' \
  SkillDescription='これはCloudFormationテストです' \
  SkillName='テストスキル' 
# Skill ID / Lambda ARNの確認
aws cloudformation describe-stacks --stack-name alexa-cfn | jq .Stacks[0].Outputs

あとはこれを実行してやればOKです。

トラブルシューティング

やっていてハマったのはだいたい以下の点です。

  • LWAのtoken取得に失敗してスタック作成失敗
  • IAMロール作り忘れてS3のzipファイルにアクセスできなくなる
  • Alexa::Ask::SkillをDependsOnさせわすれてやはりS3にアクセスできなくなる

この辺りでめげた人はCodeStar推奨です。

Overwrideについて

いろいろ試してみましたが、基本的にはあまり使わない方が良さそうです。というのもexamplePhrasesを入れ替えようとしたときに、以下のエラーが出て来ました。

Skill Update failed. Error: publishingInformation.locales.ja-JP.examplePhrases – array is too long: must have at most 3 elements but instance has 6 elements

「3つしか許可していないのに、6つある」と言われているということは、置換ではなく追加されている様子です。そしてexamplePharsesを変えれないのにスキル名や説明文をここでoverwrideしてもレビューで引っかかるでしょう。

ということで、Skill情報についてはCloudFormationで無理に管理しない方が良さそうです。

結論

CodeStarに一式あるので、大人しくそれを使いましょう。ただしスキル数が増えてくると、CodePipelineやCodeCommit / Cloud9などで課金が来るので要注意です。

たぶん使い分けとしてはこういう形になるかなと思っています。

  • チームでAlexaスキルを効率的に作りたい -> CodeStar
  • 個人でゴミやトリビアスキルなどの横展開系スキルを作りたい -> SAM / CloudFormation
  • TypeScript / Javaが至高 -> AWS CDK
  • あまりAWSのサービスに依存したくない -> Serverless Framework
  • そこまでCI / CDやリソース管理にこだわりない -> ASK CLI
  • AWSアカウント持ってない -> Alexa Hosted Skill

コアの実装流用してスキル作ることが多いので、もうちょっとSAM / CDKまわり頑張ってみる予定です。


Random posts

GitHubHomeEnglish