C#で作成したAWS Lambdaの関数をGitHub Actionsで自動デプロイする

# この記事について
AWS Lambda関数を作成した際、VSCodeを使用している場合は[拡張機能](https://marketplace.visualstudio.com/items?itemName=AmazonWebServices.aws-toolkit-vscode)を使用してローカルからAWSへデプロイすることが可能です。
しかし、Lambda関数の変更回数が多くなったり、チーム開発を行う場合にはローカルからの手動デプロイは手間になったり俗人化を招くこととなります。
そこでこの記事では、GitHub Actionsを使用してGitHubの特定のブランチにpushされたLambda関数をAWSへ自動デプロイする仕組みについて説明したいと思います。

# 対象読者
* GitHubを使用してAWS Lambdaの開発を行っている方
* チーム開発などでAWS Lambdaの自動デプロイを行いたい方

# 環境
|内容|バージョン|
|---|---|
|[.NET](https://dotnet.microsoft.com/ja-jp/download/dotnet/6.0)|6.0.x|

.NET用のAmazon.Lambda.Toolsで下のコマンドで.NETプロジェクトを作成すると、以下のようなディレクトリ構成となります。
（プロジェクトファイルが`SampleFunction/src/SampleFunction/SampleFunction.csproj`に存在する）
```
% dotnet new lambda.EmptyFunction --name SampleFunction
テンプレート "Lambda Empty Function" が正常に作成されました。
% cd SampleFunction
% tree SampleFunction
SampleFunction
 ├── src
 │   └── SampleFunction
 │       ├── Function.cs
 │       ├── Readme.md
 │       ├── SampleFunction.csproj
 │       └── aws-lambda-tools-defaults.json
 └── test
     └── SampleFunction.Tests
         ├── FunctionTest.cs
         └── SampleFunction.Tests.csproj
```
今回はこちらの構成を想定してワークフローを作成していきます。
参考：[【VSCode, C#, AWS Lambda】C#で作成したLambda Functionをローカルで動作確認する - プロジェクトの作成](https://qiita.com/shin4488/items/a01fdf54daf626cb8742#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90)

# TL;DR
masterブランチにpushされた時のみGitHub Actionsのジョブを実行するようにしています。
```yml:.github/workflows/lambda_deploy.yml
name: Deploy to AWS Lambda
on:
  push:
    branches:
      - master

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Setup dotnet
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '6.0.x'

      - name: Build
        run: |
          dotnet build ${GITHUB_WORKSPACE}/SampleFunction/src/SampleFunction/SampleFunction.csproj
          (cd ${GITHUB_WORKSPACE}/SampleFunction/src/SampleFunction/bin/Debug/net6.0 && zip -r SampleFunction.zip .)

      - name: Deploy
        # デプロイ結果のoutputはターミナルに表示したくないため、いったんファイルに出力させてそのファイルをすぐ削除している
        run: |
          aws lambda update-function-code --function-name ${{ secrets.AWS_LAMBDA_FUNCTION_NAME }} --zip-file fileb://${GITHUB_WORKSPACE}/SampleFunction/src/SampleFunction/bin/Debug/net6.0/SampleFunction.zip --publish >> deploy.log
          rm deploy.log
```

# ジョブの内容
ジョブのstepsを5段階に分けましたが、それぞれの内容を順を追って見ていきましょう。
今回使用する環境はubuntuとするため、`job.deploy.runs-on`には`ubuntu-latest`を指定します。
```yml:.github/workflows/lambda_deploy.yml
# 前後省略
jobs:
  deploy:
    runs-on: ubuntu-latest
```

## checkout
デプロイ前にプログラムコードのビルドが必要であるため、checkoutしてジョブの実行環境にソースコードを持ってきます。
GitHub Actionsにはactions/checkoutアクションが用意されており、今回は最新のv3を使用します。（`actions/checkout@v3`）
```yml:.github/workflows/lambda_deploy.yml
# 前後省略
jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3
```
同じジョブのstep間のリソースは共有できるため、以降は別stepに分けて記述していきます。

## AWS認証
今回はデプロイ先がAWS Lambdaであるため、デプロイ前にAWSユーザの認証が必要となります。
ルートユーザだと権限が強すぎるため、AWS IAMでLambda用のユーザを作成しておきます。
今回は、作成したユーザに「AWSLambda_FullAccess」「CloudWatchFullAccess」「CloudWatchEventsFullAccess」の許可ポリシーを割り当てております。（ユーザがLambdaへデプロイする権限があればカスタムの許可ポリシーなどでも良いです。）
参考：[AWS アカウント での IAM ユーザーの作成
](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html)
ユーザを作成したら、そのユーザのアクセスキーを発行します。作成したIAMユーザを選択して「アクセスキーを作成」から作成することが可能です。アクセスキーとシークレットアクセスキーを後ほど使うためためメモしておきましょう。

AWS認証は、専用のアクション[aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials)が用意されているためこちらを使用します。
先ほど作成・メモしたアクセスキーとシークレットアクセスキーを使用するのですが、直接ymlファイルに記述してしまうとユーザ情報が外部に漏れてしまいます。漏洩を防ぐためGitHubの[secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets)を使用します。
対象リポジトリで、Setting > Secrets and variables > Actionsと進みます。
キーの名前は任意で構いませんが、今回はアクセスキーを`AWS_ACCESS_KEY_ID`、シークレットアクセスキーを`AWS_SECRET_ACCESS_KEY`という名前でキーを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234275/aa8ce571-f7d3-390c-db2d-f861e315c395.png)
登録したキーをactionで使用します。
aws-actions/configure-aws-credentialsアクションでは、アクセスキー（`aws-access-key-id`）・シークレットアクセスキー（`aws-secret-access-key`）・Lambdaで使用するリージョン（`aws-region`）を指定する必要があり、それぞれ以下のように指定することでGitHub ActionsでAWSの認証ができます。
```yml:.github/workflows/lambda_deploy.yml
# 前後省略
jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}
```

## .NET環境のセットアップ
actionで.NET環境を整えていきましょう。[actions/setup-dotnet](https://github.com/actions/setup-dotnet)アクションで.NET環境の構築ができます。
今回.NETのバージョンは6.0系を使用しているため、`dotnet-version`には`'6.0.x'`を指定します。
```yml:.github/workflows/lambda_deploy.yml
# 前後省略
jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Setup dotnet
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '6.0.x'
```

## プログラムコードのビルド
.NET環境のセットアップを行ったため、`dotnet build`コマンドで.NETプロジェクトのビルドができます。
引数でプロジェクトファイルの指定が必要ですが、checkoutした際のcheckout先ディレクトリパスは`${GITHUB_WORKSPACE}`で取得できます。
参考：[Learn GitHub Actions - Variables](https://docs.github.com/en/actions/learn-github-actions/variables)
環境のところでも記載したように、プロジェクトファイルは`SampleFunction/src/SampleFunction/SampleFunction.csproj`に存在するため、ビルド先のパスは`${GITHUB_WORKSPACE}/SampleFunction/src/SampleFunction/SampleFunction.csproj`指定とします。

またLambdaへのデプロイ時にはビルドしたものをzipファイルにして、zipファイルをデプロイしますが、`zip`コマンドを使用する際には一度`bin/Debug/net6.0`に移動してからzipファイルを作成します。
**`bin/Debug/net6.0`に移動せずにzipファイルを作成すると、`${GITHUB_WORKSPACE}`直下のディレクトリからzipファイル化されてしまい、Lambdaにデプロイした際にエラーとなってしまいます。**
参考：[ディレクトリ内のファイルを zip ファイルにバックアップする
](https://maku77.github.io/linux/manage/backup.html)
**.NETプロジェクトをLambdaで動かすには`bin/Debug/net6.0`配下をデプロイする必要があります。**
```yml:.github/workflows/lambda_deploy.yml
# 前後省略
jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Setup dotnet
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '6.0.x'

      - name: Build
        run: |
          dotnet build ${GITHUB_WORKSPACE}/SampleFunction/src/SampleFunction/SampleFunction.csproj
          (cd ${GITHUB_WORKSPACE}/SampleFunction/src/SampleFunction/bin/Debug/net6.0 && zip -r SampleFunction.zip .)
```

## AWS Lambdaへデプロイ
いよいよLambdaへのデプロイです。
AWS CLIを使用できるため、`aws lambda update-function-code`コマンドでLambda関数を更新します。（update-function-codeを使うには、AWS Lambdaの関数を事前に作成しておく必要があります。）
`--function-name`で関数名を、`--zip-file`で先ほど作成したzipファイルのパスを指定します。
**ここで注意点です。コマンドが正常に実行されると、実行結果がJSON形式で表示されてしまいます。表示される実行結果の中には、環境変数やリージョンなど表示されたくない機密情報も表示されてしまいます。**
publicリポジトリなどを使用している場合は、この情報がGitHub Actions上に表示されるのはあまりよろしくないため、`>> deploy.log`を付けて一度ファイルに出力してGitHub Actions上に表示されないようにしています。
```yml:.github/workflows/lambda_deploy.yml
# 前後省略
jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Setup dotnet
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '6.0.x'

      - name: Build
        run: |
          dotnet build ${GITHUB_WORKSPACE}/SampleFunction/src/SampleFunction/SampleFunction.csproj
          (cd ${GITHUB_WORKSPACE}/SampleFunction/src/SampleFunction/bin/Debug/net6.0 && zip -r SampleFunction.zip .)

      - name: Deploy
        # デプロイ結果のoutputはターミナルに表示したくないため、いったんファイルに出力させてそのファイルをすぐ削除している
        run: |
          aws lambda update-function-code --function-name ${{ secrets.AWS_LAMBDA_FUNCTION_NAME }} --zip-file fileb://${GITHUB_WORKSPACE}/SampleFunction/src/SampleFunction/bin/Debug/net6.0/SampleFunction.zip --publish >> deploy.log
          rm deploy.log
```
このジョブが実行されると、AWSの画面上でもLambda関数が更新されることが確認できます。

# プログラムコードが変更された時のみ自動デプロイする
ここまでのGitHub Actionsの状態では、リポジトリ内のすべてのファイルの変更に対してジョブが実行されてしまいます。しかし、README.mdやymlファイル自体の変更では、プログラムコードのビルド結果は変わりません。
そのため、プログラムコードの変更時のみビルド・デプロイを行いたいものです。（プログラムコード以外の変更ではジョブを実行しない。）
次のように、**push以下に`paths-ignore`を定義することで特定ファイルのpush時にはジョブを実行しないようにする**ことが可能です。逆にpush以下に`paths`を定義すると、特定ファイルのpush時のみジョブを実行することも可能です。

```yml:.github/workflows/lambda_deploy.yml
name: Deploy to AWS Lambda
on:
  push:
    branches:
      - master
    paths-ignore:
      # mdファイルやGitHub Actionsのフローyml、vscode設定ファイル関連はpushしてもジョブ実行しないようにする
      - '**/*.md'
      - '.gitignore'
      - '.github/**'
      - '.vscode/**'
jobs:
# 以下省略
```

# 参考
* [GitHub Actions のトリガーで任意のファイルやフォルダーを指定する・除外する (Paths フィルター)](https://blog.beachside.dev/entry/2022/04/19/191500)
* [Github Actionsの使い方メモ](https://qiita.com/HeRo/items/935d5e268208d411ab5a)
* [AWS アカウント での IAM ユーザーの作成
](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html)
* [Learn GitHub Actions - Variables](https://docs.github.com/en/actions/learn-github-actions/variables)
* [ディレクトリ内のファイルを zip ファイルにバックアップする
](https://maku77.github.io/linux/manage/backup.html)