こんにちは。しげぞうです。
みなさん、AWS SAMでアプリ開発はされてますか?
AWS SAMは、AWS Serverless Application Model の略で、サーバーレスなアプリケーションを簡単に構築できるフレームワークになります。アプリケーションに必要なソースコードはもちろんのこと、アプリ上のデータを管理するデータストア(データベース)や認証などの関連リソースもSAMで開発することで一括管理することができます。
SAMについてもう少し詳しく
AWS SAMでできることのメインは次の2つです。
①サーバーレス環境アーキテクチャのリソース管理
SAMは、yamlやjsonファイル形式でAWSリソースを定義できるCloudFormationというサービスを内包しています。
厳密には一緒ではない(SAMではSAMテンプレートと呼ぶそう)ですが、CloudFormationでAWSリソースの定義をしていく要領でアプリケーションの実行環境を構築していきます。
↓SAMテンプレートの例↓
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
slack-golang-app
Globals:
Function:
Timeout: 5
Resources:
# API Gatewayの定義
SlackGolangAppApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Stage
OpenApiVersion: 3.0.2
EndpointConfiguration:
Type: REGIONAL
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: openapi.yaml # swaggerファイルのパス指定
# Lambdaの定義
GetNewsFunction:
Type: AWS::Serverless::Function
Dependson: [
GetNewsFuncLogGroup,
LambdaFunctionBaseRole
]
Properties:
FunctionName: getNews
CodeUri: get-news/
Handler: get-news
Runtime: go1.x
Architectures:
- x86_64
Tracing: PassThrough
MemorySize: 128
Timeout: 300
Role: !GetAtt LambdaFunctionBaseRole.Arn
Events:
CatchAll:
Type: Api
Properties:
Path: /get/news
Method: POST
RestApiId:
Ref: SlackGolangAppApi
②サーバーレス環境で実行するプラムの作成(コーディング)
AWSでサーバーレスといえば、Lambdaがまず思い浮かぶと思います。
※Lambda:開発者側でのサーバー管理なしにアプリケーションコードを実行できるサービス
SAMでは、そのLambdaの実行コードをローカル(Dockerコンテナ)上でコーディングしたりテストすることが可能で、
実際の現場での開発フローに採用したくなるフレームワークとなっています。
// get-news/main.go
package main
import (
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// アプリケーションコードを記述
return events.APIGatewayProxyResponse{
Body: "OK",
StatusCode: 200,
}, nil
}
// Lambda関数実行時に呼ばれる
func main() {
lambda.Start(handler)
}
SAMでアプリ開発してみた
試しにSlack上で動く簡単なBotアプリを作成してみることにしました。
コーディングは、以前から興味のあったGo言語で。
アプリとしては、キーワードを与えてその内容に該当するニュース情報を返却してくれるシンプルなものになります。
実際の成果物としては、次のようなものが完成しました。
SAMのアプリ開発における振る舞いとSlackとの連携は以下のようなイメージになります。
上記を説明すると、 次のような流れになります。
① まず、アプリで実行するプログラム(ニュースを取得する処理)をローカルで実装/検証する
② ①で作成したものをクラウド上(Lambda,API Gateway)にデプロイする
③ SlackAppにエンドポイント(API Gatewayエンドポイント)を設定し、Slackインターフェースとアプリロジックを連携させる
SAMは、上記①②の開発作業を担います。
使い慣れたローカルの環境(エディタ&CLI)でシームレスに行えるのが強みなサービスです。
開発にあたっての環境構築や実際のソースコード/ビルド方法などはついてはここでは割愛します。
興味のある方は、こちらのリポジトリをご覧ください。
以降は、今回のアプリ開発でハマったことや工夫したことを少し紹介させていただきます。
ハマったこと
Lambda関数とLambda実行ロールの依存関係
AWS SAMでLambda関数をデプロイすると、最小の実行権限を持ったLambda実行ロールが合わせて作成されます。
今回、Lambda関数から他のAWSサービスへアクセスする必要があったため、明示的に適したLambda実行ロールを準備し、Lambda関数に紐づけてやる必要がありました。
しかし、以下のように実行ロール作成の定義を追記するだけではダメで、デプロイ時のリソースの依存関係でエラーとなってしまいました。
# template.yaml
GetNewsFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: getNews
CodeUri: get-news/
Handler: get-news
# ############################################################
# ↓実行ロールのリソースを指定↓
# 指定したリソースが既に作成(or 先に作成されている)されていないとエラーになる
# ############################################################
Role: !GetAtt LambdaFunctionBaseRole.Arn
~~~
# ############################################
# ↓Lambda実行用ロールの定義を追記↓
# ############################################
LambdaFunctionBaseRole: # lambdaの実行ロール
Type: AWS::IAM::Role
Properties:
RoleName: slack-golang-app-lambda-base-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess
そこで使えるのが、DependsOn句です。
DependsOnは、SAMやCloudFormationでAWSサービスを構築するときに、サービスの構築順序を指定することができます。
# template.yaml
GetNewsFunction:
Type: AWS::Serverless::Function
# ###########################################################
# ↓Lambda関数を作成する前に構築しておくリソースを指定↓
# GetNewsFunction作成前に、LambdaFunctionBaseRoleを作成する
# ###########################################################
DependsOn: [
LambdaFunctionBaseRole
]
Properties:
FunctionName: getNews
CodeUri: get-news/
Handler: get-news
Role: !GetAtt LambdaFunctionBaseRole.Arn
~~~
LambdaFunctionBaseRole: # lambdaの実行ロール
Type: AWS::IAM::Role
Properties:
RoleName: slack-golang-app-lambda-base-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess
上記のように定義することで、無事デプロイできました。
このリソースの依存関係問題はSAMやCloudFormationを利用する際は注意していきたいところです。
SlackAppとアプリ(API)を連携する際のチャレンジ認証
SlackAppをよく使う方にとっては馴染みがあると思いますが、SlackAppと自作のプログラムを連携させるときの関門として、Challenge認証(エンドポイントURLの検証)というものがあります。
まず、SlackAppのエンドポイントにURLを設定すると下記のようなRequestBodyをもつPOSTが送信されます。(↓公式のサンプルです)
{
"token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
"type": "url_verification"
}
上記のうち、challengeキーの値をそのままResponseBodyに返してやることで、そのエンドポイントURLがSlackAppで利用可能と判断され、使用できるようになります。
API Gateway側でRequestBodyをやりくりするスマートな方法がありそうな気もしますが、今回はLambdaでChallenge認証用のロジックを作成して対応しました。
// get-news/main.go
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// RequestBodyを構造体形式で取得
requestBody, err := getRequestBody(request.Body)
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
event := requestBody.Event
challenge := requestBody.Challenge
// challengeパラメータが渡されている場合、Challenge認証用のレスポンスを返却
if challenge != "" {
responseBody, err := json.Marshal(ChallengeResponseBody{
Challenge: challenge,
})
_ = err
return events.APIGatewayProxyResponse{
Body: string(responseBody),
StatusCode: 200,
}, nil
}
// ~~~
}
工夫したこと
SlackAPIにアクセスするための認証キーを安全に管理する
アプリからSlackにアクセスするためには、Slack側で発行した認証キーが必要になります。
認証キーをソースコードにハードコーディングすると、ソースコードが外部に漏洩した際に不正アクセスの原因となってしまいます。
そのため、認証キーのような機密情報はソースコードとは切り分けて別で管理できるのようにしておくのが理想です。
Lambdaにおける認証キーの取り扱いとして環境変数を利用する方法がよくありますが、今回は認証キーをより安全に管理できるSSMのパラメータストアを利用する方法を採用しました。
SAMテンプレート(template.yaml)においてパラメータストア(Key-Value)は下記のように定義します。
# template.yaml
Parameters:
SsmParamSlackAuthTokenKeyName:
Type: String
SsmParamSlackAuthTokenValue:
Type: String
Resources:
SsmParamSlackAuthKey:
Type: AWS::SSM::Parameter
Properties:
Description: SlackAPI Auth Key
Name: !Ref SsmParamSlackAuthTokenKeyName # Key名を指定
Type: String
Value: !Ref SsmParamSlackAuthTokenValue # 保存する値を指定
SsmParamSlackAuthTokenKeyNameとSsmParamSlackAuthTokenValueは、SAMのParameters構文でデプロイ時に外から渡せるようにしています。
// get-news/main.go
import (
"log"
"github.com/slack-go/slack"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ssm"
)
/// Slackへメッセージを投稿する
func sendSlackMessage(channel, text string) {
// パラメータストアより認証キーを取得
res, err := fetchParameterStore(getEnv("SSM_SLACK_AUTH_KEY_NAME", ""))
if err != nil {
log.Print(err)
return
}
client := slack.New(res)
_, _, err = client.PostMessage(channel, slack.MsgOptionText(text, true))
if err != nil {
// エラーログを出力
log.Print(err.Error())
}
}
/// パラメータストアから値を取得する
func fetchParameterStore(paramName string) (string, error) {
sess := session.Must(session.NewSession())
svc := ssm.New(
sess,
aws.NewConfig().WithRegion(getEnv("SSM_REGION", "")),
)
res, err := svc.GetParameter(&ssm.GetParameterInput{
Name: aws.String(paramName),
WithDecryption: aws.Bool(true),
})
if err != nil {
return "SSM: パラメータ取得失敗", err
}
value := *res.Parameter.Value
return value, nil
}
上記のようにsession,ssmライブラリを使用して、SSMのパラメータストアに格納した認証キーを取得することができます。
SSMのパラメータストアでは、格納した値の暗号化管理が可能なため、よりセキュアに機密情報が管理できます。
最後に
駆け足になりましたが、AWS SAMを使ったアプリ開発でハマったところなどを書かせてもらいました。
2~3年くらいAWSを触っていますが、サーバーレスなアプリ開発をプロダクションレベルで行えていないのが悔しいとところ。。
AWS Amplifyもだいぶ整備されてきましたし、引き続き、手軽にアプリ開発できる仕組みの検討を進めていきたいです。

