Argo CD Image Updaterを用いてGitOpsを構築してみた
1. はじめに
1-1. この記事で構築するGitOps
本記事では、下図のような構成でGitOpsを構築します。
CIとしてはGitHub Actionsを利用しており、アプリケーション用リポジトリへのリリースバージョンタグの付与をトリガーとしてイメージをビルドしECRリポジトリへプッシュします。一方で、CDとしてはArgo CDとその拡張であるArgo CD Image Updaterを利用しており、ECRリポジトリへの新しいイメージタグの追加を検知して、新しいイメージをコンテナにデプロイします。
1-2. インフラ構成
上記を動作させるインフラ構成は下図の通りです。基本的にはCDK for Goで記述していますが、ALBの構築とホストゾーンへの証明書インストールのみKubernetes管理としています。
1-3. Kubernetesリソースの構成
Argo CDでデプロイ管理する主要なKubernetesリソースの構成を下図に示します。
2. AWSインフラの構築
1-2節の図のようなインフラをCDK for Goで構築する方法については、CDK for GoでEKS on EC2のインフラ構成を作ってみたという記事の中で説明しているので、詳しくはそちらをご参照ください。ここでは、GitOpsの構築にあたって発生した先述の記事との差分にフォーカスして説明していきいます。
2-1. OIDCを利用してGitHub ActionsからECRへイメージプッシュするためのIAMロールの作成
イメージのビルドとECRへのプッシュを行うCIツールをCodeBuildからGitHub Actionsへと変更しています。変更した理由は、GitHub Actionsの方がイメージのビルド&プッシュにかかる時間が短かったからです。実際、この記事の場合だとGitHub Actionsは1分30秒程度でCIが完了するのですが、CodeBuildの場合だとCIの完了までに3分30秒程度の時間がかかっていました。ビルドする時間は両者で大差ないのですが、CodeBuildではソースコードの取得に時間がかかっているようでした😅
というわけで、先に紹介した記事にあるCDKのコードからイメージビルド用のCodeBuildプロジェクトを作成するロジックを削除しています。代わりに、OIDC認証を通過したGitHubに割り当てるIAMロールをCDKで作成しています。このIAMロールを作成するコードは以下のようになります。
package my_eks
import (
iam "github.com/aws/aws-cdk-go/awscdk/v2/awsiam"
constructs "github.com/aws/constructs-go/constructs/v10"
jsii "github.com/aws/jsii-runtime-go"
)
func NewGitHubActions(stack constructs.Construct) {
// Open ID Connectプロバイダ
provider := iam.NewOpenIdConnectProvider(stack, jsii.String("Provider"), &iam.OpenIdConnectProviderProps{
Url: jsii.String("https://token.actions.githubusercontent.com"),
ClientIds: &[]*string {jsii.String("sts.amazonaws.com")},
Thumbprints: &[]*string {
jsii.String("6938fd4d98bab03faadb97b34396831e3780aea1"),
},
})
// 連携したIDプロバイダを表すプリンシパルエンティティ
// 認証されたユーザに一時的なセキュリティ認証情報を提供するために使用する。
principalGitHub := iam.NewFederatedPrincipal(provider.OpenIdConnectProviderArn(), &map[string]interface{}{
"StringLike": map[string]string{"token.actions.githubusercontent.com:sub": "repo:ご自身のGitHubアカウント名/*"},
"StringEquals": map[string]string{"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"},
}, jsii.String("sts:AssumeRoleWithWebIdentity"))
// GitHub Actionsが任意のECRリポジトリに対してイメージプッシュするためのポリシー
githubPolicy := iam.NewManagedPolicy(stack, jsii.String("policy-github"), &iam.ManagedPolicyProps{
ManagedPolicyName: jsii.String("policy-github"),
Document: iam.NewPolicyDocument(&iam.PolicyDocumentProps{
Statements: &[]iam.PolicyStatement{
iam.NewPolicyStatement(&iam.PolicyStatementProps{
Effect: iam.Effect_ALLOW,
Resources: &[]*string{jsii.String("*")},
Actions: &[]*string{
jsii.String("sts:GetCallerIdentity"),
jsii.String("ecr:GetAuthorizationToken"),
jsii.String("ecr:CompleteLayerUpload"),
jsii.String("ecr:UploadLayerPart"),
jsii.String("ecr:InitiateLayerUpload"),
jsii.String("ecr:BatchCheckLayerAvailability"),
jsii.String("ecr:PutImage"),
},
}),
},
}),
})
// GitHub用のIAMロール
iam.NewRole(stack, jsii.String("RoleGithub"), &iam.RoleProps{
AssumedBy: principalGitHub,
Path: jsii.String("/"),
RoleName: jsii.String("role-github"),
Description: jsii.String("Role assumed by GitHub"),
ManagedPolicies: &[]iam.IManagedPolicy{githubPolicy,},
})
}
続いて、アプリケーションリポジトリへのリリースバージョンタグの付与をトリガーとして、イメージをビルドしECRへプッシュするGitHub Actionsのワークフローを作成します。この記事の本題から外れるので説明を割愛しますが、以下のようなyamlファイルをアプリケーション用のリポジトリに配置しています。
name: Build and Push Docker Images to ECR Repositories
on:
push:
tags:
- 'v*'
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: ap-northeast-1
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT }}:role/role-github
role-duration-seconds: 1800
- run: aws sts get-caller-identity
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build and Push images to ECR repositories
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
VERSION=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g")
docker build -t $ECR_REGISTRY/eks-app:$VERSION -f ./build/app/Dockerfile.prod .
docker build -t $ECR_REGISTRY/eks-web:$VERSION -f ./build/web/Dockerfile .
docker build -t $ECR_REGISTRY/eks-migration:$VERSION -f ./build/migration/Dockerfile .
docker push $ECR_REGISTRY/eks-web:$VERSION
docker push $ECR_REGISTRY/eks-app:$VERSION
docker push $ECR_REGISTRY/eks-migration:$VERSION
2-2. DBマイグレートを行うCodeBuildプロジェクトを削除
紹介した記事の中ではCodeBuildからDBマイグレートを行なっていました。今回は、Argo CD Resource Hooksを利用して新しいアプリケーションイメージを自動デプロイする直前にフックをかけて、Job
リソースを起動してDBマイグレートを行う方式に変更しました。ですので、DBマイグレート用のCodeBuildプロジェクトを作成しているロジックをファイル(=stacks/my_eks/db_migrator.go
)ごと削除しています。
IRSA (= IAM Role for Service Account)の作成
2-3.ここでは、ServiceAccount
用のIAMロールを4つ作成しています。
-
External Sercets OperatorがSecrets ManagerからSecretリソース作成するためのIAMロール
(=create-secret-from-secrets-manager-role
) -
External DNSがRoute53のレコードを作成・更新・削除するためのIAMロール
(=role-for-external-dns
) -
ArgoCD Image UpdaterがECRリポジトリのイメージタグ情報を取得するためのIAMロール
(=role-for-image-updater
) - DB Migrateを実行するJobがDockerイメージをプルするためのIAMロール
(=role-for-db-migrator
)
説明すると長くなるので、ソースコードを示すのみに留めます。
package my_eks
import (
cdk "github.com/aws/aws-cdk-go/awscdk/v2"
ecr "github.com/aws/aws-cdk-go/awscdk/v2/awsecr"
eks "github.com/aws/aws-cdk-go/awscdk/v2/awseks"
iam "github.com/aws/aws-cdk-go/awscdk/v2/awsiam"
constructs "github.com/aws/constructs-go/constructs/v10"
jsii "github.com/aws/jsii-runtime-go"
)
func NewIamRolesForServiceAccounts(stack constructs.Construct, cluster eks.Cluster, repoMigration ecr.Repository) {
// ***************************************************************************
// External Sercets OperatorがSecrets ManagerからSecretリソース作成するためのIAMロール
// ***************************************************************************
principalESO := iam.NewFederatedPrincipal(cluster.OpenIdConnectProvider().OpenIdConnectProviderArn(), &map[string]interface{}{
"StringEquals": cdk.NewCfnJson(stack, jsii.String("ConditionForAccountToAccessSecrets"), &cdk.CfnJsonProps{
Value: map[string]string{
*cluster.ClusterOpenIdConnectIssuer()+":sub": "system:serviceaccount:main:account-to-access-secrets", // system:serviceaccount:ネームスペース:ServiceAccount名
},
}),
}, jsii.String("sts:AssumeRoleWithWebIdentity"))
secretAccessPolicy := iam.NewManagedPolicy(stack, jsii.String("SecretsManagerAccessPolicy"), &iam.ManagedPolicyProps{
ManagedPolicyName: jsii.String("secrets-access-policy"),
Document: iam.NewPolicyDocument(&iam.PolicyDocumentProps{
Statements: &[]iam.PolicyStatement{
iam.NewPolicyStatement(&iam.PolicyStatementProps{
Effect: iam.Effect_ALLOW,
Resources: &[]*string{jsii.String("*")},
Actions: &[]*string{
jsii.String("secretsmanager:GetResourcePolicy"),
jsii.String("secretsmanager:GetSecretValue"),
jsii.String("secretsmanager:DescribeSecret"),
jsii.String("secretsmanager:ListSecretVersionIds"),
},
}),
},
}),
})
iam.NewRole(stack, jsii.String("CreateSecretFromSecretsManagerRole"), &iam.RoleProps{
AssumedBy: principalESO,
RoleName: jsii.String("create-secret-from-secrets-manager-role"),
ManagedPolicies: &[]iam.IManagedPolicy{secretAccessPolicy,},
})
// ********************************************************
// External DNSがRoute53のレコードを作成・更新・削除するためのIAMロール
// ********************************************************
principalExternalDNS := iam.NewFederatedPrincipal(cluster.OpenIdConnectProvider().OpenIdConnectProviderArn(), &map[string]interface{}{
"StringEquals": cdk.NewCfnJson(stack, jsii.String("ConditionForAccountForExternalDNS"), &cdk.CfnJsonProps{
Value: map[string]string{
*cluster.ClusterOpenIdConnectIssuer()+":sub": "system:serviceaccount:main:account-for-external-dns",
},
}),
}, jsii.String("sts:AssumeRoleWithWebIdentity"))
changeRecordSetsPolicy := iam.NewManagedPolicy(stack, jsii.String("ChangeRecordSetsPolicy"), &iam.ManagedPolicyProps{
ManagedPolicyName: jsii.String("change-record-sets-policy"),
Document: iam.NewPolicyDocument(&iam.PolicyDocumentProps{
Statements: &[]iam.PolicyStatement{
iam.NewPolicyStatement(&iam.PolicyStatementProps{
Effect: iam.Effect_ALLOW,
Resources: &[]*string{jsii.String("arn:aws:route53:::hostedzone/*")},
Actions: &[]*string{
jsii.String("route53:ChangeResourceRecordSets"),
},
}),
},
}),
})
listRecordSetsPolicy := iam.NewManagedPolicy(stack, jsii.String("ListRecordSetsPolicy"), &iam.ManagedPolicyProps{
ManagedPolicyName: jsii.String("list-record-sets-policy"),
Document: iam.NewPolicyDocument(&iam.PolicyDocumentProps{
Statements: &[]iam.PolicyStatement{
iam.NewPolicyStatement(&iam.PolicyStatementProps{
Effect: iam.Effect_ALLOW,
Resources: &[]*string{jsii.String("*")},
Actions: &[]*string{
jsii.String("route53:ListHostedZones"),
jsii.String("route53:ListResourceRecordSets"),
},
}),
},
}),
})
iam.NewRole(stack, jsii.String("EditRoute53RecordRole"), &iam.RoleProps{
AssumedBy: principalExternalDNS,
RoleName: jsii.String("role-for-external-dns"),
ManagedPolicies: &[]iam.IManagedPolicy{changeRecordSetsPolicy, listRecordSetsPolicy},
})
// *****************************************************************
// ArgoCD Image UpdaterがECRリポジトリのイメージタグ情報を取得するためのIAMロール
// *****************************************************************
principalArgoCDImageUpdater := iam.NewFederatedPrincipal(cluster.OpenIdConnectProvider().OpenIdConnectProviderArn(), &map[string]interface{}{
"StringEquals": cdk.NewCfnJson(stack, jsii.String("ConditionForAccountForArgoCDImageUpdater"), &cdk.CfnJsonProps{
Value: map[string]string{
*cluster.ClusterOpenIdConnectIssuer()+":sub": "system:serviceaccount:argocd:argocd-image-updater", // "system:serviceaccount:<argocd-image-updaterを配置するnamespace>:<argocd-image-updaterに設定するServiceAccount名>"
},
}),
}, jsii.String("sts:AssumeRoleWithWebIdentity"))
updateImagePolicy := iam.NewManagedPolicy(stack, jsii.String("UpdateImagePolicy"), &iam.ManagedPolicyProps{
ManagedPolicyName: jsii.String("argocd-update-image-policy"),
Document: iam.NewPolicyDocument(&iam.PolicyDocumentProps{
Statements: &[]iam.PolicyStatement{
iam.NewPolicyStatement(&iam.PolicyStatementProps{
Effect: iam.Effect_ALLOW,
Resources: &[]*string{jsii.String("*")},
Actions: &[]*string{
jsii.String("ecr:GetAuthorizationToken"),
jsii.String("ecr:ListImages"),
jsii.String("ecr:BatchGetImage"),
jsii.String("ecr:GetDownloadUrlForLayer"),
},
}),
},
}),
})
iam.NewRole(stack, jsii.String("ArgoCDImageUpdaterRole"), &iam.RoleProps{
AssumedBy: principalArgoCDImageUpdater,
RoleName: jsii.String("role-for-image-updater"),
ManagedPolicies: &[]iam.IManagedPolicy{updateImagePolicy,},
})
// ******************************************************
// DB Migrateを実行するJobがDockerイメージをプルするためのIAMロール
// ******************************************************
principalDBMigrator := iam.NewFederatedPrincipal(cluster.OpenIdConnectProvider().OpenIdConnectProviderArn(), &map[string]interface{}{
"StringEquals": cdk.NewCfnJson(stack, jsii.String("ConditionForAccountForDBMigrator"), &cdk.CfnJsonProps{
Value: map[string]string{
*cluster.ClusterOpenIdConnectIssuer()+":sub": "system:serviceaccount:main:account-for-db-migrator",
},
}),
}, jsii.String("sts:AssumeRoleWithWebIdentity"))
pullImagePolicy := iam.NewManagedPolicy(stack, jsii.String("PullImagePolicyForDBMigrator"), &iam.ManagedPolicyProps{
ManagedPolicyName: jsii.String("pull-image-policy-for-db-migrator"),
Document: iam.NewPolicyDocument(&iam.PolicyDocumentProps{
Statements: &[]iam.PolicyStatement{
iam.NewPolicyStatement(&iam.PolicyStatementProps{
Effect: iam.Effect_ALLOW,
Resources: &[]*string{repoMigration.RepositoryArn()},
Actions: &[]*string{
jsii.String("ecr:BatchGetImage"),
jsii.String("ecr:GetDownloadUrlForLayer"),
},
}),
},
}),
})
iam.NewRole(stack, jsii.String("DBMigratorRole"), &iam.RoleProps{
AssumedBy: principalDBMigrator,
RoleName: jsii.String("role-for-db-migrator"),
ManagedPolicies: &[]iam.IManagedPolicy{pullImagePolicy,},
})
}
3. Kubernetesの構築
3-1. マニフェスト用リポジトリのディレクトリ構成
今回作成したk8sマニフェスト用リポジトリのディレクトリ構成は以下のようになっています。
→ tree .
.
├── README.md
├── argocd
│ ├── apps # Argo CDでデプロイ管理するApplication群
│ │ ├── argocd_image_updater.yaml
│ │ ├── external_secrets_operator.yaml
│ │ └── main.yaml
│ └── bootstrap # Argo CDの初期起動時に必要なファイル群
│ ├── init.sh # Argo CDをインストールし、初期設定を行うシェルスクリプト
│ └── manifests
│ ├── application.yaml # Argo CDでのデプロイ管理の起点となるApplication
│ └── project.yaml
├── argocd_image_updater # argocd/apps/argocd_image_updater.yamlの中身
│ └── overlays
│ └── production
│ ├── config_map.yaml
│ ├── deployment.yaml
│ ├── kustomization.yaml
│ └── service_account.yaml
└── main # argocd/apps/main.yamlの中身
├── base
│ ├── db_migrate.yaml
│ ├── external_dns.yaml
│ ├── external_secrets.yaml
│ ├── front.yaml
│ └── kustomization.yaml
└── overlays
└── production
└── kustomization.yaml
11 directories, 17 files
KubernetesへのデプロイはArgo CDをインストールするところから始まり、インストールしたArgo CDがその他のリソースをデプロイすることで進行します。この一連の作業はargocd/bootstrap/init.sh
にスクリプト化しているので、このファイルを始まりとして順に説明していきます。
3-2. Argo CDのインストール
前述したように、Argo CDのインストールと初期設定手順はスクリプト化してargocd/bootstrap/init.sh
に記述しています。これについてはArgo CDのGetting Startedをシェルスクリプト化したで説明していますので、詳しくはそちらをご参照ください。このシェルスクリプトの最後では以下のコマンド2つを実行し、AppProject
リソースとデプロイ起点となるApplication
リソースとを作成しています。
kubectl apply -f ./argocd/bootstrap/manifests/project.yaml
kubectl apply -f ./argocd/bootstrap/manifests/application.yaml
上記2つのファイルを格納しているargocd/bootstrap/manifests
がArgo CDでのデプロイ管理の起点となるリソースを定義しているディレクトリですので、次にこのディレクトリを説明します。
3-3. Argo CDでのデプロイ起点となるApplicationリソース
前述した通り、Argo CDでのデプロイ管理の起点となるリソースを定義しているディレクトリがargocd/bootstrap/manifests
です。
.
├── argocd
│ └── bootstrap
│ └── manifests
│ ├── application.yaml
│ └── project.yaml
このディレクトリ内のproject.yaml
の方でAppProject
リソースを作成し、その中にapplication.yaml
で定義したApplication
リソースをデプロイしています。それぞれのファイルの中身を以下に示します。
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: myproject
namespace: argocd
spec:
clusterResourceWhitelist:
- group: '*'
kind: '*'
description: Example infrastructure project
destinations:
- namespace: '*'
server: '*'
sourceRepos:
- '*'
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapplication
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: myproject
source:
path: argocd/apps
repoURL: https://github.com/アカウント名/リポジトリ名
targetRevision: main
directory:
recurse: true
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
syncOptions:
- CreateNamespace=false
automated:
selfHeal: false
argocd/bootstrap/manifests/application.yaml
の、.spec.source.path
と.spec.source.repoURL
とを見ていただくとわかるように、同じリポジトリ内のargocd/apps
ディレクトリをApplication
リソースのソース元に定義しています。ですので、次節ではargocd/apps
ディレクトリを説明していきます。
3-4. Argo CDでデプロイ管理するApplication群
argocd/apps
ディレクトリは以下のように3つのファイルから構成されています。
.
├── argocd
│ ├── apps
│ │ ├── argocd_image_updater.yaml
│ │ ├── external_secrets_operator.yaml
│ │ └── main.yaml
argocd/apps
ディレクトリ配下のそれぞれのファイルでは、1つずつApplication
リソースを定義しています。つまり、下図のように、argocd/bootstrap/manifests/application.yaml
で定義されたApplication
リソースの配下に3つのApplication
リソースがぶら下がっている構造[1]となっています。これにより、複数のApplication
リソースをArgo CDでのデプロイ管理下に置くことができます。
続いて、argocd/apps
ディレクトリ内の3つのファイルについて、それぞれ説明していきます。
3-4-1. External Secrets Operator
まず、argocd/apps/external_secrets_operator.yaml
についてですが、これはExternal Secrets OperatorというCRDをHelmでインストールするApplication
リソースを定義しています。External Secrets Operator自体はクラウドプロバイダが提供するシークレット管理サービスからSecret
リソースを作成するCRDであり、ここではSecrets Managerに格納されているDB秘匿情報をSecret
リソース化して、PodからDB接続情報を参照するために使用しています。
以下にargocd/apps/external_secrets_operator.yaml
の中身を示します。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: external-secrets
namespace: argocd
labels:
app.kubernetes.io/name: external-secrets
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: myproject
source:
repoURL: https://charts.external-secrets.io
targetRevision: 0.5.9
helm:
values: |
installCRDs: true
chart: external-secrets
destination:
server: https://kubernetes.default.svc
namespace: external-secrets
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
3-4-2. Argo CD Image Updater
次に、argocd/apps/argocd_image_updater.yaml
についてですが、これはArgo CD Image UpdaterというArgo CDの拡張機能をインストールするApplication
リソースを定義しています。
以下にargocd/apps/argocd_image_updater.yaml
の中身を示します。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: argocd-image-updater
namespace: argocd
labels:
app.kubernetes.io/name: argocd-image-updater
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: myproject
source:
repoURL: https://github.com/アカウント名/リポジトリ名
path: ./argocd_image_updater/overlays/production
targetRevision: main
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
.spec.source.repoURL
と.spec.source.path
とを見ていただければわかるように、同じリポジトリ内の.argocd_image_updater/overlays/production
をソース元に指定しています。ですので、次にこのディレクトリについて説明していきます。
argocd_image_updater/overlays/production
配下には以下のように4つのファイルが存在しています[2]。
.
├── argocd_image_updater
│ └── overlays
│ └── production
│ ├── config_map.yaml
│ ├── deployment.yaml
│ ├── kustomization.yaml
│ └── service_account.yaml
ここではKustomizeを利用してテンプレート生成を行なっていますので、まずはkustomization.yaml
から順番に見ていきます。kustomization.yaml
の中身は以下の通りです。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: argocd
resources:
- https://raw.githubusercontent.com/argoproj-labs/argocd-image-updater/stable/manifests/install.yaml
patchesStrategicMerge:
- config_map.yaml
- deployment.yaml
- service_account.yaml
kustomization.yaml
では、公開されているArgo CD Image Updaterインストール用のマニフェスト(https://raw.githubusercontent.com/argoproj-labs/argocd-image-updater/stable/manifests/install.yaml
)をソース元としています。そして、残りの3つのファイル(config_map.yaml
, deployment.yaml
, service_account.yaml
)でソース元を上書きし、自分用のマニフェストとしてArgo CD Image Updaterをインストールしています。
続いて、service_account.yaml
とconfig_map.yaml
とdeployment.yaml
についてそれぞれ説明していきます。
まず、service_account.yaml
を以下に示します。
apiVersion: v1
kind: ServiceAccount
metadata:
name: argocd-image-updater
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::AWSアカウント:role/role-for-image-updater
ここでは、argocd-image-updater
という名前のServiceAccount
リソースに対して、role-for-image-updater
というIAMロールを割り当てるように上書きしています。このrole-for-image-updater
は2-3節で作成したIAMロールであり、Argo CD Image UpdaterのPodがECRリポジトリにログインしてイメージの一覧を取得するのに必要なロールになります。
次に、config_map.yaml
を以下に示します。
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-image-updater-config
data:
registries.conf: |
registries:
- name: ECR
api_url: https://AWSアカウント.dkr.ecr.AWSリージョン.amazonaws.com
prefix: AWSアカウント.dkr.ecr.AWSリージョン.amazonaws.com
credentials: ext:/app/scripts/ecr-login.sh
credsexpire: 10h
ecr-login.sh: |
#!/bin/sh
aws ecr --region AWSリージョン get-authorization-token --output text --query 'authorizationData[].authorizationToken' | base64 -d
ここでは、ソース元で定義されているargocd-image-updater-config
という名のConfigMap
リソースを上書きしてregistries.conf
とecr-login.sh
とを作成しています。ecr-login.sh
はECRのプライベートレジストリにアクセスするための認証トークンを取得するシェルスクリプトとなっており、registries.conf
は、イメージレジストリとそれにアクセスするためのクレデンシャル情報を設定するファイルとなっています。registries.conf
の方でcredentials: ext:/app/scripts/ecr-login.sh
と定義されているのは、次に示すdeployment.yaml
の方でecr-login.sh
をapp/scripts
にマウントさせているからです。
続いて、deployment.yaml
を以下に示します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-image-updater
spec:
template:
spec:
containers:
- name: argocd-image-updater
volumeMounts:
- name: ecr-login-script
mountPath: /app/scripts
volumes:
- name: ecr-login-script
configMap:
defaultMode: 0755
items:
- key: ecr-login.sh
path: ecr-login.sh
name: argocd-image-updater-config
optional: true
前述した通り、ecr-login.sh
を/app/scripts
にマウントしており、このDeployment
リソースによって作成されるPodがecr-login.sh
を実行できるようになっています。また、このdeployment.yaml
からは見えませんが、ソース元であるhttps://raw.githubusercontent.com/argoproj-labs/argocd-image-updater/stable/manifests/install.yaml
の方で、先に説明したargocd-image-updater
というServiceAccount
リソースをこのDeployment
リソースに関連づけているため、ecr-login.sh
を実行すると実際に認証トークンが発行されるようになっています。
3-4-3. この記事でメインとなるApplicationリソース
最後に、argocd/apps/main.yaml
を以下に示します。これが今回Argo CDでデプロイするメインのApplication
リソースになります(メインまでが遠い...😅)。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: main
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
annotations:
argocd-image-updater.argoproj.io/write-back-method: argocd
argocd-image-updater.argoproj.io/write-back-target: kustomization:../../main/overlays/production
argocd-image-updater.argoproj.io/git-branch: main
argocd-image-updater.argoproj.io/image-list: app=AWSアカウント.dkr.ecr.AWSリージョン.amazonaws.com/eks-app,web=AWSアカウント.dkr.ecr.AWSリージョン.amazonaws.com/eks-web
argocd-image-updater.argoproj.io/app.update-strategy: semver
argocd-image-updater.argoproj.io/web.update-strategy: semver
spec:
project: myproject
source:
repoURL: https://github.com/アカウント名/リポジトリ名
path: ./main/overlays/production/
targetRevision: main
destination:
server: https://kubernetes.default.svc
namespace: main
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
このApplication
リソースで使われているイメージのタグをArgo CD Image Updaterを利用して自動更新していくので、argocd-image-updater.argoproj.io
から始まるアノテーションを駆使して設定を加えています。ここでは、アプリケーションイメージとNginxイメージとを対象として、新しいイメージを検知した際にgit管理を行わなずに直接Kubernetesを更新する設定としています。また、新しいイメージはセマンティックバージョニングのルールに従って検知していくように設定しています[3]。
そして、argocd/apps/main.yaml
のspec.source
からわかるように、このApplication
リソースのソース元は/main/overlays/production/
ディレクトリとなっています。ですので、次にmain
ディレクトリについて説明していきます。
まず、main
ディレクトリの構成を以下に示します。
.
└── main
├── base
│ ├── db_migrate.yaml
│ ├── external_dns.yaml
│ ├── external_secrets.yaml
│ ├── front.yaml
│ └── kustomization.yaml
└── overlays
└── production
└── kustomization.yaml
ディレクトリ構成とファイル名からわかるように、メインアプリケーションのテンプレート生成にはKustomizeを利用しており、/main/overlays/production/
ディレクトリ配下にはkustomization.yaml
の1ファイルのみが格納されています。ですので、argocd/apps/main.yaml
のリソース元となっているのはmain/overlays/production/kustomization.yaml
であり、このファイルの中身は以下のようになっています。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
commonLabels:
env: production
resources:
- ../../base
そしてこのファイルでは、ソース元としてさらにmain/base/
ディレクトリを相対パスで指定しており、このディレクトリ内のmain/base/kustomization.yaml
は以下のようになっています。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- db_migrate.yaml
- external_secrets.yaml
- external_dns.yaml
- front.yaml
つまり、このメインアプリケーションは/main/base/
ディレクトリ配下の4つのファイル(db_migrate.yaml
, external_secrets.yaml
, external_dns.yaml
, front.yaml
)で定義されていますので、次節にてこれらのファイルを順に説明していきます。
3-5. この記事でメインとなるApplicationリソースの構成
/main/base/
ディレクトリ配下の4つのファイルを説明する前に、これらのファイルが定義しているメインアプリケーションの構成について説明します。メインアプリケーションの構成は下図のようになっており、大きく以下の4つにグループ分けしています。
- アプリケーションをホストし、外部公開するためのリソース
- Secrets Managerに格納されたデータベースの秘匿値を元にSecretを作成するリソース
- パブリックホストゾーンにAレコードを追加するためのリソース
- DBマイグレートを行うリソース
それぞれグループを1つのファイルでリソース定義しており、これらを順番に説明していきます。
3-5-1. アプリケーションをホストし、外部公開するためのリソース
このグループでは、Deployment
リソースでアプリケーションコンテナとNginxコンテナを保有するPodをデプロイし、そこにService
リソースとIngress
リソースとを接続することでアプリケーションを外部公開しています。このとき、EKSクラスター構築時にAWS LoadBalancer Controllerをインストールしている[4]ので、Ingress
リソースのデプロイによって自動的にALBをプロビジョニングし、Ingress
リソースとの紐付けを行います。
上記を定義しているのがmain/base/front.yaml
であり、これを以下に示します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: front-deployment
spec:
replicas: 2
selector:
matchLabels:
app: front
template:
metadata:
labels:
app: front
spec:
containers:
- name: app
image: AWSアカウント.dkr.ecr.AWSリージョン.amazonaws.com/eks-app:v0.0.1
resources:
requests:
memory: "64Mi"
cpu: "200m"
limits:
memory: "128Mi"
cpu: "400m"
ports:
- containerPort: 8080
env: # DB接続のための秘匿情報をSecretリソースから取得して環境変数として設定している。
- name: DB_HOST
valueFrom:
secretKeyRef:
name: database-secrets
key: host
- name: DB_PORT
valueFrom:
secretKeyRef:
name: database-secrets
key: port
- name: DB_USER
valueFrom:
secretKeyRef:
name: database-secrets
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: database-secrets
key: password
- name: DB_NAME
valueFrom:
secretKeyRef:
name: database-secrets
key: dbname
- name: web
image: AWSアカウント.dkr.ecr.AWSリージョン.amazonaws.com/eks-web:v0.0.1
resources:
requests:
memory: "64Mi"
cpu: "200m"
limits:
memory: "128Mi"
cpu: "400m"
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: front-nodeport-service
spec:
type: NodePort
ports:
- name: http-port
protocol: TCP
port: 80
targetPort: 80
selector:
app: front
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: alb
spec:
controller: ingress.k8s.aws/alb
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: front-ingress
annotations:
alb.ingress.kubernetes.io/group.name: "main-ingress"
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
alb.ingress.kubernetes.io/target-type: ip # ALBがPod単位でアクセスを振り分けるようにする。
alb.ingress.kubernetes.io/healthcheck-port: '80'
alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
alb.ingress.kubernetes.io/healthcheck-path: '/health_check' # アプリケーション側で/health_checkをヘルスチェック用のパスとして実装している。
alb.ingress.kubernetes.io/healthcheck-interval-seconds: '15'
alb.ingress.kubernetes.io/healthcheck-timeout-seconds: '5'
alb.ingress.kubernetes.io/healthy-threshold-count: '2'
alb.ingress.kubernetes.io/unhealthy-threshold-count: '2'
alb.ingress.kubernetes.io/success-codes: '200'
spec:
ingressClassName: alb
rules:
- host: ドメイン名
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: front-nodeport-service
port:
number: 80
3-5-2. Secrets Managerに格納されたデータベースの秘匿値を元にSecretを作成するリソース
このグループでは、Secrets Managerに格納されたデータベース接続のための秘匿情報からSecret
リソースを作成しています。
まず、取得対象となるシークレットを下図に示します[5]。
そして、このシークレットからSecret
リソースの作成を定義しているのがmain/base/external_secrets.yaml
であり、以下にその中身を示します。
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::AWSアカウント:role/create-secret-from-secrets-manager-role
name: account-to-access-secrets
namespace: main
---
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: db-secrets-store
namespace: main
spec:
provider:
aws:
service: SecretsManager
region: AWSリージョン
auth:
jwt:
serviceAccountRef:
name: account-to-access-secrets
---
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
name: external-secrets
labels:
app: front
spec:
refreshInterval: 1m
secretStoreRef:
name: db-secrets-store
kind: SecretStore
target:
name: database-secrets
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: database-secrets
property: password
- secretKey: dbname
remoteRef:
key: database-secrets
property: dbname
- secretKey: port
remoteRef:
key: database-secrets
property: port
- secretKey: host
remoteRef:
key: database-secrets
property: host
- secretKey: username
remoteRef:
key: database-secrets
property: username
3-4-1節で説明した通り、既にExternal Secrets OperatorをインストールしているのでSecretStore
およびExternalSecret
というリソースが使用できる前提でmain/base/external_secrets.yaml
を説明します。
まず、2-3節で作成したSecrets Managerから秘匿情報を取得するためのIAMロール(create-secret-from-secrets-manager-role
)を紐付けたServiceAccount
を作成しています。次に、作成したServiceAccount
からSecrets Managerへアクセスを行うためにSecretStore
リソースを作成します。そして、Secrets ManagerのシークレットからどのようにSecret
リソースを作成するかをExternalSecret
リソースとして定義することで、DB接続情報を保有したSecret
リソースを作成しています。
3-5-3. パブリックホストゾーンにAレコードを追加するためのリソース
このグループでは、Ingress
リソース作成時に自動生成されるALBをターゲットとしてRoute53のホストゾーンにAレコードを自動追加するリソースを定義しています[6]。これを定義しているファイルがmain/base/external_dns.yaml
であり、以下にその中身を示します。
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::AWSアカウント:role/role-for-external-dns # 2-3節で作成したIAMロール。Route53のホストゾーンとそのレコードセット一覧を取得したり、レコードセットを編集する権限を保有したロール。
name: account-for-external-dns
namespace: main
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list","watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: account-for-external-dns
namespace: main
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: main
labels:
app: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: account-for-external-dns
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.12.2 # https://github.com/kubernetes-sigs/external-dns
args:
- --source=ingress
- --source=service
- --domain-filter=ドメイン名
- --provider=aws
- --policy=sync
- --aws-zone-type=public
- --registry=txt
- --txt-owner-id=eks-cluster # EKSクラスター名
securityContext:
fsGroup: 65534
ここでやっていることは見かけより単純で、必要な権限を与えたServiceAccount
を作成し、そのServiceAccount
をPod(Deployment
リソース)に関連づけた上で、Ingress
レコードから自動作成したALBをRoute53のホストゾーンのターゲットに追加するコンテナを起動しているだけです。
3-5-4. DBマイグレートを行うリソース
ここでは、Argo CDがマニフェスト用リポジトリと同期する直前に、DBマイグレートを行うためのリソースを定義しています。これを定義しているmain/base/db_migrate.yaml
を以下に示します。
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrator
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
activeDeadlineSeconds: 120
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: db-migrator
image: AWSアカウント.dkr.ecr.AWSリージョン.amazonaws.com/eks-migration:latest
imagePullPolicy: Always
resources:
requests:
memory: "64Mi"
cpu: "200m"
limits:
memory: "128Mi"
cpu: "400m"
command:
- "/bin/ash"
args:
- "-c"
- "/app/cmd"
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: database-secrets
key: host
- name: DB_PORT
valueFrom:
secretKeyRef:
name: database-secrets
key: port
- name: DB_USER
valueFrom:
secretKeyRef:
name: database-secrets
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: database-secrets
key: password
- name: DB_NAME
valueFrom:
secretKeyRef:
name: database-secrets
key: dbname
4. まとめ
この記事では、Argo CD Image Updaterを利用してGitOpsを構築する方法について説明しました。この記事とCDK for GoでEKS on EC2のインフラ構成を作ってみたとArgo CDのGetting Startedをシェルスクリプト化したとを合わせると、EKS on EC2の構築からArgo CDのインストールとGitOpsの構築までを一気通貫で説明したことになります。これらの記事を実践していくと、作業としては(1)cdk deploy
コマンドの実行と(2)GitHub Actionsでのイメージビルド&プッシュ実行と(3)argocd/bootstrap/init.sh
スクリプトの実行とで本格的なGitOpsが構築できます。たった3つの作業で自分専用のお砂場GitOps環境が構築できますので、ぜひお試しいただけたらと思います。アプリケーションをデプロイして遊ぶも良し、Kubernetesともっとお友達になるのも良しという感じで、誰かの何かのお役に立てれば幸いです^^
-
これをApp of Apps Patternと言います。 ↩︎
-
この4ファイルの作成にあたってはEKS環境へArgo CD Image Updaterを導入し、デプロイ時間と管理コストを削減した話を参考にさせていただきました。 ↩︎
-
CDKでプロビジョニングしたEKSクラスターにAWS LoadBalancer Controllerをインストールする方法はこちらをご参照ください。 ↩︎
-
詳細はAWSの公式ドキュメントをご参照ください。 ↩︎
Discussion