CDKでセルフマネージドGitlabの検証環境を構築するときにハマったこと

CDKでセルフマネージドGitlabの検証環境を構築するときにハマったこと

2025.09.24

はじめに

みなさんこんにちは、クラウド事業本部コンサルティング部の浅野です。
セルフホスティング型のGitlabの検証環境をサクッと構築してみたい時がありませんか。
Gitlabの各機能をサクッと検証したいときに備えて、CDKでセルフマネージドなGitlabのコミュニティエディション環境を構築してみました。

EC2内で手動でインストールする時とは異なり、ユーザーデータ経由でGitlabの設定をすると、問題が発生したので対応までの解説も交えていきます。

構成

EC2

  • t3.medium (x86_64) EC21台
  • OS : Amazon Linux 2023

プライベート環境にEC2を配置し、SSL終端はALBで行う構成をとっています。

2025-09-24-cdk-self-managed-gitlab-ce-demo-01

プロジェクト構造

ソースコード全体に関しては以下を参照してください。

https://github.com/haruki-0408/demo-self-managed-gitlab

ディレクトリ構成は以下の通りです。

CDKプロジェクトディレクトリ構成
			
			.
├── README.md
├── bin
├── cdk.context.json
├── cdk.json
├── cdk.out
├── jest.config.js
├── lib
│   └── demo-self-managed-gitlab-stack.ts
├── node_modules
├── package-lock.json
├── package.json
├── parameters.example.ts
├── parameters.ts
├── scripts
│   └── gitlab-setup.sh
└── tsconfig.json


		

scripts/gitlab-setup.shにGitlab設定を行うユーザーデータスクリプトを配置してlib/demo-self-managed-gitlab-stack.tsにて参照する構成をとっています。

CDK

事前作業

今回はALBに独自ドメイン & SSL証明書設定(ACM管理)でGitlab環境を立ち上げることを前提としているため、事前作業としてドメインの用意とSSL証明書の用意を別途行ってください。

事前準備が完了したらparameter.tsに以下のパラメータを設定します。

  • ドメイン名
  • ACMにて用意したSSL証明書のARN
parameter.ts
			
			export interface GitLabParameters {
  domainName: string;           // GitLabのドメイン名
  certificateArn: string;      // 既存のSSL証明書ARN(AWS Certificate Manager)
}

// 実際のパラメータを設定してください
export const parameters: GitLabParameters = {
  domainName: 'your-domain.example.com',     // ⚠️ 実際のドメイン名に変更してください
  certificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',  // ⚠️ 実際の証明書ARNに変更してください
};

		

これらのパラメータは、ユーザーデータ内のGitlab設定に反映させています。

スタックファイル

ClaudeCodeで作成しました。スタック内で構成図に必要なリソースを定義しています。特殊なことはしておらず、別ファイルで切り出したユーザーデータスクリプトをスタックファイルで参照しています。参考までに添付しておきます。

スタックファイル
			
			import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as targets from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';
import * as certificatemanager from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as route53targets from 'aws-cdk-lib/aws-route53-targets';
import { readFileSync } from 'fs';
import { join } from 'path';

export interface GitLabStackProps extends cdk.StackProps {
  domainName: string;
  instanceType?: ec2.InstanceType;
  keyPairName?: string;
  certificateArn: string;  // 既存のSSL証明書ARN
}

export class DemoSelfManagedGitlabStack extends cdk.Stack {
  public readonly instance: ec2.Instance;
  public readonly secret: secretsmanager.Secret;
  public readonly logGroup: logs.LogGroup;
  public readonly loadBalancer: elbv2.ApplicationLoadBalancer;
  public readonly certificate: certificatemanager.ICertificate;

  constructor(scope: Construct, id: string, props: GitLabStackProps) {
    super(scope, id, props);

    // VPC - Public/Private Subnet構成
    const vpc = new ec2.Vpc(this, 'GitLabVPC', {
      maxAzs: 2,
      natGateways: 1, // 単一のNAT Gateway
      natGatewaySubnets: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
    });

    // Security Group for ALB
    const albSecurityGroup = new ec2.SecurityGroup(this, 'GitLabALBSecurityGroup', {
      vpc,
      description: 'Security group for GitLab ALB',
      allowAllOutbound: true,
    });

    albSecurityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(80),
      'HTTP access'
    );
    albSecurityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(443),
      'HTTPS access'
    );

    // Security Group for EC2 (Private)
    const ec2SecurityGroup = new ec2.SecurityGroup(this, 'GitLabEC2SecurityGroup', {
      vpc,
      description: 'Security group for GitLab EC2 instance',
      allowAllOutbound: true,
    });

    ec2SecurityGroup.addIngressRule(
      albSecurityGroup,
      ec2.Port.tcp(80),
      'HTTP from ALB'
    );

    // GitLab root password保存用 Secrets Manager
    this.secret = new secretsmanager.Secret(this, 'GitLabRootPassword', {
      secretName: 'gitlab-root-password',
      description: 'GitLab root user password',
      generateSecretString: {
        secretStringTemplate: JSON.stringify({ username: 'root' }),
        generateStringKey: 'password',
        excludeCharacters: '"@/\\\'',
        passwordLength: 32,
      },
    });

    // IAM Role for EC2
    const role = new iam.Role(this, 'GitLabInstanceRole', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
      ],
    });

    this.secret.grantWrite(role);
    this.secret.grantRead(role);

    // CloudWatch Log Groupの作成
    this.logGroup = new logs.LogGroup(this, 'GitLabLogGroup', {
      logGroupName: `/aws/ec2/gitlab/${props.domainName}`,
      retention: logs.RetentionDays.ONE_MONTH,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    role.addToPolicy(new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        'logs:CreateLogGroup',
        'logs:CreateLogStream',
        'logs:PutLogEvents',
        'logs:DescribeLogStreams',
        'logs:DescribeLogGroups',
      ],
      resources: [this.logGroup.logGroupArn + '*'],
    }));

    role.addToPolicy(new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        'ec2:DescribeVolumes',
        'ec2:DescribeTags',
      ],
      resources: ['*'],
    }));


    // 既存のSSL証明書を参照
    this.certificate = certificatemanager.Certificate.fromCertificateArn(
      this, 
      'GitLabCertificate', 
      props.certificateArn
    );

    // UserData script
    const userDataScript = this.loadUserDataScript(props.domainName, this.secret.secretArn);

    // EC2 Instance - Private Subnetに配置
    this.instance = new ec2.Instance(this, 'GitLabInstance', {
      instanceType: props.instanceType || ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
      }),
      vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
      },
      securityGroup: ec2SecurityGroup,
      role,
      keyPair: props.keyPairName ? ec2.KeyPair.fromKeyPairName(this, 'GitLabKeyPair', props.keyPairName) : undefined,
      userData: ec2.UserData.custom(userDataScript),
      userDataCausesReplacement: true,
      blockDevices: [
        {
          deviceName: '/dev/xvda',
          volume: ec2.BlockDeviceVolume.ebs(30, {
            volumeType: ec2.EbsDeviceVolumeType.GP3,
          }),
        },
      ],
    });

    // Application Load Balancer
    this.loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'GitLabALB', {
      vpc,
      internetFacing: true,
      securityGroup: albSecurityGroup,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
    });

    // Target Group
    const targetGroup = new elbv2.ApplicationTargetGroup(this, 'GitLabTargetGroup', {
      vpc,
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP,
      targets: [new targets.InstanceTarget(this.instance)],
      healthCheck: {
        path: '/-/health',
        protocol: elbv2.Protocol.HTTP,
        healthyHttpCodes: '200',
        interval: cdk.Duration.seconds(60),
        timeout: cdk.Duration.seconds(10),
        healthyThresholdCount: 2,
        unhealthyThresholdCount: 10,
      },
    });

    // HTTP Listener (リダイレクト用)
    this.loadBalancer.addListener('HTTPListener', {
      port: 80,
      defaultAction: elbv2.ListenerAction.redirect({
        protocol: 'HTTPS',
        port: '443',
        permanent: true,
      }),
    });

    // HTTPS Listener
    this.loadBalancer.addListener('HTTPSListener', {
      port: 443,
      certificates: [this.certificate],
      defaultTargetGroups: [targetGroup],
    });

    // Route53 Aレコード作成
    const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
      domainName: props.domainName,
    });

    new route53.ARecord(this, 'GitLabDNSRecord', {
      zone: hostedZone,
      recordName: props.domainName,
      target: route53.RecordTarget.fromAlias(new route53targets.LoadBalancerTarget(this.loadBalancer)),
    });
  }

  private loadUserDataScript(domain: string, secretArn: string): string {
    try {
      // scripts/gitlab-setup.sh を読み込み
      const scriptPath = join(__dirname, '..', 'scripts', 'gitlab-setup.sh');
      let script = readFileSync(scriptPath, 'utf8');
      
      // 変数置換のみ実行
      script = script.replace(/\$\{DOMAIN_NAME\}/g, domain);
      script = script.replace(/\$\{SECRET_ARN\}/g, secretArn);
      
      return script;
    } catch (error) {
      throw new Error(`Failed to load GitLab setup script: ${error}`);
    }
  }
}


		

発生した問題

まず当初予定していたユーザーデータは以下です。

動作不良のユーザーデータスクリプト
			
			#!/bin/bash
set -e

# システムアップデート
dnf update -y

# GitLab公式リポジトリを追加
curl "https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh" | bash

# GitLab CEをインストール
export EXTERNAL_URL="http://${DOMAIN_NAME}"

dnf install -y gitlab-ce

# GitLab設定ファイルを作成
cat >> /etc/gitlab/gitlab.rb << EOF
nginx['listen_port'] = 80
nginx['listen_https'] = false
EOF

# GitLab設定実行
gitlab-ctl reconfigure # >>>>>>>>> 問題発生箇所 <<<<<<<<<<

# GitLabの初期rootパスワードを取得してSecrets Managerに保存
if [ -f "/etc/gitlab/initial_root_password" ]; then
  ROOT_PASSWORD=$(grep '^Password:' /etc/gitlab/initial_root_password | sed 's/^Password: //')
  if [ -n "$ROOT_PASSWORD" ]; then
    aws secretsmanager put-secret-value --secret-id "${SECRET_ARN}" --secret-string "{\"username\":\"root\",\"password\":\"$ROOT_PASSWORD\"}" --region "ap-northeast-1"
  fi
fi

		

手順としては以下の公式ドキュメントに沿ったシンプルな内容です。

https://docs.gitlab.com/install/package/amazonlinux_2023/

リポジトリからパッケージをダウンロードしてきて外部URL等の環境変数を設定し、ALBでSSL終端するための設定を入れて、設定を読み込んで起動し、その後EC2内に保管されているルートパスワードをAWS SecretsManagerに保管するという流れでした。

「gitlab-ctl reconfigure」 の途中でフリーズする

上記のユーザーデータをもとに実際にデプロイし、EC2が立ち上がってからGitlabの起動を待ちましたが、dnf install -y gitlab-ceのインストール終了後、gitlab-ctl reconfigureの実行がなかなか進みませんでした。

EC2内に入り/var/log/cloud-init-output.logを確認してみると以下の箇所でフリーズ(ハング)していました。

フリーズ箇所
			
			sh-5.2$ sudo tail -f /var/log/cloud-init-output.log

.
.
.
    - execute systemctl daemon-reload
[2025-09-17T01:47:05+00:00] INFO: template[/usr/lib/systemd/system/gitlab-runsvdir.service] sending run action to execute[systemctl enable gitlab-runsvdir] (immediate)
  * execute[systemctl enable gitlab-runsvdir] action run
    [execute] Created symlink /etc/systemd/system/multi-user.target.wants/gitlab-runsvdir.service → /usr/lib/systemd/system/gitlab-runsvdir.service.
[2025-09-17T01:47:05+00:00] INFO: execute[systemctl enable gitlab-runsvdir] ran successfully
    - execute systemctl enable gitlab-runsvdir
[2025-09-17T01:47:05+00:00] INFO: template[/usr/lib/systemd/system/gitlab-runsvdir.service] sending run action to execute[systemctl start gitlab-runsvdir] (immediate)
  * execute[systemctl start gitlab-runsvdir] action run  # ここで一生進まない

		

今回はコミュニティエディションの v18.3.2 for AmazonLinux2023をインストールしようとしていますが、異なるバージョンでも必ず同じ箇所でフリーズしました。

原因

今回のフリーズの原因を探っていくうちに「systemd」の設定と起動の依存関係が深く関わっていることが判明しました。まずはそれらの説明に移る前に「systemd」に関する基本的な内容を勉強したので解説しておきます。

systemdとは

LinuxのOSは、電源を入れたときに「最初に起動するプログラム(initシステム)」が存在します。
このプログラムがOSの立ち上げを管理し、必要なサービス(例: ネットワーク、ログ管理、ユーザーのログイン環境など)を順番に起動していきます。

昔は「SysV init」という仕組みが主流でしたが、現在多くのLinuxディストリビューションで使われているのが systemd(システムディー) です。

systemdの役割は簡単に言うと以下です。

  • OS起動時に必要なサービスを立ち上げる
  • 不要になったサービスを止める
  • サービスが異常終了したら再起動する
  • ログや依存関係を整理して、システム全体を効率的に管理する

つまり「Linux全体のスタートボタン兼 管理人」のような存在です。

ターゲットとは何か

systemdは「どんな状態のシステムを用意するか」を ターゲット という単位で管理します。

ターゲットは「目標とするシステム状態」を表すと同時に、その状態を実現するために必要な「サービス群の集合体」でもあります。つまり、特定のシステムレベルに必要なサービスをまとめたグループのようなものです。

たとえば代表的なターゲットに以下のようなものがあります。

  • default.target
    システムの標準的な起動状態。通常はmulti-user.targetやgraphical.targetへのシンボリックリンクになっている。

  • rescue.target
    シングルユーザーモード。最小限のサービスだけで立ち上げ、トラブル対応に使う。

  • multi-user.target
    複数ユーザーがログインできる状態(テキストログイン)を実現するためのサービス群。SSH、ネットワーク、各種アプリケーション(GitLabなど)が含まれる。サーバー用途でよく使う。

  • graphical.target
    デスクトップ環境(GUI)を実現するためのサービス群。multi-user.targetのサービスに加えて、ディスプレイマネージャーやGUIアプリケーションが含まれる。PCユーザー向け。

各サービス(GitLabなど)は特定のターゲットに属し、そのターゲットが起動されるときに一緒に起動されます。

実際の原因調査

それでは、実際のフリーズ箇所から原因を切り分けていきます。先ほどのフリーズ箇所で実行していたコマンドを確認すると以下でした。

systemctl start gitlab-runsvdir

この箇所で起動しようとしているサービスのステータスを確認してみます。

サービスステータス確認
			
			systemctl status gitlab-runsvdir
  ○ gitlab-runsvdir.service - GitLab Runit supervision process
     Loaded: loaded (/usr/lib/systemd/system/gitlab-runsvdir.service;
   enabled; preset: disabled)
     Active: inactive (dead)

		

「inactive」状態になり、起動に失敗していることがわかります。続いてロードしているサービスファイルを読み込んでみてどんな内容か確認してみます。

/usr/lib/systemd/system/gitlab-runsvdir.service
			
			sudo cat /usr/lib/systemd/system/gitlab-runsvdir.service

[Unit]
Description=GitLab Runit supervision process
After=multi-user.target

[Service]
Slice=gitlab.slice
Delegate=memory cpu
ExecStart=/opt/gitlab/embedded/bin/runsvdir-start
Restart=always
TasksMax=4915

[Install]
WantedBy=multi-user.target

		

この設定ファイル内の一部を解説します。ここで先ほど解説したターゲットの話が出てきます。

  • WantedBy=multi-user.target: GitLabサービスをmulti-user.targetの構成要素として登録します。
  • After=multi-user.target: multi-user.targetの起動完了を待ってからGitLabが起動開始します。
  • ExecStart=〜: /opt/gitlab/embedded/bin/runsvdir-startスクリプトを実行します。

ではExecStartで実際に実行されるスクリプトの中身を確認してみましょう。

			
			sudo cat /opt/gitlab/embedded/bin/runsvdir-start

#!/bin/bash
#
# Copyright 2012-2025 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

PATH=/opt/gitlab/bin:/opt/gitlab/embedded/bin:/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin

ulimit -c 0
ulimit -d unlimited
ulimit -e 0
ulimit -f unlimited
ulimit -i 62793
ulimit -l 64
ulimit -m unlimited
# WARNING: Increasing the global file descriptor limit increases RAM
# consumption on startup dramatically!
ulimit -n 50000
ulimit -q 819200
ulimit -r 0
ulimit -s 10240
ulimit -t unlimited
ulimit -u unlimited
ulimit -v unlimited
ulimit -x unlimited
echo "1000000" > /proc/sys/fs/file-max

umask 022

exec env - PATH=$PATH \
runsvdir -P /opt/gitlab/service 'log: ...........................................................................................................................................................................................................................................................................................................................................................................................................'

		

このスクリプトでは/opt/gitlab/service/の中身を監視しようとしています。

			
			ls -la /opt/gitlab/service/
total 16
drwxr-xr-x.  2 root root     6 Sep 17 06:55 .
drwxr-xr-x. 10 root root 16384 Sep 24 01:50 ..

		

しかし、実際に中身を確認すると監視すべきファイルやディレクトリの内容が存在せず、空のままです。

次に「systemd」の起動に関する依存関係を追ってみましょう。

			
			sudo systemctl list-dependencies

default.target
○ ├─display-manager.service
○ ├─systemd-update-utmp-runlevel.service
○ └─multi-user.target
●   ├─amazon-ssm-agent.service
●   ├─atd.service
●   ├─auditd.service
●   ├─chronyd.service
○   ├─gitlab-runsvdir.service # 起動に失敗している
○   ├─hibinit-agent.service
●   ├─irqbalance.service
●   ├─libstoragemgmt.service
●   ├─sshd.service
○   ├─sssd.service
●   ├─sysstat.service
●   ├─systemd-ask-password-wall.path
●   ├─systemd-homed.service
●   ├─systemd-logind.service
●   ├─systemd-networkd.service
○   ├─systemd-update-utmp-runlevel.service
●   ├─systemd-user-sessions.service
○   ├─update-motd.service
●   ├─basic.target
●   │ ├─-.mount
○   │ ├─rpmdb-rebuild.service
●   │ ├─tmp.mount
●   │ ├─paths.target
●   │ ├─slices.target
●   │ │ ├─-.slice
●   │ │ └─system.slice
●   │ ├─sockets.target
●   │ │ ├─dbus.socket
●   │ │ ├─sssd-kcm.socket
●   │ │ ├─systemd-coredump.socket
●   │ │ ├─systemd-initctl.socket
●   │ │ ├─systemd-journald-audit.socket
●   │ │ ├─systemd-journald-dev-log.socket
●   │ │ ├─systemd-journald.socket
●   │ │ ├─systemd-networkd.socket
●   │ │ ├─systemd-udevd-control.socket
●   │ │ ├─systemd-udevd-kernel.socket
●   │ │ └─systemd-userdbd.socket
●   │ ├─sysinit.target
●   │ │ ├─dev-hugepages.mount
●   │ │ ├─dev-mqueue.mount
●   │ │ ├─dracut-shutdown.service
○   │ │ ├─import-state.service
●   │ │ ├─kmod-static-nodes.service
○   │ │ ├─ldconfig.service
●   │ │ ├─proc-sys-fs-binfmt_misc.automount
○   │ │ ├─selinux-autorelabel-mark.service
●   │ │ ├─sys-fs-fuse-connections.mount
●   │ │ ├─sys-kernel-config.mount

		

結果を確認すると 「multi-user.target」群に指定された「gitlab-runsvdir.service」が起動されていませんね。

結局の原因

これまでの調査と下記Gitlabのissueページから、ユーザーデータ経由でGitLabが起動しない根本的な原因が判明しました。

https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3918

systemdの依存関係による循環待機(デッドロック)が原因でした

フリーズしたタイミングで実際にsystemctl list-jobsの結果を確認してみます。

			
			systemctl list-jobs
JOB  UNIT                                 TYPE  STATE
199  cloud-init.target                    start waiting
225  systemd-update-utmp-runlevel.service start waiting
250  update-motd.service                  start waiting
110  multi-user.target                    start waiting
109  graphical.target                     start waiting
1307 gitlab-runsvdir.service              start waiting
214  cloud-final.service                  start running

		

multi-user.targetwaitingとなり終了していないことがわかります。ではなぜ終了していないのか原因を探ります。cloud-final.servicecloud-init.targetなどユーザーデータ関係のjobが怪しそうです。

まずはSTATE: runningとなっているcloud-final.serviceがどのサービスに依存されているかを以下のコマンドで確認しました。

			
			systemctl list-dependencies --reverse cloud-final.service
cloud-final.service
○ ├─update-motd.service
○ └─cloud-init.target
○   └─multi-user.target
○     └─graphical.target

		

この結果からmulti-user.targetcloud-init.targetを通じてcloud-final.serviceの完了を待機していることが確認できました。つまり以下の依存関係で循環依存が発生していました。

循環依存の流れ

  • cloud-final.service(ユーザーデータ) → gitlab-ctl reconfigureを実行中
  • gitlab-ctl reconfiguresystemctl start gitlab-runsvdirを実行
  • gitlab-runsvdir.servicemulti-user.targetの完了を待機(After=multi-user.targetの設定にて)
  • multi-user.targetcloud-init.targetcloud-final.serviceの完了を待機
  • cloud-final.servicegitlab-ctl reconfigureの完了を待機

この循環依存関係により、永続的なデッドロック状態が発生していることが調査の結果確認できました。

上記issueを閲覧して要約すると、どうやらNFS対応のために GitLab サービスをmulti-user.target以降のタイミングにずらしたアップデートが、CloudFormation ベースの自動プロビジョニング環境では逆に依存ループを引き起こした」 ということが言及されていました。

手動インストールでは問題が発生しない理由
EC2内に入って行う手動インストールでは、システム起動が完了してmulti-user.targetに到達した後に作業するため、この循環待機は発生しません。

対策

原因はsystemdのターゲット依存関係にあるので、Gitlabの設定ファイルで指定ターゲット設定を変更してあげることで対応が可能です。

以下のようにユーザーデータを編集しました。

正常なユーザーデータスクリプト
			
			#!/bin/bash
set -e

# システムアップデートと基本依存関係のインストール
dnf update -y
dnf install -y policycoreutils-python-utils openssh-server openssh-clients perl wget

# GitLab公式リポジトリを追加
curl "https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh" | bash

# GitLab CEをインストール
dnf install -y gitlab-ce

# GitLab設定ファイルに追記
cat >> /etc/gitlab/gitlab.rb << EOF

# GitLab基本設定
external_url 'http://${DOMAIN_NAME}'
nginx['listen_port'] = 80
nginx['listen_https'] = false

# AWS EC2 UserData環境でのsystemdターゲット競合を回避
package['systemd_after'] = 'basic.target'
package['systemd_wanted_by'] = 'basic.target'
EOF

# GitLab設定を適用
gitlab-ctl reconfigure

# GitLabの初期rootパスワードを取得してSecrets Managerに保存
if [ -f "/etc/gitlab/initial_root_password" ]; then
  ROOT_PASSWORD=$(grep '^Password:' /etc/gitlab/initial_root_password | sed 's/^Password: //')
  if [ -n "$ROOT_PASSWORD" ]; then
    aws secretsmanager put-secret-value --secret-id "${SECRET_ARN}" --secret-string "{\"username\":\"root\",\"password\":\"$ROOT_PASSWORD\"}" --region "ap-northeast-1"
  fi
fi


		

変更点として/etc/gitlab/gitlab.rb設定ファイルの中身に以下の記述を追記しました。

			
			# AWS EC2 UserData環境でのsystemdターゲット競合を回避
package['systemd_after'] = 'basic.target'
package['systemd_wanted_by'] = 'basic.target'

		

先ほどの依存ツリーを見るとbasic.targetmulti-user.targetツリー内にあり、より早い段階で完了するsystemdターゲットです。GitLabサービスの起動条件をbasic.targetに変更することで
cloud-final.service実行時には既にbasic.targetが完了済みとなっていることが先ほどのツリーから判明しているので、GitLabサービスがすぐに起動可能になります。よってmulti-user.targetの完了を待つ必要がなくなるため、循環待機が発生しないようになります。

まとめると、より早い段階で完了するターゲットを指定してGitlabの起動設定を行うことで、ユーザーデータ実行中でもGitLabサービスが正常に起動できるようになるということです。

動作確認

上記のユーザーデータで起動したところ、インストールがスムーズに進み「gitlab-runsvdir」の起動が確認できました。

以下のように独自ドメイン経由でサインイン画面が表示され、Secrets Managerにルートパスワードも保存されており正しくサインイン可能なことが確認できました。

2025-09-24-cdk-self-managed-gitlab-ce-demo-02

2025-09-24-cdk-self-managed-gitlab-ce-demo-03

最後に

今回はCDKデプロイを通じて、独自ドメインで稼働するセルフマネージドGitlab環境を作成する際にハマったことを上げて原因を切り分けながら、解決策までまとめてみました。
ユーザーデータ内でシステムを設定する際には、実行タイミングや他システムとの起動状況との競合が原因でハマる可能性があることを深く実感できました。このブログが誰かの参考になれば幸いです。今回は以上です。

この記事をシェアする

FacebookHatena blogX

関連記事