※2020/12/6追記 DependsOnを使ってCloudFromationテンプレートを書き換えました。
はじめに
こんにちは!
本記事は先日公開した【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る!第1回(全3回)の続きとなります。
前回はSwaggerで記載したAPI定義を取り込んだAPI Gatewayと簡単なメッセージを返すだけのLambda関数を実装し、Serverless Frameworkを使ってAWSへデプロイするところまで行いました。
第2回ではAWS WAFを適用し、特定のIPからしかAPI Gatewayにアクセスできないようにします。これはAPI Gatewayはパブリックに公開されてしまうので、なにもアクセス制限をしないとURLさえ知っていればアクセスできてしまうためです。IP制限をかけることで安心して開発ができるようになります。
前回、第三回の記事はこちら
- 【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る!第1回(全3回)
- 【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る!第3回 Dynamo編(全3回)
今回作るもの(再掲)
今回は以下のアーキテクト図のようなWeb APIバックエンドを作っていきます。
API GatewayでクライアントからのAPIリクエストを受信し、該当するLambda関数を呼び出し、必要に応じてDynamoDBからのデータ読み出しおよび書き込みを行います。さらに、WAFを適用することでセキュアにします。
APIとしては、ID・名前・身長・体重・年齢の情報を持つPersonモデルを登録・取得・更新・削除するAPIを作りたいと思います。
- GET dev/slsTestApp/v1/api/person/{personId}
- POST dev/slsTestApp/v1/api/person
- PUT dev/slsTestApp/v1/api/person/{personId}
- DELETE dev/slsTestApp/v1/api/person/{personId}
第1回目の記事では、具体的なAPIロジックは実装せず、簡単なメッセージを返すだけのLambda関数をバックエンドとするAPI Gatewayを作りました。第2回となる本記事ではWAFを適用していきます。
AWS WAFについて
AWS WAFはCloudFront、API Gateway、ALBなどへのアクセス制限をかけることができるマネージドサービスです。
2019年11月にアップデートが行われており、CloudFromationのテンプレートのリソース名ではAWS::WAFv2
と表現されます(古いバージョンはAWS::WAF
とAWS::WAFRegional
)。
GlobalなWAFとRegionalなWAF
AWS WAFには大きく分けて二種類あり、一つはリージョンを持たないグローバルなAWSサービスに紐づけるGlobalなWAFと、もう一つはリージョンを持つAWSサービスに紐づけるRegionalなWAFです。
簡単にいうと、
- GlobalなWAF → CloudFront
- RegionalなWAF → API Gateway、ALBほか
という関係になります。それぞれ旧バージョンのWAFではAWS::WAF
とAWS::WAFRegional
で表現されます。
新バージョンのWAFv2ではGlobalかRegionalかでリソースは分かれず、Scopeプロパティでどちらにするかを指定する方式に変わりました。
ここまで大雑把に新旧のWAFについて説明してきましたが、本記事では新バージョンのWAFv2を使うこととします。
以降、WAFと記載してもそれはWAFv2を指すこととします。
WAFの構成要素
今回はIP制限を掛けるため、以下の三つが大きな構成要素となります。
- IPセット
- WebACL
- Association
各要素について、簡単な説明とテンプレート例を示します。
IPセット
対象のIPを配列形式で指定します。
ここで作成するのはIPのリストのみです。WebACLのアクセスルールでこのIPリストにあるもののみ通信を許可するか(ホワイトリスト形式)、反対にこのIPリストにあるもののみ通信を拒否するか(ブラックリスト形式)選ぶことができます。ほとんどの場合は前者のホワイトリスト形式だと思います。
テンプレート例は以下の通りです。
IPは皆さんの自宅のIPなどを指定してください。
Resources:
SlsTestAppIPSet:
Type: AWS::WAFv2::IPSet
Properties:
Addresses:
- ${self:provider.environment.ALLOWEDIP}
Description: IP set for slsTestApp Access.
IPAddressVersion: IPV4
Name: SlsTestAppAPIAllowedIPSet
Scope: REGIONAL
プロパティ名 | 説明 |
---|---|
Addresses | CIDER表記のIPアドレスを配列形式で指定します。上記コード例ではserverless.ymlの環境変数に指定したものを記載する方式にしています |
Description | IPセットの説明を記載します(任意) |
IPAddressVersion | IPV4またはIPV6を指定します |
Name | IPセットの名前を指定します(任意) |
Scope | CLOUDFRONTもしくはREGIONALを指定します。今回はリージョンごとのサービスであるAPI Gatewayに適用するため、上記コード例ではREGIONALを指定します |
WebACL
WAFの具体的なルールを設定します。
テンプレート例は以下の通りです。IPセット構築後に作成したいので、DependsOn
でIPセットのリソース名を指定しています。
Resources:
SlsTestAppWebACL:
Type: AWS::WAFv2::WebACL
Properties:
DefaultAction:
BLOCK: {}
Description: WebACL for slsTestApp Access.
Name: slsTestAppAPIWebACL
Rules:
- Action:
ALLOW: {}
Priority: 0
Name: SlsTestAppAPIAccessRule
VisibilityConfig:
CloudWatchMetricsEnabled: false
MetricName: SlsTestAppRuleMetric
SampledRequestsEnabled: false
Statement:
IPSetReferenceStatement:
Arn:
"Fn::GetAtt": [SlsTestAppIPSet, Arn]
Scope: REGIONAL
VisibilityConfig:
CloudWatchMetricsEnabled: false
MetricName: SlsTestAppWebACLMetric
SampledRequestsEnabled: false
DependsOn: SlsTestAppIPSet
プロパティ名 | 説明 |
---|---|
DefaultAction | WebACLのどのルールにも当てはまらない場合の動作を指定します。AllowもしくはBlockです。ルール外のリクエストは弾きたいので、上記コード例ではBlock を指定しています |
Description | IPセットの説明を記載します(任意) |
Name | IPセットの名前を指定します(任意) |
Rules | ルールの詳細をリスト形式で記載します。 Action→ルールに一致した場合の挙動を指定 Priority→ルールの優先度。最小値は0で、小さい順から優先的にルール判定が行われる Statement→ルールの具体的な内容を指定します。今回はIP制限のため、 IPSetReferenceStatement を指定し、先ほどのIPセットのArnを指定しています |
Scope | CLOUDFRONTもしくはREGIONALを指定します。今回はリージョンごとのサービスであるAPI Gatewayに適用するため、上記コード例ではREGIONALを指定します |
VisibilityConfig | CloudWatchメトリクスを送信するかどうか、メトリクス名、Webリクエストのサンプリングを行うかどうか指定します。今回はすべてfalseにしていますが、必要であればtrueに変えてください。 |
Association
WebACLをどのリソースに適用するかを定義します。
テンプレート例は以下の通りです。WeACL構築後に作成したいので、DependsOn
でWebACLのリソース名を指定しています。
Resources:
SlsTestAppWebACLAssociation:
Type: "AWS::WAFv2::WebACLAssociation"
Properties:
ResourceArn: arn:aws:apigateway:${self:provider.region}::/restapis/${cf:slsTestApp-${self:provider.stage}.Id}/stages/${self:provider.stage}
WebACLArn:
"Fn::GetAtt": [SlsTestAppWebACL, Arn]
DependsOn: SlsTestAppWebACL
ResourceArn
にWAF(正確にはWebACL)を適用したいリソースのArn、 WebACLArn
に適用するWebACLのArnを指定します。
API Gatewayは直接Arnを出力することができなさそうだったので、代わりにIDを出力するようAPIGatewayのテンプレートにOutputsを追加しそれを参照するようにしています。
ちなみにServerless Frameworkでは${cf:{スタック名}.{Outputsのキー名}}
という書式を使うことで別スタックのOutputsで出力した値を読み込むことができます
Resources:
ApiGatewayRestApi:
Type: "AWS::ApiGateway::RestApi"
Properties:
Body: ${file(./templates/swagger.yaml)}
…中略…
Outputs:
Id:
Value:
Ref: ApiGatewayRestApi
WAFのデプロイ
serverless.ymlを作成し、sls deploy
コマンドを実行してください。serverless.ymlの中身はresources
でテンプレートを読み込むくらいの内容になると思います。
動作確認
キャリアの電波に接続したスマホなどでブラウザを開きAPIのURLを打ち込んでみてください。
{message:Forbidden}
などのメッセージが表示されていれば成功です。
APIの実行方法はこちらを参照してください。
おわりに
今回は先日公開した【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る!第1回(全3回)で作成したAPI GatewayにAWS WAFを適用する方法を解説しました。
次回はバックエンドのロジックを作りこんでいきたいと思います。