EKSにデプロイしたGitLabのオブジェクトストレージをS3に変更する
Helmを使って、EKSにGitLabを構築する機会がありました。
デフォルト設定では、オブジェクトストレージとしてminioが使われます。
minioは同一クラスター上でPodsとして動作しており、データはノードのEBS上に保存されます。
ノードの置き換え時にデータが消えてしまうことや、S3と比べて割高なため、minioをS3に置き換えます。
前提
- EKSにHelmを使ってGitLabを構築済みであること
S3バケットの作成
S3バケットを作成します。公式ドキュメントに以下の記載があり、それぞれバケットを分けて作成します。
A different bucket is needed for each, otherwise performing a restore from backup doesn’t function properly.
翻訳: それぞれに異なるバケットが必要です。そうしないと、バックアップからの復元が正しく機能しません。
Configure the GitLab chart with an external object storage | GitLab
作成するS3バケットの概要
今回作成するS3バケットは以下です。
S3バケット | 概要 |
---|---|
registry | コンテナイメージ |
lfs | Git Large File Storage objects |
artifact | CI/CDジョブのartifacts |
uploads | ユーザーのアップロード(アイコン画像等) |
packages | プロジェクト パッケージ (PyPI、Maven、NuGet等) |
backups | バックアップ |
tmp | バックアップを復元するときに一時的に使用 |
サンプルのvalues.yamlを参考に最低限のS3バケットを作成しています。
以下の場合は、ドキュメントを参考に追加でS3バケットを用意してください。
- Terraform Stateの管理をGitLabで行いたい(
terraform_state
) - マージリクエストの差分保存をDBからオブジェクトストレージを移したい(
external_diffs
)
TerraformによるS3バケットとIAMロールの作成
それぞれのバケットを作成します。
また、PodsからS3バケットにアクセスするためのIAM Roleも作成します。
Terraformコードは以下です。
terraform {
required_version = "~> 1.10.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.82.0"
}
}
}
provider "aws" {
region = var.region
}
variable "name_prefix" {}
variable "region" {
default = "ap-northeast-1"
}
variable "oidc_provider_arn" {}
locals {
buckets_names = [
"registry",
"lfs",
"artifacts",
"uploads",
"packages",
"backups",
"tmp"
]
oidc_provider = element(split("/id/", var.oidc_provider_arn), 1)
}
data "aws_kms_key" "aws_s3" {
key_id = "alias/aws/s3"
}
resource "aws_s3_bucket" "gitlab" {
for_each = toset(local.buckets_names)
bucket = "${var.name_prefix}-${each.key}"
force_destroy = true
}
resource "aws_s3_bucket_versioning" "gitlab" {
for_each = aws_s3_bucket.gitlab
bucket = each.value.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "gitlab" {
for_each = aws_s3_bucket.gitlab
bucket = each.value.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = data.aws_kms_key.aws_s3.arn
}
}
}
resource "aws_s3_bucket_public_access_block" "gitlab" {
for_each = aws_s3_bucket.gitlab
bucket = each.value.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
## S3 access policy for all buckets except registry and backups (covered by separate policies)
resource "aws_iam_policy" "gitlab_s3_policy" {
name = "${var.name_prefix}-s3-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
]
Effect = "Allow"
Resource = [for k, v in aws_s3_bucket.gitlab : "${v.arn}/*" if !contains(["registry", "backups"], k)]
},
# Backup Utility and Geo Replication
{
Action = [
"s3:ListBucket",
]
Effect = "Allow"
Resource = [for k, v in aws_s3_bucket.gitlab : v.arn if !contains(["registry", "backups"], k)]
},
]
})
}
resource "aws_iam_policy" "gitlab_s3_backups_policy" {
name = "${var.name_prefix}-s3-backups-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
]
Effect = "Allow"
Resource = ["${aws_s3_bucket.gitlab["backups"].arn}/*"]
}
]
})
}
## S3 access policy for registry bucket
resource "aws_iam_policy" "gitlab_s3_registry_policy" {
name = "${var.name_prefix}-s3-registry-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# Docker Registry S3 bucket requires specific permissions
# https://docs.docker.com/registry/storage-drivers/s3/#s3-permission-scopes
{
Action = [
"s3:ListBucket",
"s3:GetBucketLocation",
"s3:ListBucketMultipartUploads",
]
Effect = "Allow"
Resource = aws_s3_bucket.gitlab["registry"].arn
},
{
Action = [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListMultipartUploadParts",
"s3:AbortMultipartUpload"
]
Effect = "Allow"
Resource = "${aws_s3_bucket.gitlab["registry"].arn}/*"
},
]
})
}
resource "aws_iam_policy" "gitlab_s3_kms_policy" {
name = "${var.name_prefix}-s3-kms-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"kms:Decrypt",
"kms:GenerateDataKey"
]
Effect = "Allow"
Condition = {
"StringLike" = {
"kms:EncryptionContext:aws:s3:arn" = [
for key, values in aws_s3_bucket.gitlab : values.arn
]
}
},
Resource = data.aws_kms_key.aws_s3.arn
}
]
})
}
resource "aws_iam_role" "gitlab_eks_global" {
name = "${var.name_prefix}-eks-global"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRoleWithWebIdentity"
Effect = "Allow"
Principal = { Federated = var.oidc_provider_arn }
Condition = {
"StringEquals" = {
"${local.oidc_provider}:aud" = "sts.amazonaws.com",
"${local.oidc_provider}:sub" = "system:serviceaccount:gitlab:gitlab-global"
}
}
},
]
})
}
resource "aws_iam_role_policy_attachment" "gitlab_s3_policy" {
policy_arn = aws_iam_policy.gitlab_s3_policy.arn
role = aws_iam_role.gitlab_eks_global.name
}
resource "aws_iam_role_policy_attachment" "gitlab_s3_backups_policy" {
policy_arn = aws_iam_policy.gitlab_s3_backups_policy.arn
role = aws_iam_role.gitlab_eks_global.name
}
resource "aws_iam_role_policy_attachment" "gitlab_s3_registry_policy" {
policy_arn = aws_iam_policy.gitlab_s3_registry_policy.arn
role = aws_iam_role.gitlab_eks_global.name
}
resource "aws_iam_role_policy_attachment" "gitlab_s3_kms_policy" {
policy_arn = aws_iam_policy.gitlab_s3_kms_policy.arn
role = aws_iam_role.gitlab_eks_global.name
}
output "gitlab_eks_global_role" {
value = aws_iam_role.gitlab_eks_global.arn
}
terraform.tfvars
で任意の値を渡して、リソースを作成します。
name_prefix = "" # Example: example-gitlab-eks
region = "" # Example: us-east-1
oidc_provider_arn = "" # Example: arn:aws:iam::0123456890:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/HOGEHOGEHOGE
oidc_provider_arn
は、IAMのIDプロバイダから確認できます。
AWS CLIを使う場合は、以下コマンドで取得できます。
# 1. クラスターのOIDC IDを取得
CLUSTER_ID=$(aws eks describe-cluster --name <クラスター名> \
--query "cluster.identity.oidc.issuer" \
--output text --region us-east-2 | cut -d'/' -f5)
# 2. OIDC IDを使ってARNをフィルタリング
aws iam list-open-id-connect-providers \
--query "OpenIDConnectProviderList[?contains(Arn, '${CLUSTER_ID}')].Arn" \
--output text
Terraformを実行して、リソースを作成します。
terraform init
terraform plan
terraform apply
IAM Role ARNが出力されますので、これを控えておきます。
今回は、手順を簡略化するためIAMロールを1つだけ作りました。
IAMロールやServiceAccountを複数作成することで、細かく権限管理をすることも可能です。(例: registry用のPodsはregistryのS3バケットにしかアクセスできない)
TerraformコードやIAMポリシーは以下を参考にしました。
ServiceAccountの作成
PodsからS3バケットにアクセスするために、ServiceAccountを用意します。
kubectl create serviceaccount gitlab-global
作成したServiceAccoutにアノテーションを追加します。
IAMロールARNは先程Terraformで作成したものを記載します。
kubectl annotate serviceaccount -n gitlab gitlab-global eks.amazonaws.com/role-arn=<IAMロールARN>
Secretsの作成
GitLabがs3cmdやRailsを使用するため、Configファイルが必要です。
helmチャート側ではConfigファイルの内容をSecretsで渡しています。
以下のConfigファイルを用意します。リージョン名やバケット名は各自の環境に合わせて、置き換えてください。
provider: AWS
use_iam_profile: true
region: <リージョン名> # Example: us-east-1
[default]
bucket_location = <リージョン名> # Example: us-east-1
registry.yaml
のバケット名は、先程Terraformで作成したregistryバケットのバケット名を入れます。
registryバケットは、本記事のTerraformで作った場合は「<name_prefix>-registry」の名前で作成されます。
s3:
bucket: <バケット名> # Example: example-gitlab-eks-registry
region: <リージョン名> # Example: us-east-1
v4auth: true
kubectl create secret generic gitlab-rails-storage \
--from-file=connection=rails.yaml
kubectl create secret generic s3cmd-config \
--from-file=config=s3cmd.conf
kubectl create secret generic registry-storage \
--from-file=config=registry.yaml
values.yaml
の準備
GitLab Helm Chartにて、S3を使うサンプルが公開されていました。
examples/values-external-objectstorage.yaml · master · GitLab.org / charts / GitLab Chart · GitLab
上記を参考に、values.yaml
に以下の記述を追加します。
バケットのexample-gitlab-*
の部分は環境に合わせて置き換えてください。
global:
serviceAccount:
# グローバルServiceAccount設定
# このSAは以下のコンポーネントで使用されます:
# - webservice
# - sidekiq
# - その他のGitLabコアコンポーネント
#
# 以下のコンポーネントは独自のSAを使用します:
# - certmanager (gitlab-certmanager)
# - redis (gitlab-redis)
# - prometheus (gitlab-prometheus-server)
enabled: true
create: false # name指定でtrueはできないため、事前にsaを作成する
name: gitlab-global
minio:
enabled: false
registry:
bucket: example-gitlab-eks-registry
appConfig:
lfs:
bucket: example-gitlab-eks-lfs
connection:
secret: gitlab-rails-storage
key: connection
artifacts:
bucket: example-gitlab-eks-artifacts
connection:
secret: gitlab-rails-storage
key: connection
uploads:
bucket: example-gitlab-eks-uploads
connection:
secret: gitlab-rails-storage
key: connection
packages:
bucket: example-gitlab-eks-packages
connection:
secret: gitlab-rails-storage
key: connection
backups:
bucket: example-gitlab-eks-backups
tmpBucket: example-gitlab-eks-tmp
toolbox:
backups:
objectStorage:
config:
secret: s3cmd-config
key: config
registry:
enabled: true
service:
type: NodePort
storage:
secret: registry-storage
key: config
ALB設定含むバージョン
nginx-ingress:
enabled: false
global:
serviceAccount:
# グローバルServiceAccount設定
# このSAは以下のコンポーネントで使用されます:
# - webservice
# - sidekiq
# - その他のGitLabコアコンポーネント
#
# 以下のコンポーネントは独自のSAを使用します:
# - certmanager (gitlab-certmanager)
# - redis (gitlab-redis)
# - prometheus (gitlab-prometheus-server)
enabled: true
create: false # name指定でtrueはできないため、事前にsaを作成する
name: gitlab-global
hosts:
domain: <ドメイン名>
ingress:
# Common annotations used by kas, registry, and webservice
annotations:
alb.ingress.kubernetes.io/backend-protocol: HTTP
alb.ingress.kubernetes.io/certificate-arn: <ACM ARN>
alb.ingress.kubernetes.io/group.name: gitlab
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443}]'
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
kubernetes.io/ingress.class: alb
nginx.ingress.kubernetes.io/connection-proxy-header: "keep-alive"
class: none
configureCertmanager: false
enabled: true
path: /*
pathType: ImplementationSpecific
provider: aws
tls:
enabled: false
minio:
enabled: false
registry:
bucket: example-gitlab-eks-registry
appConfig:
lfs:
bucket: example-gitlab-eks-lfs
connection:
secret: gitlab-rails-storage
key: connection
artifacts:
bucket: example-gitlab-eks-artifacts
connection:
secret: gitlab-rails-storage
key: connection
uploads:
bucket: example-gitlab-eks-uploads
connection:
secret: gitlab-rails-storage
key: connection
packages:
bucket: example-gitlab-eks-packages
connection:
secret: gitlab-rails-storage
key: connection
backups:
bucket: example-gitlab-eks-backups
tmpBucket: example-gitlab-eks-tmp
gitlab:
kas:
enabled: true
ingress:
# Specific annotations needed for kas service to support websockets
annotations:
alb.ingress.kubernetes.io/healthcheck-path: /liveness
alb.ingress.kubernetes.io/healthcheck-port: "8151"
alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=4000,routing.http2.enabled=false
alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=86400
alb.ingress.kubernetes.io/target-type: ip
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/connection-proxy-header: "keep-alive"
nginx.ingress.kubernetes.io/x-forwarded-prefix: "/path"
# k8s services exposed via an ingress rule to an ELB need to be of type NodePort
service:
type: NodePort
webservice:
enabled: true
service:
type: NodePort
# gitlab-shell (ssh) needs an NLB
gitlab-shell:
enabled: false
toolbox:
backups:
objectStorage:
config:
secret: s3cmd-config
key: config
registry:
enabled: true
service:
type: NodePort
storage:
secret: registry-storage
key: config
global.serviceAccount.create
をtrue
にすることで、ServiceAccountをHelm側で作成することも可能です。
この場合、各コンポーネントごとにServiceAccountが作成されます。
検証用にIAM Roleの設定をシンプルにするため、1つのServiceAccountを使用することにしました。そのため、今回はfalse
としています。
また、global.serviceAccount.name
とglobal.serviceAccount.create
は同時に設定できません。
global.serviceAccount.create
をtrue
にする場合は、ServiceAccount名が自動で設定されることに気をつけてください。
Do not use global.serviceAccount.create=true with global.serviceAccount.name, as it instructs the charts to create multiple ServiceAccount objects with the same name. Instead, use global.serviceAccount.create=false if specifying a global name.
Service Account |Configure charts using globals | GitLabより引用
Helmリリースの更新
用意したvalues.yaml
を使って、リソースを更新します。
helm upgrade gitlab gitlab/gitlab -n gitlab -f values.yaml
動作確認
GitLabのProfileのアイコンを変更して、アイコン画像がS3にアップロードされるか確認してみます。
ログイン後、左上のアイコンからEdit profile
を選択します。
Choose file...
を選択して、適当なファイルを指定します。
Update profile settings
でProfileを更新します。
uploads
用のS3バケット(S3バケット名は<name-prefix>-upload
)を見てみると、avatar.png
という名前で保存されていることが確認できました。
おわりに
オブジェクトストレージをデフォルトから、S3に変更してみました。
以下の改善点はありますが、S3への変更の最低限の部分はできたかと思います。
- Pod Identityの利用
- kubectl・helm cli部分のTerraform化
DBやキャッシュの外出しの対応を引き続き検証してみたいと思います。
以上、AWS事業本部の佐藤(@chari7311)でした。