👨‍💻

GithubActionsからECRにarm64のDockerImageをPushする with terraform

2024/12/16に公開

前回terraform用のRoleを作成したので、それを使ってGithub ActionsからECRにImageをPushしてみようと思います。

例によって私はGithubActions, terraformの素人なので参考程度にお楽しみください。

前提

  • arm64へのビルドはamd64のマシンから行っています。
  • pushされたimageを使ったecsの起動などはまだ試していません(2024/12/16)

GithubActionsに用意されているarm64環境を使ってないのは、個人開発でお金を書けたくなかったからです。
お金をかけたくないからね、仕方ないね。

Github Actionsに必要なAWS側の設定を行う

GithubActionsからECRにPushするには、その権限を付与する環境が用意できている必要があります。
まずはそれを作っていきましょう。

Imageを配置するRepositoryを作成する

resource "aws_ecr_repository" "your_project" {
  name = "your-image-repository"
}

シンプルですね

GithubActions用のOIDC Providerを構築する

OIDC ProviderをAWS内に構築できるなんて知らなかったです。凄いですね、AWS
しかもこんなちょっとの設定で...

data "http" "github_actions_openid_configuration" {
  url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
}

data "tls_certificate" "github_actions" {
  url = jsondecode(data.http.github_actions_openid_configuration.response_body).jwks_uri
}

resource "aws_iam_openid_connect_provider" "github_actions" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = data.tls_certificate.github_actions.certificates[*].sha1_fingerprint # 記載しなくても良い
}

thumbprint_listですが、実際には特に指定しなくてもよくなっているようです。
【GitHub Actions】AWSとのOIDC連携で特定のFingerprintを指定する必要がなくなりました

GithubActionsからAssumeRoleするための権限設定をする

GithubActionsからAWSの機能を利用するときにAssumeRoleを行うためのRoleを作成します。

resource "aws_iam_role" "github_actions" {
  name               = "oidc-github-actions"
  assume_role_policy = data.aws_iam_policy_document.github_actions_assumerole.json
}

data "aws_iam_policy_document" "github_actions_assumerole" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github_actions.arn] # ID プロバイダの ARN
    }
    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    # 特定のリポジトリの特定のブランチからのみ認証を許可する
    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:sub"
      values   = [
        "repo:github-account-id/your-project:ref:refs/heads/main",
      ]
    }
  }
}

作成したRoleにECRへのPushを行う権限を付与

AssumeRoleが出来るようになったので、対象のRoleにECRへのLogin/Push権限を付与します。

data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

locals {
  account_id = data.aws_caller_identity.current.account_id
  region     = data.aws_region.current.id
}

resource "aws_iam_policy" "github_actions" {
  name = "project-github-actions"
  policy = data.aws_iam_policy_document.github_actions.json
}
data "aws_iam_policy_document" "github_actions" {
  statement {
    effect = "Allow"
    actions = [
      "ecr:GetAuthorizationToken",
    ]
    resources = ["*"]
  }

  statement {
    effect = "Allow"
    actions = [
      "ecr:InitiateLayerUpload",
      "ecr:CompleteLayerUpload",
      "ecr:UploadLayerPart",
      "ecr:BatchCheckLayerAvailability",
      "ecr:PutImage",
      "ecr:BatchGetImage", // github_actions docker/build-and-pushを使う場合
    ]
    resources = [
"arn:aws:ecr:${local.region}:${local.account_id}:repository/${aws_ecr_repository.your_project.name}",
    ]
  }
}

resource "aws_iam_role_policy_attachment" "github_actions" {
  role       = aws_iam_role.github_actions.name
  policy_arn = aws_iam_policy.github_actions.arn
}

ecr:GetAuthorizationTokenはECRそのものに対して実行されるためresoucesは*になります。
そのためstatementは分ける必要があります。

また今回Github Actionsでdocker/build-and-pushのプラグイン?を利用するためecr:BatchGetImageも付与しています。

本terraformをAssumeRoleした権限で実行できるように設定する

前回作成したterraform用のRoleにAssumeRoleして、本terraformを実行できるように設定します。

data "terraform_remote_state" "setup" {
  backend = "s3"

  config = {
    bucket = "your-bucket-terraformstate"
    key    = "setup/terraform.tfstate"  // こっちは`setup`
    region = "ap-northeast-1"
  }
}

terraform {
  required_version = ">= 1.10"

  backend "s3" {
    bucket = "your-bucket-terraformstate"
    key    = "production/terraform.tfstate"
	  encrypt = true
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region = "ap-northeast-1"

  assume_role {
    role_arn = data.terraform_remote_state.setup.outputs.terraform_execute_role_arn
  }
}

今回はterraform_remote_stateを利用してみました。
これは別のproject?で実行したterraformの実行結果(state)から値を参照してくることが出来るものです。

前回backendに指定していたbucketに保存されているtfstateを指定して、そこにoutputされている値を取得してきてくれます。

というわけなので、これを利用するためには前回作成したterraformにoutputsを追加してあげる必要があります。

terraform/setup/outputs.tf
output "terraform_execute_role_arn" {
  value = aws_iam_role.terraform.arn
}

これでterraform実行用のRoleとして作成したIAM Roleのarnを、本terraformから参照できるようになります。

全体

ぐちゃぐちゃになっているので、これを改めて全体として載せておきます。

main.tf
terraform/production/main.tf
data "terraform_remote_state" "setup" {
  backend = "s3"

  config = {
    bucket = "your-bucket-terraformstate"
    key    = "setup/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

terraform {
  required_version = ">= 1.10"

  backend "s3" {
    bucket = "your-bucket-terraformstate"
    key    = "production/terraform.tfstate"
	  encrypt = true
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region = "ap-northeast-1"

  assume_role {
    role_arn = data.terraform_remote_state.setup.outputs.terraform_execute_role_arn
  }
}

data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

locals {
  account_id = data.aws_caller_identity.current.account_id
  region     = data.aws_region.current.id
}

########## Github Actions/ECR ##########
# Imageを配置するECRを作成
resource "aws_ecr_repository" "your_project" {
  name = "your-image-repository"
}

# Github Actions用のOIDC Provider設定
data "http" "github_actions_openid_configuration" {
  url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
}

data "tls_certificate" "github_actions" {
  url = jsondecode(data.http.github_actions_openid_configuration.response_body).jwks_uri
}

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

# Github Actions実行時にAssumeRoleするRoleの設定
resource "aws_iam_role" "github_actions" {
  name               = "oidc-github-actions"
  assume_role_policy = data.aws_iam_policy_document.github_actions_assumerole.json
}
data "aws_iam_policy_document" "github_actions_assumerole" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github_actions.arn] # ID プロバイダの ARN
    }
    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    # 特定のリポジトリの特定のブランチからのみ認証を許可する
    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:sub"
      values   = [
        "repo:github-account-id/your-project:ref:refs/heads/main",
      ]
    }
  }
}

# Github Actions実行に必要な権限を設定
resource "aws_iam_policy" "github_actions" {
  name = "project_github_actions"
  policy = data.aws_iam_policy_document.github_actions.json
}
data "aws_iam_policy_document" "github_actions" {
  statement {
    effect = "Allow"
    actions = [
      "ecr:GetAuthorizationToken",
    ]
    resources = ["*"]
  }

  statement {
    effect = "Allow"
    actions = [
      "ecr:InitiateLayerUpload",
      "ecr:CompleteLayerUpload",
      "ecr:UploadLayerPart",
      "ecr:BatchCheckLayerAvailability",
      "ecr:PutImage",
      "ecr:BatchGetImage", // github_actions docker/build-and-pushを使う場合
    ]
    resources = [
      "arn:aws:ecr:${local.region}:${local.account_id}:repository/${aws_ecr_repository.your_project.name}",
    ]
  }
}

resource "aws_iam_role_policy_attachment" "github_actions" {
  role       = aws_iam_role.github_actions.name
  policy_arn = aws_iam_policy.github_actions.arn
}

terraform実行 RoleのPolicyを更新する

今回作成したmain.tfを実行するのには色々と権限が必要です。
前回作成したmain.tfのpolicyを更新しましょう。

せっかくなのでpikeが気になるかたは自分でやってみてもいいかもしれません。
必要な部分のみ抽出して記載します。

terraform/setup/main.tf
resource "aws_iam_policy" "terraform" {
  name = "terraform"
  path = "/"

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "dynamodb:DeleteItem",
                "dynamodb:DescribeTable",
                "dynamodb:GetItem",
                "dynamodb:PutItem"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeAccountAttributes"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": [
                "ecr:CreateRepository",
                "ecr:DeleteRepository",
                "ecr:DescribeRepositories",
                "ecr:ListTagsForResource"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "VisualEditor3",
            "Effect": "Allow",
            "Action": [
                "iam:AttachRolePolicy",
                "iam:CreateOpenIDConnectProvider",
                "iam:CreatePolicy",
                "iam:CreateRole",
                "iam:CreatePolicyVersion", # 個別に追加した
                "iam:DeleteOpenIDConnectProvider",
                "iam:DeletePolicy",
                "iam:DeleteRole",
                "iam:DetachRolePolicy",
                "iam:GetOpenIDConnectProvider",
                "iam:GetPolicy",
                "iam:GetPolicyVersion",
                "iam:GetRole",
                "iam:ListAttachedRolePolicies",
                "iam:ListInstanceProfilesForRole",
                "iam:ListPolicyVersions",
                "iam:ListRolePolicies",
                "iam:UpdateOpenIDConnectProviderThumbprint"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "VisualEditor4",
            "Effect": "Allow",
            "Action": [
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:PutObject"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
  })
}

個別に追加した、となっている部分は今回作成したmain.tfの初回実行は不要だけど、後から一部変更して再実行したときに必要になる権限です。

これでterafform側は完成ですね。

Github Actionsを設定する

AWS側が設定できたのでGithub Actionsもやっていきます。
Actionsのトリガー設定などは好みに合わせて修正してください。

name: Build and Push DockerImage to ECR

on: 
  push:
    branches:
      - main
  pull_request:
    types: [closed]
    branches:
      - main

env:
  AWS_ASSUME_ROLE_ARN: ${{secrets.ASSUME_ROLE_ARN}}
  AWS_REGION: ${{secrets.AWS_REGION}}
  ECR_REPOSITORY: your-ecr-repository

jobs:
  build-and-push:
	# mainへの直接PushかPullRequestがmergeされたときのみ実行
    if: |
      github.event_name == 'push' ||
      (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
    
    runs-on: ubuntu-latest
    # for OIDC AssumeRole
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4

      # amd64のマシンでarm64にビルドするために必要なエミュレータ
      - name: Setup QEMU
        uses: docker/setup-qemu-action@v3
      - name: Setup Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@v3

      # terraformで作成したRoleにAssumeRole
      - name: Get AWS Credentials
        id: creds
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.AWS_ASSUME_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      # 作成するImageのMeta情報を作成
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ steps.creds.outputs.aws-account-id }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}

      # ECRへのログイン
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      # DockerfileのビルドとPush
      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/arm64 # arm64を指定
          push : true
          tags: ${{ steps.meta.outputs.tags }}
          provenance: false

特に語ることがないので、設定だけ載せてしまいました。
ARNの情報などはあまりリポジトリにテキストとして残したくないので、僕はsecretsを利用しています。

これでmainへマージ、プッシュされたらGithub ActionsでプロジェクトをビルドしてECRにPushができるようになります。
嬉しいですね。動いた時、僕はすごく嬉しかったです、

おまけ: Dockerfile

Dockerfileは本題からずれるのですが、一応おいておきます。
ちなみに言語はGoです

Dockerfile
########## ビルドステージ ##########
FROM golang:1.22-bullseye as builder

RUN apt update && \
  apt install -y upx

COPY ./ /workspace/project
WORKDIR /workspace/project

ENV ARCH="arm64"

RUN go mod download && \ 
    GOOS=linux GOARCH=${ARCH} CGO_ENABLED=0 go build -ldflags '-w -s' -trimpath -o /opt/app ./presentation/echo/server && \
    upx -8 /opt/app

## 実行ステージ
FROM gcr.io/distroless/static:latest
EXPOSE 8080

COPY --from=builder /opt/app /app

ENTRYPOINT [ "/app" ]

upxっていうの初めて知ったんですが、圧縮率も凄いけど圧縮時間も結構凄いです。8分くらいかかりました。
容量が問題にならないうちは、個人開発の場合であればupxなしでもいいかもしれないですね。

感想

terraformにも慣れてきたけど、いつも一枚のファイルで書いているのがそろそろ辛くなってきました。
そのうち気が向いたらmoduleなどに切り出していこうと思います。

なんかへんに切り出してもそれはそれで煩雑になりそうで、どうするのがいいかまだ見えてないので一枚に書いてるんですよねぇ。
どんな流派があるんだろう?

僕はこの後ECSやったりCloudFrontやったり、DNSとかVPCとかなんか色々と設定しないと個人アプリが本番稼働できないなぁと思って心が折れそうです。
DBは外部だし、Next.jsもたぶんVercelだしね。
疎通とか穴あけとかね

あぁ〜いつ完成するんだろう

Discussion