GitHub Actions を Slack workflow から実行する仕組みを AWS + Terraform で作ってみた

はじめに

アプリボットSREチームです。
弊社では、運用するいくつかのWebサイトのデプロイにGitHub Actionsを利用しています。

コンテンツの更新などは、エンジニアではないメンバーが担当することが多いのですが、普段慣れていないGitHub Actionsの画面内で操作を行うことには抵抗があるようでした。

その為、本番反映の時だけ、反映(デプロイ)のみを行う人としてサポートするということが度々発生していました。

そこで、作業者が誰でも簡単にデプロイを行える環境を作れないか?を検討してみたところ、AWS Chatbot + GitHub Actions + Terraformで簡単に実現することができましたので、紹介させていただきます。

※Terraform、GitHub Actionsのサンプルコードを下記にまとめていますので、よろしかったら試してみてください。

構成図

概要

項目説明
SlackワークフローSlackチャンネルのショートカット「ワークフロー」→「Run command」より2回のクリックでデプロイできます
AWS ChatbotSlack チャンネルに投稿されたawsコマンド(lambda)を実行します
AWS LambdaGitHub ActionsのWorkflow dispatchを実行しています(GitHub Token必須)
GitHub Actionson Workflow dispatchトリガーで動作します
Slack通知GitHub Actionsの成功、失敗通知をします

実行結果イメージ

Terraformの構成

各リソースについて説明します。

AWS Lambda

初回時のみnull_resourceを使用してmain.goをビルドしています。

# Lambda Function
resource "aws_lambda_function" "lambda" {
  description      = "Official Site Release"
  filename         = "${path.module}/lambda/archive/main.zip"
  function_name    = local.function_name
  role             = aws_iam_role.lambda.arn
  handler          = "main"
  source_code_hash = data.archive_file.lambda.output_base64sha256
  runtime          = "go1.x"
  memory_size      = 128
  timeout          = 15
  publish          = true
  environment {
    variables = {
      BRANCH          = local.variables_BRANCH
      REPO_NAME       = local.variables_REPO_NAME
      REPO_OWNER      = local.variables_REPO_OWNER
      WORKFLOW_NAME   = local.variables_WORKFLOW_NAME
      PARAMETER_STORE = local.variables_PARAMETER_STORE
    }
  }
}

resource "null_resource" "lambda" {
  triggers = {
    file_content = md5(file("${path.module}/lambda/source/main.go"))
  }

  provisioner "local-exec" {
    command = "GOOS=linux GOARCH=amd64 go build -o ${path.module}/lambda/bin/main ${path.module}/lambda/source/main.go"
  }
}

data "archive_file" "lambda" {
  depends_on       = [null_resource.lambda]
  type             = "zip"
  source_dir       = "${path.module}/lambda/bin/"
  output_path      = "${path.module}/lambda/archive/main.zip"
  output_file_mode = "0666"
}

# IAM Role
resource "aws_iam_role" "lambda" {
  name = local.lambda_role_name
  path = "/service-role/"

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

resource "aws_iam_role_policy_attachment" "lambda_execution" {
  role       = aws_iam_role.lambda.id
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "lambda_ssm" {
  name = "ssm"
  role = aws_iam_role.lambda.id

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameter"
            ],
            "Resource": "*"
        }
    ]
}
EOF
}

Golang

各環境変数を参照し、GitHub Actionsのworkflow dispatchを実行します。

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/ssm"
)

type WorkflowDispatchPayload struct {
    Ref    string                 `json:"ref,omitempty"`
}

func main() {
    lambda.Start(handler)
}

func handler(ctx context.Context) {
    // AWS Systems Manager パラメータストアからGitHubトークンを取得
    paramerStore := os.Getenv("PARAMETER_STORE")
    githubToken, err := getGitHubTokenFromParameterStore(paramerStore)
    if err != nil {
        fmt.Printf("Failed to get GitHub token: %v", err)
        return
    }

    // GitHubリポジトリとワークフローの情報
    repoOwner := os.Getenv("REPO_OWNER")
    repoName := os.Getenv("REPO_NAME")
    workflowName := os.Getenv("WORKFLOW_NAME")

    // Workflow DispatchのPayloadを構築
    payload := WorkflowDispatchPayload{
        Ref: os.Getenv("BRANCH"), // 実行するブランチ名
    }

    // Workflow Dispatchのリクエストを作成
    payloadBytes, err := json.Marshal(payload)
    if err != nil {
        fmt.Printf("Failed to marshal payload: %v", err)
        return
    }

    url := fmt.Sprintf("https://api.github.com/repos/%s/%s/actions/workflows/%s/dispatches", repoOwner, repoName, workflowName)
    req, err := http.NewRequest("POST", url, bytes.NewReader(payloadBytes))
    if err != nil {
        fmt.Printf("Failed to create request: %v", err)
        return
    }

    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("Accept", "application/vnd.github.v3+json")
    req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", githubToken))

    // Workflow Dispatchのリクエストを送信
    client := http.DefaultClient
    resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("Failed to send request: %v", err)
        return
    }
    defer resp.Body.Close()

    // レスポンスをチェック
    if resp.StatusCode != http.StatusCreated {
        fmt.Print(url, "\n")
        fmt.Printf("Unexpected response status: %s\n", resp.Status)
        b, err := io.ReadAll(resp.Body)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(string(b))
        return
    }

    fmt.Println("Workflow Dispatch triggered successfully!")
}

func getGitHubTokenFromParameterStore(parameterPath string) (string, error) {
    // AWSセッションの作成
    sess := session.Must(session.NewSession())

    // AWS Systems Manager サービスクライアント
    ssmClient := ssm.New(sess)

    // パラメータストアからGitHubトークンを取得
    input := &ssm.GetParameterInput{
        Name:           aws.String(parameterPath),
        WithDecryption: aws.Bool(true),
    }

    result, err := ssmClient.GetParameter(input)
    if err != nil {
        return "", err
    }

    return *result.Parameter.Value, nil
}

AWS Chatbot

Slackワークスペースとの連携設定と、AWSリソースの権限を付与をしています。(初回時のみSlackワークスペースとの連携設定が必要)
また、仕様として同一チャンネル名で複数の作成ができないので注意が必要です。

# Chatbot
resource "awscc_chatbot_slack_channel_configuration" "chatbot" {
  configuration_name = local.configuration_name
  slack_workspace_id = local.slack_workspace_id
  slack_channel_id   = local.slack_channel_id

  # チャンネルロール(IAM Role)
  iam_role_arn       = aws_iam_role.chatbot.arn
  # ガードレールポリシ (チャンネルロールよりも優先)
  guardrail_policies = [
    aws_iam_policy.chatbot_guardrail.arn # 必須
  ]

  logging_level = "ERROR"

  depends_on       = [
    aws_iam_role.chatbot,
    aws_iam_policy.chatbot_guardrail
  ]
}

resource "aws_iam_role" "chatbot" {
  name               = local.chatbot_role_name
  assume_role_policy = data.aws_iam_policy_document.chatbot_assume.json
}

data "aws_iam_policy_document" "chatbot_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "chatbot.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_policy" "chatbot_guardrail" {
  name   = "chatbot_guardrail_policy"
  policy = data.aws_iam_policy_document.chatbot_guardrail.json
}

data "aws_iam_policy_document" "chatbot_guardrail" {
  statement {
    effect = "Allow"

    actions = [
      "lambda:invokeAsync",
      "lambda:invokeFunction"
    ]

    resources = [
      "*",
    ]
  }
  statement {
    effect = "Allow"

    actions = [
      "cloudwatch:Describe*",
      "cloudwatch:Get*",
      "cloudwatch:List*"
    ]

    resources = [
      "*",
    ]
  }
}

# 以下、IAMロール ポリシー (ガードレールポリシと同じもの)
resource "aws_iam_role_policy_attachment" "chatbot_readonly" {
  role       = aws_iam_role.chatbot.id
  policy_arn = "arn:aws:iam::aws:policy/AWSResourceExplorerReadOnlyAccess"
}

resource "aws_iam_role_policy" "chatbot_lambda" {
  name = "lambda"
  role = aws_iam_role.chatbot.id

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "lambda:invokeAsync",
                "lambda:invokeFunction"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
EOF
}

resource "aws_iam_role_policy" "chatbot_noti" {
  name = "AWS-Chatbot-NotificationsOnly-Policy"
  role = aws_iam_role.chatbot.id

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "cloudwatch:Describe*",
                "cloudwatch:Get*",
                "cloudwatch:List*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
EOF
}

詳しくはこちらにまとめています。
https://github.com/applibot-inc/lambda_chatbot

まとめ

GitHub Actionsを使った開発を行っているリポジトリの運用がChatOpsで可能となり、サポートや確認のみのやりとりの工数を大幅に削減できました。

Slackワークフローはエンジニア職以外の方との共通利用に適していて、他にも色々と応用が可能です。

例えば、エンジニアではないメンバでEC2の稼働・停止をしたいという要件や、単体でAWS Lambdaを実行することもできます。利便性は高まるかと思われますので、ぜひご活用ください。

関連記事一覧

  1. この記事へのコメントはありません。

てっくぼっと!をもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む