GitHub Actionsによるマネージドコンテナサービス(AWS・Azure・GCP)へのデプロイ
クラウドサービスプロバイダ(AWS、Azure、GCP)それぞれのコンテナサービスに対して、GitHub ActionsでCDをする方法をまとめました。
最近それぞれのクラウドを使うことが増えてきたため、備忘録としてまとめます。
全ての認証はOIDCベースで行っています。
※ 本記事では、クラウドサービスプロバイダはAWS、Azure、GCPに絞っています。
概要
検証などでサクッとエンドポイントを立てたい時、クラウド上にあるマネージドなコンテナサービスが重宝します。
クラウドサービスプロバイダそれぞれで似たようなサービスが展開されています。
AWS: AWS App Runner
Azure: Azure Container Apps
GCP: Cloud Run
検証用途では頻繁にコードが変更されるため、都度手動でデプロイすることは非効率的です。検証の最初にデプロイフローを自動化することは、検証の高速化に役立ちます。
今回はこれらの3つのマネージドなコンテナサービスに対して、mainブランチにマージされたらDockerfileをビルドし、各クラウドサービスプロバイダのコンテナレジストリにプッシュ、そしてコンテナサービスへのデプロイといった一連の処理をするCDをGitHub Actionsで作成してみました。
また、これらに必要なリソース用のterraformも準備したので、どの場合でも使いまわせると思います。
GitHub: https://github.com/hosimesi/code-for-techblogs/tree/main/cloud_contaier_app_cicd
前提条件
立ち上げるサービスは今回の本質ではないので、nginxを立てます。また、今回は全てのサービスで東京リージョンで構築します。
GitHub Actionsへのクレデンシャルの持たせ方はいくつかありますが、どのクラウドサービスの場合もOIDCを使用します。
基本的に以下の手順で進めると、どの環境でもサービスを立ち上げられます。
cd path/to/your-cloud-provider
terraform init
terraform plan
terraform apply
AWS
AWSにはAmazon Elastic Container Registryというコンテナレジストリがあります。今回は以下の構成でCDを構築します。
コンテナレジストリ: Amazon Elastic Container Registry
コンテナーアプリ: AWS App Runner
App Runnerの場合、ECRに新たなイメージがPushされると自動でデプロイが走るオプション(auto_deployments_enabled)が存在しているため、GitHub Actionsで明示的にサービスをアップデートすることはしません。
Terraform
FIXMEに書いてある通り、自身の環境に合わせて変えてください。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
resource "aws_ecr_repository" "sample_ecr_repository" {
name = "sample-aws-ecr-repository"
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = true
}
}
resource "aws_iam_role" "sample_apprunner_role" {
name = "sample-apprunner-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "build.apprunner.amazonaws.com"
}
Action = "sts:AssumeRole"
},
]
})
}
resource "aws_iam_role_policy_attachment" "sample_apprunner_policy_attachment" {
role = aws_iam_role.sample_apprunner_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess"
}
resource "aws_apprunner_service" "sample_apprunner_service" {
service_name = "sample-apprunner-service"
source_configuration {
authentication_configuration {
access_role_arn = aws_iam_role.sample_apprunner_role.arn
}
auto_deployments_enabled = false
image_repository {
image_configuration {
port = "80"
}
image_identifier = "${aws_ecr_repository.sample_ecr_repository.repository_url}:latest"
image_repository_type = "ECR"
}
}
instance_configuration {
cpu = 256
memory = 512
}
}
# actions用
resource "aws_iam_openid_connect_provider" "sample_iam_openid_connect_provider" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
# thumbprintはterraform documentのまま。
thumbprint_list = ["cf23df2207d99a74fbe169e3eba035e633b65d94"]
}
resource "aws_iam_policy" "sample_iam_policy" {
name = "sample-iam-policy"
description = "sample-iam-policy"
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage",
],
Resource = ["*"],
},
],
})
}
data "aws_caller_identity" "current" {}
resource "aws_iam_role" "sample_actions_role" {
name = "sample-actions-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = "sts:AssumeRoleWithWebIdentity",
Principal = {
Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"
},
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com",
# FIXME: Please set your branch name
"token.actions.githubusercontent.com:sub" = "repo:hosimesi/code-for-techblogs:ref:refs/heads/main"
}
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "sample_actions_role_policy_attachment" {
role = aws_iam_role.sample_actions_role.name
policy_arn = aws_iam_policy.sample_iam_policy.arn
}
CD
Terraformでリソース作成後、GitHubのSettingsからSecretを登録します。作成したロール名のARNをそのまま登録することになるので、自身の環境に合わせて変えてください。
name: Run creating docker image and deploy to app runner.
on:
push:
branches: [main]
jobs:
aws:
name: Deploy to App Runner.
env:
# FIXME: Set your own repository name
ECR_REPOSITORY: sample-aws-ecr-repository
REGION: ap-northeast-1
# DOCKERFILE: docker/Dockerfile
DOCKERFILE: docker/Dockerfile
runs-on: ubuntu-latest
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: actions/checkout@v4
with:
ref: main
- id: 'auth'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ACTIONS_ROLE_ARN }}
aws-region: ${{ env.REGION }}
- name: Authorize Docker
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Docker image to ECR.
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:latest -f $DOCKERFILE .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
Azure
AzureにはAzure Container Registryというコンテナレジストリがあります。今回は以下の構成でCDを構築します。
コンテナレジストリ: Azure Container Registry
コンテナーアプリ: Azure Container Apps
Terraform
まず、Azureの通常リソースと、GitHub Actions用のリソースを作成するProviderが異なるので複数providerを指定します。
通常リソースはazurerm provider、CD用リソースはEntra ID(旧Azure Active Directory)はazuread providerを使用します。
tenant_id、subscription_idを自身の環境に合わせて書き換えてください。
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "3.89.0"
}
azuread = {
source = "hashicorp/azuread"
version = "~> 2.47.0"
}
}
}
provider "azuread" {
# FIXME: Replace with your tenant ID
tenant_id = "your-tenant-id"
}
provider "azurerm" {
skip_provider_registration = true
# FIXME: Replace with your subscription ID
subscription_id = "your-subscription-id"
features {}
}
resource "azurerm_resource_group" "sample_resource_group" {
name = "sample-azure-resource-group"
location = "japaneast"
}
resource "azurerm_container_registry" "sample_container_registry" {
name = "sampleazurecontainerregistryhoshii" # FIXME: Replace with your container registry name
resource_group_name = azurerm_resource_group.sample_resource_group.name
location = azurerm_resource_group.sample_resource_group.location
sku = "Basic"
admin_enabled = true
}
resource "azurerm_user_assigned_identity" "sample_container_app_user_assigned_identity" {
name = "sample-azure-container-app-identity"
location = azurerm_resource_group.sample_resource_group.location
resource_group_name = azurerm_resource_group.sample_resource_group.name
}
resource "azurerm_role_assignment" "sample_container_registry_role_assignment" {
scope = azurerm_container_registry.sample_container_registry.id
role_definition_name = "AcrPull"
principal_id = azurerm_user_assigned_identity.sample_container_app_user_assigned_identity.principal_id
}
resource "azurerm_container_app_environment" "sample_container_app_environment" {
name = "sample-azure-container-app-environment"
location = azurerm_resource_group.sample_resource_group.location
resource_group_name = azurerm_resource_group.sample_resource_group.name
}
resource "azurerm_container_app" "sample_container_app" {
name = "sample-azure-container-app"
container_app_environment_id = azurerm_container_app_environment.sample_container_app_environment.id
resource_group_name = azurerm_resource_group.sample_resource_group.name
revision_mode = "Single"
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.sample_container_app_user_assigned_identity.id]
}
registry {
server = azurerm_container_registry.sample_container_registry.login_server
identity = azurerm_user_assigned_identity.sample_container_app_user_assigned_identity.id
}
ingress {
external_enabled = true
target_port = 80
transport = "auto"
traffic_weight {
latest_revision = true
percentage = 100
}
}
template {
container {
name = "sample-azure-container-app"
# FIXME: Replace with your image name
image = "${azurerm_container_registry.sample_container_registry.login_server}/sample-azure-repository:latest"
cpu = 0.25
memory = "0.5Gi"
}
}
}
# actions用
data "azuread_domains" "sample-azuread-domains" {
only_initial = true
}
# Retrieve client configuration
data "azuread_client_config" "current" {}
# Create an application
resource "azuread_application" "sample-github-actions-azuread-application" {
display_name = "sample-github-actions-azuread-app"
owners = [data.azuread_client_config.current.object_id]
}
# Create a service principal
resource "azuread_service_principal" "sample-github-actions-azuread-service-principal" {
client_id = azuread_application.sample-github-actions-azuread-application.client_id
owners = [data.azuread_client_config.current.object_id]
}
# Create a federated identity credential
resource "azuread_application_federated_identity_credential" "sample-github-actions-azuread-app-federated-identity-credential" {
application_id = "/applications/${azuread_application.sample-github-actions-azuread-application.object_id}"
display_name = "sample-github-actions-azuread-app-federated-identity-credential"
description = "Federated identity credential for GitHub Actions"
audiences = ["api://AzureADTokenExchange"]
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:hosimesi/code-for-techblogs:ref:refs/heads/main"
}
# ACR Push Role
data "azurerm_role_definition" "acrpush" {
name = "AcrPush"
}
# Contributor Role
data "azurerm_role_definition" "contributor" {
name = "Contributor"
}
# Role Assignment to application
resource "azurerm_role_assignment" "acrpush" {
scope = azurerm_container_registry.sample_container_registry.id
role_definition_name = data.azurerm_role_definition.acrpush.name
principal_id = azuread_service_principal.sample-github-actions-azuread-service-principal.object_id
}
# Role Assignment to container apps
resource "azurerm_role_assignment" "contributor" {
scope = azurerm_container_app.sample_container_app.id
role_definition_name = data.azurerm_role_definition.contributor.name
principal_id = azuread_service_principal.sample-github-actions-azuread-service-principal.object_id
}
CD
Terraformでリソース作成後、GitHubのSettingsからSecretを登録します。secretsにはTENANT_ID、CLIENT_ID、SUBSCRPTION_IDを設定します。
他にもFIXMEに書いてある通り、自身の環境に合わせて値は変えてください。
name: Run creating docker image and deploy to azure container apps
on:
push:
branches: [main]
jobs:
azure:
name: Deploy to Container Apps.
env:
# FIXME: Replace with your own values
CONTAINER_REGISTRY: sampleazurecontainerregistryhoshii
AZURE_CONTAINER_REGISTRY: sampleazurecontainerregistryhoshii.azurecr.io
REPOSITORY_NAME: sample-azure-repository
CONTAINER_APPS_NAME: sample-azure-container-app
RESOURCE_GROUP: sample-azure-resource-group
DOCKERFILE: docker/Dockerfile
runs-on: ubuntu-latest
permissions:
contents: "read"
id-token: "write"
steps:
- uses: actions/checkout@v4
with:
ref: main
- id: Auth
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Authorize Docker
run: az acr login -n $CONTAINER_REGISTRY --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Build and push Docker image to Azure Container Registry
run: |
docker build -t $AZURE_CONTAINER_REGISTRY/$REPOSITORY_NAME:latest -f $DOCKERFILE .
docker push $AZURE_CONTAINER_REGISTRY/$REPOSITORY_NAME:latest
- name: Deploy to Container Apps
run: |
az containerapp update \
-n $CONTAINER_APPS_NAME \
-g $RESOURCE_GROUP \
--subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} \
--image $AZURE_CONTAINER_REGISTRY/$REPOSITORY_NAME:latest
GCP
GCPにはArtifact Registryというコンテナレジストリがあります。今回は以下の構成でCDを構築します。
コンテナレジストリ: Artifact Registry
コンテナーアプリ: Cloud Run
Terraform
FIXMEに書いてある通り、自身の環境に合わせて変えてください。
provider "google" {
project = var.project
region = var.region
}
resource "google_service_account" "github_actions_service_account" {
account_id = "github-actions-sa"
display_name = "github actions service account"
}
resource "google_service_account" "sample_cloud_run_service_account" {
account_id = "sample-cloud-run-sa"
display_name = "sample cloud run service account"
}
resource "google_artifact_registry_repository" "sample_artifact_registry_repository" {
location = var.region
repository_id = "sample-artifact-registry-repository"
description = "sample artifact registry repository"
format = "DOCKER"
}
resource "google_cloud_run_v2_service" "sample_cloud_run" {
name = "sample-cloud-run"
location = var.region
description = "sample cloud run service"
ingress = "INGRESS_TRAFFIC_ALL"
template {
containers {
name = "sample-cloud-run"
# FIXME: Replace with your artifact registry repository
image = "${var.region}-docker.pkg.dev/${var.project}/${google_artifact_registry_repository.sample_artifact_registry_repository.repository_id}/sample-image:latest"
ports {
container_port = 80
}
resources {
cpu_idle = true
}
}
scaling {
min_instance_count = 0
max_instance_count = 1
}
service_account = google_service_account.sample_cloud_run_service_account.email
}
traffic {
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
percent = 100
}
}
data "google_iam_policy" "noauth" {
binding {
role = "roles/run.invoker"
members = [
"allUsers",
]
}
}
resource "google_cloud_run_service_iam_policy" "noauth" {
location = google_cloud_run_v2_service.sample_cloud_run.location
project = var.project
service = google_cloud_run_v2_service.sample_cloud_run.name
policy_data = data.google_iam_policy.noauth.policy_data
}
# github actions用
resource "google_iam_workload_identity_pool" "sample_gh_act_pool" {
workload_identity_pool_id = "sample-gh-act-pool"
display_name = "sample-gh-act-pool"
description = "Pool for GitHub Actions"
disabled = false
}
resource "google_iam_workload_identity_pool_provider" "sample_gh_act_provider" {
workload_identity_pool_provider_id = "sample-gh-act-provider"
workload_identity_pool_id = google_iam_workload_identity_pool.sample_gh_act_pool.workload_identity_pool_id
display_name = "sample-gh-act-provider"
description = "Provider for GitHub Actions"
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.repository" = "assertion.repository"
}
oidc {
issuer_uri = "https://token.actions.githubusercontent.com"
}
}
resource "google_project_iam_member" "artifact_registry_writer" {
project = var.project
role = "roles/artifactregistry.writer"
member = "serviceAccount:${google_service_account.github_actions_service_account.email}"
}
resource "google_project_iam_member" "service_account_user" {
project = var.project
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${google_service_account.github_actions_service_account.email}"
}
resource "google_project_iam_member" "admin" {
project = var.project
role = "roles/run.admin"
member = "serviceAccount:${google_service_account.github_actions_service_account.email}"
}
resource "google_project_iam_member" "workload_identity_user" {
project = var.project
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:${google_service_account.github_actions_service_account.email}"
}
resource "google_service_account_iam_binding" "workload_identity_user" {
service_account_id = google_service_account.github_actions_service_account.name
role = "roles/iam.workloadIdentityUser"
members = [
# FIXME: Replace with your workload identity pool name
"principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.sample_gh_act_pool.name}/attribute.repository/hosimesi/code-for-techblogs"
]
}
variables.tf
variable "project" {
description = "gcp project id"
type = string
default = "your-gcp-project-id" # FIXME: Replace with your GCP project ID
}
variable "region" {
description = "gcp region"
type = string
default = "asia-northeast1"
}
CD
まず、secretを追加します。
GCP_WORKLOAD_IDENTITY_PROVIDERは以下の形式です。
projects/<project番号>/locations/global/workloadIdentityPools/<pool_id>/providers/<provider_id>
※ project_idとかではないので注意です。workload identity providerの画面からコピペするといいと思います。
GCP_GITHUB_ACTIONS_SAは作成したGitHub Actions用のサービスアカウントのEmailです。
FIXMEに書いてある通り、自身の環境に合わせて変えてください。
name: Run creating docker image and deploy to cloud run.
on:
push:
branches: [main]
jobs:
gcp:
name: Deploy to Cloud Run.
env:
SERVICE_NAME: sample-cloud-run # FIXME: Set your own service name
REPOSITORY_NAME: sample-artifact-registry-repository # FIXME: Set your own repository name
REGION: asia-northeast1
ARTIFACT_REPOSITORY: asia-northeast1-docker.pkg.dev
IMAGE_NAME: sample-image # FIXME: Set your own image name
DOCKERFILE: docker/Dockerfile
runs-on: ubuntu-latest
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: actions/checkout@v4
with:
ref: main
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_GITHUB_ACTIONS_SA }}
- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'
with:
version: '>= 363.0.0'
- name: Authorize Docker
run: gcloud auth configure-docker $ARTIFACT_REPOSITORY --quiet
- name: Build and push Docker image to Artifact Registry
run: |
docker build -t $ARTIFACT_REPOSITORY/$GCP_PROJECT/$REPOSITORY_NAME/$IMAGE_NAME:latest -f $DOCKERFILE .
docker push $ARTIFACT_REPOSITORY/$GCP_PROJECT/$REPOSITORY_NAME/$IMAGE_NAME:latest
- name: Deploy to Cloud Run
run: |-
gcloud run services update $SERVICE_NAME \
--image=$ARTIFACT_REPOSITORY/$GCP_PROJECT/$REPOSITORY_NAME/$IMAGE_NAME:latest \
--region=$REGION
まとめ
3大クラウドプロバイダーのコンテナサービスへのCDを作成しました。
注意点として、初回のterraform apply時に、イメージがまだRegistryに追加されておらず、Container Serviceが立ち上がらずエラーになります。
この時は手動で一度Container Registryに上げてから再度実行するか、リソースの作成する順序を分けましょう。
Discussion