GitHub ActionsでオープンデータをS3にアップロードしてLambdaで処理する
先日、GitHubで公開しているオープンデータを AWS内で処理する設定を行いました。
その過程を若干一般化して投稿したいと思います。
AWS側の処理はオープンデータをS3にアップロードして、Lambdaで実行する流れになります。
S3へのアップロードをトリガーにLambdaを動かすこともできますが、既にファイルパスを渡すとS3のファイルを取得して処理するものを作成していますので、S3へのアップロード後にGitHub側からLambdaを呼び出すことにしました。
概要
GitHub Actions で AWSのリソースを使えるようにするには、AWS側とGitHub側双方に設定が必要です。
具体的には以下の設定が必要になります。
- 
Amazon Web Service 設定
1-1. IDプロバイダ追加
1-2. IAMロール作成
1-3. IAMポリシー作成 - 
GitHub 設定
2-1. シークレット変数作成
2-2. ワークフローファイル作成
2-3. 実行テスト 
それぞれはそんなに難しいものではないので、順に説明していきます。
1. Amazon Web Service設定
AWS側の設定は、ここではWebコンソール上で行っていきます。
1-1. IDプロバイダ追加
IAMのページから以下を開きます。
アクセス管理 > IDプロバイダ
次に プロバイダの追加 ボタンをクリックします。
IDプロバイダの作成 ページで以下を入力します。
| 項目 | 値 | 
|---|---|
| プロバイダのタイプ | OpenID Connect | 
| プロバイダの URL | https://token.actions.githubusercontent.com | 
| 対象者 | sts.amazonaws.com | 

次に、サムプリントを取得 ボタンをクリックして、サムプリントを取得します。
正しく取得されたら、プロバイダを追加 ボタンをクリックします。
これで IDプロバイダが作成されます。
1-2. IAMロール作成
次にIAMロールを作成します。
アクセス管理 > ロール
を開いて、ロールを作成ボタンをクリックします。
ステップ 1 信頼されたエンティティ を選択のページでは以下を入力して、次へをクリックします。
| 項目 | 値 | 
|---|---|
| 信頼されたエンティティタイプ | ウェブアイデンティティ | 
| アイデンティティプロバイダー | token.actions.githubusercontent.com | 
| Audience | sts.amazonaws.com | 
ステップ 2 許可を追加 では IAMポリシーを設定しますが、後で説明するのでここでは何も選択せず次へをクリックします。
ステップ 3 名前、確認、および作成 では ロールの名前を付けて、ロールを作成ボタンをクリックします。
これでIAMロールは作成されますがこのままでは使えないようなので、もう一度、アクセス管理 > ロール を開いて、作成したロールのページを開きます。
信頼関係タブを開くと、以下のようなJSONが表示されると思います。
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::[AWSアカウントID]:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        }
      }
    }
  ]
}
ここの Condition 以下の部分を書き換える必要があります。信頼ポリシーを編集ボタンをクリックして以下のように書き換えます。
これで、特定のリポジトリの特定のブランチのみで、AWSリソースを使えるようにできます。
[GitHubアカウント名]、[リポジトリ名]、[対象とするブランチ名] は使用したいアカウント、リポジトリ、ブランチの名前に置き換えてください。
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::[AWSアカウントID]:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:sub": "repo:[GitHubアカウント名]/[リポジトリ名]:ref:refs/heads/[対象とするブランチ名]"
        }
      }
    }
  ]
}
後述の設定ではmainブランチを使っていますが、それであれば以下のようになります。
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:sub": "repo:[GitHubアカウント名]/[リポジトリ名]:ref:refs/heads/main"
        }
      }
また、StringEquals を StringLike に変更することでワイルドカード*が使用でき、ref:refs/heads/main を * に置き換えれば、mainブランチ以外でもAWSのリソースを使えるようにできます。
      "Condition": {
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:[GitHubアカウント名]/[リポジトリ名]:*"
        }
      }
このIAMロールのARNは後ほどGitHub側で使います。
1-3. IAMポリシー作成
最後にIAMポリシーを作成します。IAMポリシーは今回使用する S3へのアップロードs3:putObject と Lambdaの呼び出し lambda:InvokeFunction のみ許可するようにしました。
もし S3のputイベントをトリガーにLambdaを動かすのであれば、lambda:InvokeFunction は必要ないです。
作成したIAMポリシーをIAMロールにアタッチしておきます。
これで IAMロールで設定した GitHubのリポジトリの指定したブランチから、許可したリソースを使えるようになるはずです。
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
          "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::[S3バケット名]/*"
    },
    {
      "Effect": "Allow",
      "Action": [
          "lambda:InvokeFunction"
      ],
      "Resource": "[LambdaファンクションのARN]"
    }
  ]
}
2. GitHub設定
ここからは GitHub側の設定です。
2-1. シークレット変数作成
GitHub リポジトリページ内にシークレット変数を作成します。
Settingsタブの
General > Security > Secret > Actions
を開きます。
New repository secret ボタンを押すとシークレット変数を作成できます。
ここに設定した変数は、GitHub Actions のログには表示されません(マスクされる)。
これでパブリックリポジトリでもAWS側の設定が漏洩することはないはずです。
| 変数名 | 値 | 
|---|---|
| AWS_ROLE_ARN | IAMロールのARN | 
| AWS_REGION | リージョン(例: ap-northeast-1) | 
| BUCKET_NAME | S3バケット名 | 
| FUNCTION_NAME | Lambdaファンクション名 | 

2-2. ワークフローファイル作成
GitHub Actions のワークフローファイルをリポジトリに作成して、pushします。
name: CI
on:
  push:
    branches:
      - main
jobs:
  check-secret:
    runs-on: ubuntu-latest
    outputs:
      my-key: ${{ steps.my-key.outputs.defined }}
    steps:
      - id: my-key
        if: "${{ env.MY_KEY != '' }}"
        run: echo "::set-output name=defined::true"
        env:
          MY_KEY: ${{ secrets.AWS_ROLE_ARN }}
  deploy:
    runs-on: ubuntu-latest
    needs: 
      - check-secret
    if: needs.check-secret.outputs.my-key == 'true'
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - uses: tj-actions/changed-files@v18
        id: changed-files
        with:
          files: |
            **/*.ttl
      - uses: aws-actions/configure-aws-credentials@v1
        if: steps.changed-files.outputs.any_changed == 'true'
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ secrets.AWS_REGION }}
      - name: Upload all changed files
        if: steps.changed-files.outputs.any_changed == 'true'
        run: |
          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
            echo "$file was changed"
            aws s3 cp $file s3://${{ secrets.BUCKET_NAME }}/$file
            echo '{"file": "/'$file'" }' > payload.json
            aws lambda invoke --function-name ${{ secrets.FUNCTION_NAME }} --payload "$(cat payload.json | base64)" response.json
            cat response.json
          done
このワークフローファイルについては、以下のように記述しています。
mainブランチに push したときにワークフローを実行します。
on:
  push:
    branches:
      - main
シークレット変数 AWS_ROLE_ARN が定義されているかどうかチェックします。
定義がなければ、deploy の処理をスキップします。これで、誰かがこのリポジトリをフォークしても シークレット変数が定義されていないことで GitHub Actions がエラーになることが避けられると思います。
  check-secret:
    runs-on: ubuntu-latest
    outputs:
      my-key: ${{ steps.my-key.outputs.defined }}
    steps:
      - id: my-key
        if: "${{ env.MY_KEY != '' }}"
        run: echo "::set-output name=defined::true"
        env:
          MY_KEY: ${{ secrets.AWS_ROLE_ARN }}
  deploy:
    runs-on: ubuntu-latest
    needs: 
      - check-secret
    if: needs.check-secret.outputs.my-key == 'true'
追加・更新のあったファイルを検出します。
ここでは ttl拡張子を持つファイルのみを抽出する設定にしています。
これにより、README.md など対象でないファイルの処理を除外することができます。
      - uses: tj-actions/changed-files@v18
        id: changed-files
        with:
          files: |
            **/*.ttl
AWSのリソースを使えるようにIAMロールを指定します。
if: steps.changed-files.outputs.any_changed == 'true' で 更新ファイルにttlファイルがあるかどうかをチェックしています。
なければ、スキップします。
      - uses: aws-actions/configure-aws-credentials@v1
        if: steps.changed-files.outputs.any_changed == 'true'
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ secrets.AWS_REGION }}
更新のあった ttlファイルを S3バケットにアップロードして、Lambdaファンクションを呼び出します。
lambdaファンクションの payload の指定は Base64エンコードが必要みたいなので、--payload "$(cat payload.json | base64)" としています。
      - name: Upload all changed files
        if: steps.changed-files.outputs.any_changed == 'true'
        run: |
          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
            echo "$file was changed"
            aws s3 cp $file s3://${{ secrets.BUCKET_NAME }}/$file
            echo '{"path": "/'$file'" }' > payload.json
            aws lambda invoke --function-name ${{ secrets.FUNCTION_NAME }} --payload "$(cat payload.json | base64)" response.json
            cat response.json
          done
2-3. 実行テスト
作成したワークフローファイルと、アップロードしたい ttlファイル、正しくアップロード除外されるかのテストのためttlファイル以外を push してみます。
例えば、以下の3つのファイルをpushします。
.github/workflows/ci.ymlgo2museum/museum.ttlREADME.md
Actions タブで、GitHub Actionsが正しく動いているか確認します。
以下のように表示されると、アップロードとLambdaの呼び出しが行えていることになります(Lambdaの戻り値は実装により異なります)。

以後、ttlファイルが追加・更新されると、S3アップロードとLambdaが呼び出されます。
まとめ
今回 GitHub と AWS を連携させて処理を行ってみました。思ったよりも簡単に設定できることがわかりました。
オープンデータ等、広く外部に公開したいファイルについては、AWSでホスティングするよりもGitHubで公開したほうが扱いやすいと思いますし、将来的に他の方が作られたオープンデータを受け入れるようなことをしたい場合もこの仕組みであれば、そのまま流用して自動化ができると思います。
割とそういった使い方には相性の良い組み合わせかもしれません。
この設定は、以下のWebサイトのために行ったものです。
特定の形式で公開されるオープンデータWeb API(SPARQLエンドポイント)を登録して、検索できるようにしたサイトです。
以下に特徴と使い方をまとめていますので、興味がある方いましたら使ってみてください。
Discussion