Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

個人開発をクラウドシフトすることにした〜Github Actions 用の IAM ロールを作る〜

個人開発用に VPS を借り始めてもう7年近くになる。これまでにボードゲーム関連で作ったサービスは基本的に VPS 1台に全部載せて運用している。コンテナすら利用していないので、PHPMySQL のバージョンを上げるだけで一苦労だ。

日々の運用が toil になってしまって放置気味なこと、得た知見を業務にそのまま生かすのが難しいことから、今後作るものは Amazon Web ServicesGoogle Cloud などのクラウドサービス上に構築することに決めた。社会人になって資金面の余裕が生まれたという理由もある。

同人活動に近いということもあり収益化はほぼできておらず、普段から赤字を垂れ流しているわけだが、やはり自ら手を動かして得た知見に勝るものはないだろう。これは勉強代ということにしておく。

そして、VPSAmazon EC2 に変えても、勉強としては何の意味もないだろう、ということで、マネージドなサービスにちゃんと寄せていくことにする。とはいえ、個人開発レベルで Amazon RDS や Amazon ElastiCache を使うのはコストに見合うメリットが得られないのは明らかだ。この辺り、どう折り合いをつけていくかは今後の課題とする。

クラウドシフトプロジェクトの手始めに、Github Actions 上で動かす Visual Regression Test の結果を Amazon S3 にアップロードする仕組みを作る作業に取り掛かった。完成品は以下のような感じで、Github の Checks API を利用して良い感じに表示できるようにしてみた。

まずは、Github Actions のコンテナから画像を S3 にアップロードするのに必要な IAM ロールを作ることにした。慣れない Terraform を黙々と書いていった。

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region  = "ap-northeast-1"
  profile = "AgricolaDevJP-admin"
}

// ref: https://zenn.dev/yukin01/articles/github-actions-oidc-provider-terraform
data "http" "github_oidc_configuration" {
  url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
}

data "tls_certificate" "github_oidc" {
  url = jsondecode(data.http.github_oidc_configuration.response_body).jwks_uri
}

resource "aws_iam_openid_connect_provider" "github_oidc" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = data.tls_certificate.github_oidc.certificates[*].sha1_fingerprint
}

resource "aws_s3_bucket" "agricola_card_image_generator_snapshot_bucket" {
  bucket        = "agricola-card-image-generator-snapshot"
  force_destroy = true
}

resource "aws_s3_bucket_public_access_block" "agricola_card_image_generator_snapshot_bucket" {
  bucket                  = aws_s3_bucket.agricola_card_image_generator_snapshot_bucket.id
  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

resource "aws_s3_bucket_versioning" "agricola_card_image_generator_snapshot_bucket" {
  bucket = aws_s3_bucket.agricola_card_image_generator_snapshot_bucket.id
  versioning_configuration {
    status = "Disabled"
  }
}

resource "aws_s3_bucket_policy" "agricola_card_image_generator_snapshot_bucket" {
  bucket = aws_s3_bucket.agricola_card_image_generator_snapshot_bucket.id
  policy = data.aws_iam_policy_document.agricola_card_image_generator_snapshot_bucket_public_access_policy.json
}

data "aws_iam_policy_document" "agricola_card_image_generator_snapshot_bucket_public_access_policy" {
  statement {
    principals {
      type        = "*"
      identifiers = ["*"]
    }
    actions = [
      "s3:GetObject"
    ]
    resources = [
      "${aws_s3_bucket.agricola_card_image_generator_snapshot_bucket.arn}/*"
    ]
  }
}

resource "aws_iam_role" "agricola_card_image_generator_snapshot_github_actions_role" {
  name               = "agricola-card-image-generator-github-actions-role"
  assume_role_policy = data.aws_iam_policy_document.agricola_card_image_generator_snapshot_github_actions_role_policy.json
}

data "aws_iam_policy_document" "agricola_card_image_generator_snapshot_github_actions_role_policy" {
  statement {
    effect = "Allow"
    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github_oidc.arn]
    }
    actions = [
      "sts:AssumeRoleWithWebIdentity"
    ]
    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values = [
        "repo:AgricolaDevJP/agricola-card-image-generator:*"
      ]
    }
  }
}

resource "aws_iam_role_policy" "agricola_card_image_generator_snapshot_bucket_put_policy" {
  name   = "agricola-card-image-generator-snapshot-bucket-put-policy"
  role   = aws_iam_role.agricola_card_image_generator_snapshot_github_actions_role.id
  policy = data.aws_iam_policy_document.agricola_card_image_generator_snapshot_bucket_put_policy.json
}

data "aws_iam_policy_document" "agricola_card_image_generator_snapshot_bucket_put_policy" {
  statement {
    effect = "Allow"
    actions = [
      "s3:PutObject"
    ]
    resources = [
      "${aws_s3_bucket.agricola_card_image_generator_snapshot_bucket.arn}/*"
    ]
  }
}

ググっても credential を環境変数に埋め込む方法ばかり出てくるが、セキュリティ上の問題から定期的なローテートが必要で面倒だ。そこで、GitHubAWS アカウントから、こちら側が用意したロールに Assume Role するようにしてみた。

zenn.dev

上記の記事を参考に、thumbprint を直書きしないようにしたのもポイントである。

AWS を扱う上では避けたくても避けられない IAM と仲良くなるための第一歩としてちょうど良いスタートになったと思う。