Lambda@EdgeでレスポンスにHTTPヘッダを付与するやつをGitHub Actionsで自動デプロイする

# 概要

- フロントにNuxt.jsを採用していると、`npm run start`するだけだと`X-Frame-Options`とか`CSP`とか`x-xss-protection`といったヘッダがレスポンスに付与されない
- `Fastify`をNodeサーバーとして使っているのでそちらで頑張ってもよかったけど、せっかくAWSで`CloudFront`->`ALB`->`Fargate`な仕組みを使っているので、AWSで頑張ってみようと思った
- Lambda@Edgeを使って、Nuxtからのレスポンスにヘッダを付与するやつを組んだ
- アプリケーションと連動して動いたほうがいいし、手動でデプロイする運用がしんどいと思ったので、GitHub ActionsでCLIベースの自動デプロイが組めるところまで持ってきた
- それをやるがためにCloudFormationまで導入しててんやわんやした

日本語文献がそんなになかったので、まとまりがないメモですが貼っておきます。

# Lambda@Edgeのソース内容

下記ブログを参考にしました。CSPだけ模倣するわけにいかなかったので、配列ベースでドメインを管理してjoinする感じで組む改修はしました。

[http://blog.serverworks.co.jp/tech/2020/02/27/add-security-headers-lambda-edge-cloudfront/](http://blog.serverworks.co.jp/tech/2020/02/27/add-security-headers-lambda-edge-cloudfront/)

```jsx
'use strict';

exports.handler = (event, context, callback) => {

  //Get contents of response
  const cf = event.Records[0].cf
  const request = cf.request
  const response = cf.response;
  const headers = response.headers;

  //Set new headers
  headers['strict-transport-security'] = [{
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubdomains; preload'
  }];
  headers['content-security-policy'] = [{
    key: 'Content-Security-Policy',
    // 配列ベースで管理する
    value: `default-src ${CSPDefaultSrcList}; img-src * blob: data:; script-src ${CSPScriptSrcList}; style-src ${CSPStyleSrcList}; object-src 'none';`
  }];
  headers['x-content-type-options'] = [{key: 'X-Content-Type-Options', value: 'nosniff'}];
  // getFrameOptionsはrequestを受け取って、iframe用のページだけDENYしなかったりする
  headers['x-frame-options'] = [{key: 'X-Frame-Options', value: getFrameOptions(request)}];
  headers['x-xss-protection'] = [{key: 'X-XSS-Protection', value: '1; mode=block'}];
  headers['referrer-policy'] = [{key: 'Referrer-Policy', value: 'same-origin'}];

  //Return modified response
  callback(null, response);
};

// 以下略
```

# 大まかな設計

- 雑に説明すると、Lambda@Edgeをデプロイするには、ソースコードを更新するだけじゃなくて、紐付けているCloudFrontも一緒に更新する必要がある
- プロダクションコードと同じリポジトリに、awsディレクトリを切って、Lambda@Edgeのソースを配置
- CloudFrontとLambdaを作成するCloudFormationのyamlも置いておく
- Develop/MasterマージしたときにGitHub Actionsが動いて、前述のyamlを使ってAWS CLIでデプロイする
    - GitHub ActionsでAWS CLI動かすのは公式がアクションを出してくれているので簡単

    ```yaml
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1
    ```

# 組み方

- すでにCloudFrontは事前にコンソールで作っていたので、CloudFormerを使ってCloudFormationのymlにエクスポートした
- CloudFormerは癖が強いけど、[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-using-cloudformer.html)のやり方通りにできる

## CloudFront/Lambda@EdgeのCloudFormation
自社の構成がバレるので一部だけ引用して貼る。同じyml内にLambdaとCloudFront両方書く

```yml
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31

Parameters:
  AwsLambdaRole:
    Type: String
    Description: Names of existing Roles you want to add to the newly created Managed Policy

#...中略...

Resources:
  AddSecurityHeaderToResponse:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: AddSecurityHeaderToResponse/
      Role: !Ref AwsLambdaRole
      Runtime: nodejs12.x
      Handler: index.handler
      Timeout: 5
      AutoPublishAlias: stg
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service:
                - "lambda.amazonaws.com"
                - "edgelambda.amazonaws.com"
# ...中略...
  StagingCloudFront:
    Type: 'AWS::CloudFront::Distribution'
    Properties:
# ...中略...
        DefaultCacheBehavior:
          LambdaFunctionAssociations:
            - EventType: origin-response
              LambdaFunctionARN: !Ref AddSecurityHeaderToResponse.Version
```

## GitHub Actions

こんな感じでAWSのCLIコマンドを実行すればいい。環境変数は適宜。`if`を使えるので環境変数を切り替えることで検証環境と本番環境で1ファイルで運用することができます。

```yml
      - name: Deploy latest Lambda function
        run: |
          aws cloudformation package --template-file $AWS_STACK_FILE_NAME --output-template-file packaged.yaml --s3-bucket $BUCKET_NAME --region us-east-1
        working-directory: ./aws/lambda_edge

      - name: Update Cloudfront Distribution
        run: |
          aws cloudformation deploy --template-file packaged.yaml --stack-name $AWS_STACK_NAME --parameter-overrides MainStreamBackendAcm=$AWS_CERTIFICATE_ARN AwsLambdaRole=${{ secrets.AWS_LAMBDA_EDGE_ROLE }} --region us-east-1
        working-directory: ./aws/lambda_edge
```

# メリデメ

- Pros
    - 一旦CSP頑張ってみることにしたので、アプリケーションでドメイン追加したときに必ず（ほぼ）同期デプロイで出せる
    - Lambdaを使うとローカル開発ではヘッダーを付与するのができないので、せめて同一リポジトリにおいて意識できる
    - Nginxの設定ファイルの管理のほうが個人的に辛いのでそれしなくてよくなったのがよい
    - Nuxt以外のサービスを導入することになったとしても、CloudFrontのレイヤーでHTTPヘッダを担保しているとセキュリティ安心感ある
- Cons
    - 普通にCloudFormationつらかった
    - IAM権限で必要なのが多すぎて、リソースや権限を*使わないように頑張るのがつらかった
    - 別にインフラ全部をCloudFormationしたいわけじゃなかったのに、Lambda@Edgeをリリースする関係で使い始めてしまったので中途半端

CloudFormationとか全く知らなかったので結構手間取りました。

1日半くらい溶かしてしまったので、まあまあ大変な方でしたね。

# 参考文献

## CloudFormerで現状のCloudFrontの値をとりあえず取得

[https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-using-cloudformer.html](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-using-cloudformer.html)

めっちゃβ版って感じのUXでした

## CloudFormationでLambdaのデプロイ

[https://qiita.com/ytaka95/items/5899c44c85e71fdc5273](https://qiita.com/ytaka95/items/5899c44c85e71fdc5273)

[CloudFormationでLambda＠Edgeをデプロイする - Qiita](https://qiita.com/kter/items/b3cf2693236cd129d69e)

## CLIでCloudFormationをデプロイする公式Doc

基本はpackageを動かしたあとにdeployコマンド。

作成も更新も同じコマンドでいい。create-stack/update-stackとかあるけどdeployだけでオールOKっぽい　ref: [https://stackoverflow.com/questions/49945531/aws-cloudformation-create-stack-vs-deploy](https://stackoverflow.com/questions/49945531/aws-cloudformation-create-stack-vs-deploy)

[https://docs.aws.amazon.com/cli/latest/reference/cloudformation/package.html](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/package.html)

[https://docs.aws.amazon.com/cli/latest/reference/cloudformation/deploy/index.html](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/deploy/index.html)

## CloudFormationのyamlには外から引数を渡すことができる

[https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html)

開発と本番で違うところがあるからこれで引数で渡せるようにして、GitHub Actionsのyamlで環境変数を使って使い分ける感じ

## CloudFormationでCloudFrontをデプロイする

[https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-viewercertificate.html](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-viewercertificate.html)

## 公式が出している、ちょっと古いけどサンプルになるやつ

[https://github.com/awslabs/serverless-application-model/blob/master/examples/2016-10-31/lambda_edge/template.yaml](https://github.com/awslabs/serverless-application-model/blob/master/examples/2016-10-31/lambda_edge/template.yaml)

## CloudFormation deployコマンドを叩いたときに無言で落ちるけど実際は権限エラーだよってやつ

[https://github.com/awslabs/serverless-application-model/issues/58](https://github.com/awslabs/serverless-application-model/issues/58)

## あまり役に立たなかったけど足しにはなった公式のFAQ

[https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-template-validation/](https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-template-validation/)

これも役に立たなかったけど一応見た

[https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-validate-template.html](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-validate-template.html)

## デプロイ時には専用のIAMユーザー作って権限を付ける

コレが一番大変だった。疲れた。


デプロイ用のIAM作って、手元でコマンドを叩いて権限不足で弾かれるのを延々と繰り返す。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">AWSの設定している時、いつも「最小限のポリシーにしてから、失敗したら必要な権限だけ足していく」をやっているんだけど、これ無限に時間溶かさないですか？玄人の方々はどうやって乗り切ってるんだ</p>&mdash; 名人 | ㈱NoSchool CTO (@Meijin_garden) <a href="https://twitter.com/Meijin_garden/status/1280371407294103552?ref_src=twsrc%5Etfw">July 7, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


このへんとか見た。
[https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-permissions.html](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-permissions.html)

[https://gist.github.com/stu-smith/7fec25367e1b83fb0709c708a704ff04](https://gist.github.com/stu-smith/7fec25367e1b83fb0709c708a704ff04)

---

以上です。

よかったらTwitterフォローしてください〜
https://twitter.com/Meijin_garden