Pull Requestへのコメントでlambroll deployを実行する

AtlantisのようにPull Requestが作成されたタイミングでlambroll deploy --dry-runが実行され、/deployとコメントすることでlambroll deployが実行されるGitHub Actionsのワークフローを作ってみた。

デモ

  • デプロイされたら自動でマージされる
    • エラーになったらマージされない
  • ボットのコメントは最新のもの以外は自動で最小化される

実装

ファイル構成

/
├── .github/
│   └── workflows/
│       ├── deploy.yml
│       ├── dry-run.yml
│       └── lambroll.yml
├── .gitignore
├── .lambdaignore
├── function.jsonnet
├── index.mjs
└── option.jsonnet

deploy.yml

name: Deploy

on:
  issue_comment:
    types: [created]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  deploy:
    uses: ./.github/workflows/lambroll.yml
    if: startsWith(github.event.comment.body, '/deploy')
    secrets: inherit
    with:
      deploy: true
      pr_num: ${{ github.event.issue.number }}
  reaction:
    runs-on: ubuntu-latest
    if: startsWith(github.event.comment.body, '/deploy')
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - name: Reaction
        run: |
          gh api --method=POST -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28' \
            /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
            -f 'content=+1'

dry-run.yml

name: Deploy (dry-run)

on:
  pull_request:
    branches: [main]
    paths:
      - function.jsonnet
      - index.mjs
      - option.jsonnet
    types: [opened, synchronize]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  deploy:
    uses: ./.github/workflows/lambroll.yml
    secrets: inherit
    with:
      deploy: false
      pr_num: ${{ github.event.number }}

lambroll.yml

name: lambroll

on:
  workflow_call:
    inputs:
      deploy:
        type: boolean
        required: true
      pr_num:
        type: number
        required: true
      checkout_ref:
        type: string
        default: ""

permissions:
  id-token: write
  contents: write
  issues: write
  pull-requests: write

jobs:
  lambroll:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ inputs.checkout_ref }}
      - uses: winebarrel/lastcmt@v0.6.3
      - uses: aws-actions/configure-aws-credentials@v5
        with:
          aws-region: ap-northeast-1
          role-to-assume: arn:aws:iam::123456789012:role/lambroll-deploy
      - uses: fujiwara/lambroll@v1
        with:
          version: v1.4.1
      - name: Deploy
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          LAMBROLL_OPTION: option.jsonnet
          DEPLOY: ${{ inputs.deploy }}
          OPTS: ${{ !inputs.deploy && '--dry-run' || '' }}
          PR_NUM: ${{ inputs.pr_num }}
          LASTCMT_KEY: lambroll-deploy-${{ inputs.deploy }}
        run: |
          set -exo pipefail
          MERGEABLE=$(gh pr view $PR_NUM --json mergeable -q '.mergeable')
          if $DEPLOY && [ "$MERGEABLE" != "MERGEABLE" ]; then
            echo ':red_circle: Pull request is not mergeable.' | lastcmt $PR_NUM
            exit 1
          fi

          set +e
          lambroll deploy --no-color $OPTS 2>&1 | tee deploy.log
          RET=$?
          set -e

          ICON=$([ $RET -eq 0 ] && echo -n ':green_circle:' || echo -n ':red_circle:')
          echo "## $ICON lambroll deploy $OPTS" > comment.txt
          echo '```'     >> comment.txt
          cat deploy.log >> comment.txt
          echo '```'     >> comment.txt
          if $DEPLOY; then
            echo ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} >> comment.txt
          fi
          lastcmt $PR_NUM comment.txt

          if $DEPLOY && [ $RET -eq 0 ]; then
            gh pr merge $PR_NUM --merge --delete-branch
          fi

          exit $RET

IAM

resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"
  client_id_list = [
    "sts.amazonaws.com"
  ]
}

resource "aws_iam_role" "lambroll_deploy" {
  name = "lambroll-deploy"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github.arn
        }
        Action = [
          "sts:AssumeRoleWithWebIdentity",
          "sts:TagSession",
        ]
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" = "repo:foo/bar:*"
          }
        }
      },
    ]
  })
}

resource "aws_iam_role_policy" "lambroll_deploy" {
  role = aws_iam_role.lambroll_deploy.id
  name = "lambroll-deploy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = "iam:PassRole"
        Resource = "arn:aws:iam::1234567890:role/lambda-role"
      },
      {
        Effect = "Allow"
        Action = [
          "lambda:Get*",
          "lambda:List*",
          "lambda:CreateAlias",
          "lambda:DeleteFunction",
          "lambda:UpdateAlias",
          "lambda:UpdateFunctionCode",
          "lambda:UpdateFunctionConfiguration",
        ]
        Resource = "*"
      },
      {
        Effect   = "Allow"
        Action   = "s3:GetObject"
        Resource = "arn:aws:s3:::my-bucket/terraform.tfstate"
      },
    ]
  })
}

resource "aws_iam_role" "lambda_hello" {
  name = "lambda-hello"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_hello" {
  role = aws_iam_role.lambda_hello.name
  arn  = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

関連リンク