GithubActionsからECRにarm64のDockerImageをPushする with terraform
前回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を追加してあげる必要があります。
output "terraform_execute_role_arn" {
value = aws_iam_role.terraform.arn
}
これでterraform実行用のRoleとして作成したIAM Roleのarnを、本terraformから参照できるようになります。
全体
ぐちゃぐちゃになっているので、これを改めて全体として載せておきます。
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
が気になるかたは自分でやってみてもいいかもしれません。
必要な部分のみ抽出して記載します。
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 /opt/app /app
ENTRYPOINT [ "/app" ]
upxっていうの初めて知ったんですが、圧縮率も凄いけど圧縮時間も結構凄いです。8分くらいかかりました。
容量が問題にならないうちは、個人開発の場合であればupxなしでもいいかもしれないですね。
感想
terraformにも慣れてきたけど、いつも一枚のファイルで書いているのがそろそろ辛くなってきました。
そのうち気が向いたらmoduleなどに切り出していこうと思います。
なんかへんに切り出してもそれはそれで煩雑になりそうで、どうするのがいいかまだ見えてないので一枚に書いてるんですよねぇ。
どんな流派があるんだろう?
僕はこの後ECSやったりCloudFrontやったり、DNSとかVPCとかなんか色々と設定しないと個人アプリが本番稼働できないなぁと思って心が折れそうです。
DBは外部だし、Next.jsもたぶんVercelだしね。
疎通とか穴あけとかね
あぁ〜いつ完成するんだろう
Discussion