TECHSTEP

ITインフラ関連の記事を公開してます。

GitLab CI/CDチュートリアルを実施する

今回はGitLab CI/CDを使い始めるため、GitLabの案内するチュートリアルをやってみたのでそちらの紹介です。

docs.gitlab.com

GitLab CI/CDとは

GitLabのCI/CDを理解するうえで、事前にいくつかの用語を整理しておきます。

.gitlab-ci.yml

GitLab CI/CDを利用するには、GitLab Projectのルート直下に .gitlab-ci.yml というファイルを配置する必要があります。このファイルはGitLab CI/CDの設定や処理内容を定義するファイルであり、ここに記載された内容に従ってCI/CDを実行します。なお、 .gitlab-ci.yml というファイル名は別のものでも利用可能で、その場合はCI/CDの設定からファイル名を変更して実現します。

Runner

RunnerはGitLab CI/CDを実行するエージェントです。このエージェントは物理・仮想環境どちらでも利用可能です。

.gitlab-ci.yml にはジョブを実行するのに利用するコンテナイメージを指定できます。Runnerは指定されたイメージを取得し、GitLab Projectを取得し、エージェントのローカル、またはコンテナイメージ上でジョブを実行します。

SaaSを利用する場合、ユーザーが設定することで外部のホストをRunnerとして利用することもできますが、GitLabのホストする共有Runnerを最初から利用できます。共有Runnerは、複数のOSやCPU/GPUインスタンスサイズの違うものから好きなものを利用できます。特に指定がない場合は Smallサイズ (2vCPU / 8GB)の Linux インスタンスが選択されます。なお、共有Runnerの利用できる上限は、契約しているプランによって変動します。

docs.gitlab.com

GitLab Self-managedの場合、新たにRunnerを登録するか、GitLabをホストするサーバー上にRunnerを導入することが可能です。

Pipeline

Pipelineは主に JobStage から成り立ちます。

Job には、例えば「Dockerfileからコンテナイメージをビルドする」「Pythonのアプリケーションに対して単体テストを実行する」「AWS上のEC2インスタンスにデプロイする」など、CI/CDの中で実行したいことを定義します。

Stage は1つ以上のJobを含み、いつJobを実行するかを決定します。同じStageに含まれるJobは同じタイミングで実行されます。典型的なStageは build test deploy などになります。

docs.gitlab.com

CI/CD variables

GitLab CI/CDではいくつかの環境変数が利用できます。これらの環境変数はどのJobからも呼び出し可能であり、これを利用することでJobやPipelineのふるまいを制御したり、Pipeline内で再利用したい値を一時的に保存したりすることができます。

docs.gitlab.com

CI/CD component

CI/CD componentsは再利用可能な単一のPipelineを構成するユニットです。componentsを利用することで、同一のPipelineコードを繰り返し記載することを避けることができたり、小さなPipelineを組み合わせて巨大なPipelineを構成することもできます。

docs.gitlab.com

チュートリアルの流れ

ここからはGitLabのチュートリアルに沿って .gitlab-ci.yml の作成からCI/CDの実行までを行います。

docs.gitlab.com

前提条件

チュートリアルを実施するには、いくつか前提条件があります。

  • (特に記載ありませんが) GitLab SaaSが利用可能な状態であること
  • GitLab Projectを作成済みであること
  • 操作するアカウントが、プロジェクトに対して Maintainer または Owner Roleを付与されていること

今回は saas-cicd-example というプロジェクトを作成しました。またアカウントは Owner Roleを付与しています。

Runnerの確認

まず利用可能なRunnerがあるかを確認します。

Runnerの情報を表示するには、Projectの 設定 から CI/CD を選択し、その先にある Runner の項目を確認します。

Runner という項目の 展開 をクリックすると、利用可能なRunnerが表示されます。

今回は利用可能なRunnerは78台あることが表示されています。ここで最低1台、緑色で表示されているRunnerが存在すれば、利用可能なRunnerがあることを意味します。

.gitlab-ci.ymlの作成

Runnerの存在を確認したので、次に .gitlab-ci.yml を作成します。といってもここではアプリケーションコードやリソースなどの作成は行わず、テキストメッセージをログ上に出力するだけになります。

まず、今回の .gitlab-ci.yml を以下に示します。

build-job:
  stage: build
  script:
    - echo "Hello, $GITLAB_USER_LOGIN!"

test-job1:
  stage: test
  script:
    - echo "This job tests something"

test-job2:
  stage: test
  script:
    - echo "This job tests something, but takes more time than test-job1."
    - echo "After the echo commands complete, it runs the sleep command for 20 seconds"
    - echo "which simulates a test that runs 20 seconds longer than test-job1"
    - sleep 20

deploy-prod:
  stage: deploy
  script:
    - echo "This job deploys something from the $CI_COMMIT_BRANCH branch,"
  environment: production

上記ファイルをProject上で作成し配置します。今回はGitLab UI上で新規ファイルを作成し、ファイル作成後は main ブランチに対し直接コミットします。

コミットするとPipelineが起動します。

ここで、今回の .gitlab-ci.yml について補足します。

Job

まずこのファイルを見ると、大きく以下のような構成となっていることがわかります。

<Jobの名称>:
  stage: <stageの名称>
  script: <scriptの内容>

.gitlab-ci.yml の基本的な使い方として、Job単位で処理を分けます。そして各Jobの中でStageやscriptを定義し、いつ・どんな処理を行うか定義します。

さらに一番最後の deploy-prod というJobは、stage・scriptに加えて environment という項目があります。これはJobがデプロイする先のenvironmentを定義しています。

environment はコードがデプロイされた場所を示すものです。これを利用すると、各環境に対するデプロイ履歴を管理し、どのサーバに何がデプロイされているかを知ることができます。またenvironmentはデプロイ先のURLを表示したり、使わなくなったenvironmentを簡単に削除したりもできます。

docs.gitlab.com

Stage

次に各Jobに設定された stage を見てみます。 stageとはjobを実行するタイミングを制御するものと説明しましたが、この .gitlab-ci.yml を実行したあとにパイプラインを確認すると以下のようになります。

特徴的なのは test というStageに2つのJobが含まれていることです。ここでStageに含まれる test-job1 test-job2 というJobは、どちらも stage: test と定義されています。

このように、同じStageを指定したJobは同じタイミングで実行されることがわかります。

CI/CD variables

最後に今回の .gitlab-ci.yml に含まれるCI/CD variablesについてみてみます。ここでは GITLAB_USER_LOGIN CI_COMMIT_BRANCH という2つの変数を確認できます。

これら変数は、それぞれ以下の値を提供します。

  • GITLAB_USER_LOGIN: パイプラインまたはJobを開始したユーザーの名称を含みます。
  • CI_COMMIT_BRANCH: コミットブランチの名称を含みます。

docs.gitlab.com

パイプラインの確認

パイプラインが開始したので、 ビルド から パイプライン を選択します。パイプラインが1つ実行中なのでパイプラインID (ここでは 1118402017 ) を選択します。

いくつかのJobが実行中であることが確認できます。

ここでJobを1つ選択すると、Jobの実行ログや実行期間、使用したRunner、関連するJobなどの情報が表示されます。

しばらくするとパイプラインが完了するので、最後に実行される deploy-prod を選択します。こちらも同様に実行ログなどが確認できます。

最後にパイプラインについて補足します。

ログの見方

ここでは deploy-prod を例に、Jobのログについて簡単に見ていきます。

最初にGItLab Runnerの情報が出力されます。ここでGitLabのバージョン情報、使用するRunnerの名称などがわかります。

次にRunnerの実行環境を設定します。デフォルトでは ruby:3.1 のDocker環境を利用するため、DockerイメージのPullなどを実行します。

次に environment の準備を行います。

続いてGitリポジトリからソースを取得します。ここでは git fetch –depth 20 のような内容でGitの履歴を取得します。その後最新のコミットにcheckout、リモートURLの設定などを行います。

次にJobに定義されたscriptを実行します。今回は echo で特定の文字列を出力するだけですが、その処理が行われていることも確認できます。

最後にディレクトリやファイルをクリーンアップし、ジョブの完了を表示して完了です。

environment

今回のパイプラインを実行後、 environment のページに移動すると、 production というenvironment が作成されているのを確認できます。左メニューの 操作 から 環境 を選択します。

遷移先の画面で production を確認できます。

GitLab Projectの作成方法の整理

今回はGitLabのProjectの作成方法を整理します。

背景

GtiLabの機能を利用するには、 Project というリソースを用意する必要があります。Projectの作成方法は複数あるため、ここではそれらを整理・検証します。

docs.gitlab.com

GitLab Projectを作成するには、大きく以下の4つの方法があります。

  • GitLab Webコンソールから作成
  • git push による作成
  • GitLab APIから作成
  • IaCツールから作成

検証

今回はGitLab SaaS (Freeプラン) で検証しています。

GitLab Webコンソールから作成

まずはGitLabのWebコンソールからの方法です。

docs.gitlab.com

GitLabにログイン後、 プロジェクト に移動し、 新しいプロジェクト を選択します。

ここから作成する場合は4つの選択肢があります。

  • 空のプロジェクトを作成: 何のファイルも含まないプロジェクトを作成します
  • テンプレートから作成: ビルトイン、またはインスタンスやグループで共有されたテンプレートから作成します
  • プロジェクトのインポート: GitHubや別のGitLabインスタンスからデータを移行します
  • 外部リポジトリのCI/CDを実行: GitHubなどの外部リポジトリと接続し、GitLab CI/CDを実行します

まず 空のプロジェクトを作成 を選択した場合です。ここでは以下のパラメータを設定します。

  • プロジェクト名: ここでは test-project-01 を指定
  • プロジェクトのURL: ここではnamespace (グループ or ユーザー) を指定します
  • プロジェクトslug: デフォルトではプロジェクト名が入ります
  • プロジェクトデプロイメントターゲット(オプション): KubernetesやTerraformなどのデプロイターゲットを指定します
  • 表示レベル: プライベート or 公開 を指定します
  • プロジェクトの設定
    • リポジトリを初期化しREADMEファイルを生成する
    • 静的アプリケーションセキュリティテスト(SAST)を有効にする

特に問題なければプロジェクトは作成されます。

続いて テンプレートから作成 の場合です。テンプレートから作成の場合、3つの方法があります。

  • ビルトイン: GitLabの提供するテンプレートから好きなものを選択します
  • インスタンス: インスタンス上の特定のProject上にカスタムテンプレートを用意し、インスタンス上で利用できます (Self-managed, Premiumプラン以上のみ)
  • グループ: 特定のProjectをテンプレートとして利用できます (Premiumプラン以上のみ)

ここでは利用可能な ビルトイン から適当なものを選択します。

ここで プレビュー を選択すると、作成後のプロジェクトの様子を事前に確認できます。

作成時はプロジェクト名やURLなど、 空のプロジェクトを作成 と同様の設定を指定できます。

プロジェクトを作成 を選択すると以下のような画面に遷移します。 テンプレートから作成 は、GitLabの別のProjectをインポートしてProjectを作成するため、以下のような画面が表示されます。

特に問題なければProjectが作成されます。

git pushによる作成

git push によるリポジトリの作成はGitHubでも利用可能ですが、GitLabも同様に対応しています。

ここではローカル環境にGitリポジトリを作成し、最終的にはGitLabにPushすることでプロジェクトを作成します。

$ mkdir test-project-03
$ cd test-project-03/

# 初期化
$ git init

# 適当なファイルの作成とコミット
$ echo "test-project-03" >> README.md
$ git add .
$ git commit -m "initial commit"

# git push
$ git push --set-upstream https://gitlab.com/fy0323/test-project-03.git master

Webコンソールを見ると、指定のプロジェクトの作成を確認できます。

GitLab APIから作成

GitLabは各種APIを提供しており、Project APIも提供しています。APIを利用するには以前と同様アクセストークンを利用する必要があるので、事前に用意します。

docs.gitlab.com

プロジェクトの作成は /projects に対しPOSTリクエストを実行します。

$ curl --request POST \
       --header "PRIVATE-TOKEN:<GitLabアクセストークン>" \
       --form "name=test-project-04" "https://gitlab.com/api/v4/projects/"

返り値を整形したものが以下です。

APIへのリクエストの返り値

{
    "id": 53500118,
    "description": null,
    "name": "test-project-04",
    "name_with_namespace": "Futa Yamaji / test-project-04",
    "path": "test-project-04",
    "path_with_namespace": "fy0323/test-project-04",
    "created_at": "2024-01-03T02:04:59.831Z",
    "default_branch": "main",
    "tag_list": [],
    "topics": [],
    "ssh_url_to_repo": "git@gitlab.com:fy0323/test-project-04.git",
    "http_url_to_repo": "https://gitlab.com/fy0323/test-project-04.git",
    "web_url": "https://gitlab.com/fy0323/test-project-04",
    "readme_url": null,
    "forks_count": 0,
    "avatar_url": null,
    "star_count": 0,
    "last_activity_at": "2024-01-03T02:04:59.831Z",
    "namespace": {
        "id": 5188655,
        "name": "Futa Yamaji",
        "path": "fy0323",
        "kind": "user",
        "full_path": "fy0323",
        "parent_id": null,
        "avatar_url": "https://secure.gravatar.com/avatar/f80a051bbccca2f8581c42f58223f90f?s=80&d=identicon",
        "web_url": "https://gitlab.com/fy0323"
    },
    "container_registry_image_prefix": "registry.gitlab.com/fy0323/test-project-04",
    "_links": {
        "self": "https://gitlab.com/api/v4/projects/53500118",
        "issues": "https://gitlab.com/api/v4/projects/53500118/issues",
        "merge_requests": "https://gitlab.com/api/v4/projects/53500118/merge_requests",
        "repo_branches": "https://gitlab.com/api/v4/projects/53500118/repository/branches",
        "labels": "https://gitlab.com/api/v4/projects/53500118/labels",
        "events": "https://gitlab.com/api/v4/projects/53500118/events",
        "members": "https://gitlab.com/api/v4/projects/53500118/members",
        "cluster_agents": "https://gitlab.com/api/v4/projects/53500118/cluster_agents"
    },
    "code_suggestions": true,
    "packages_enabled": true,
    "empty_repo": true,
    "archived": false,
    "visibility": "private",
    "owner": {
        "id": 3976409,
        "username": "fy0323",
        "name": "Futa Yamaji",
        "state": "active",
        "locked": false,
        "avatar_url": "https://secure.gravatar.com/avatar/f80a051bbccca2f8581c42f58223f90f?s=80&d=identicon",
        "web_url": "https://gitlab.com/fy0323"
    },
    "resolve_outdated_diff_discussions": false,
    "container_expiration_policy": {
        "cadence": "1d",
        "enabled": false,
        "keep_n": 10,
        "older_than": "90d",
        "name_regex": ".*",
        "name_regex_keep": null,
        "next_run_at": "2024-01-04T02:04:59.857Z"
    },
    "issues_enabled": true,
    "merge_requests_enabled": true,
    "wiki_enabled": true,
    "jobs_enabled": true,
    "snippets_enabled": true,
    "container_registry_enabled": true,
    "service_desk_enabled": true,
    "service_desk_address": "contact-project+fy0323-test-project-04-53500118-issue-@incoming.gitlab.com",
    "can_create_merge_request_in": true,
    "issues_access_level": "enabled",
    "repository_access_level": "enabled",
    "merge_requests_access_level": "enabled",
    "forking_access_level": "enabled",
    "wiki_access_level": "enabled",
    "builds_access_level": "enabled",
    "snippets_access_level": "enabled",
    "pages_access_level": "private",
    "analytics_access_level": "enabled",
    "container_registry_access_level": "enabled",
    "security_and_compliance_access_level": "private",
    "releases_access_level": "enabled",
    "environments_access_level": "enabled",
    "feature_flags_access_level": "enabled",
    "infrastructure_access_level": "enabled",
    "monitor_access_level": "enabled",
    "model_experiments_access_level": "enabled",
    "model_registry_access_level": "enabled",
    "emails_disabled": false,
    "emails_enabled": true,
    "shared_runners_enabled": true,
    "lfs_enabled": true,
    "creator_id": 3976409,
    "import_url": null,
    "import_type": null,
    "import_status": "none",
    "import_error": null,
    "open_issues_count": 0,
    "description_html": "",
    "updated_at": "2024-01-03T02:04:59.831Z",
    "ci_default_git_depth": 20,
    "ci_forward_deployment_enabled": true,
    "ci_forward_deployment_rollback_allowed": true,
    "ci_job_token_scope_enabled": false,
    "ci_separated_caches": true,
    "ci_allow_fork_pipelines_to_run_in_parent_project": true,
    "build_git_strategy": "fetch",
    "keep_latest_artifact": true,
    "restrict_user_defined_variables": false,
    "runners_token": "<Runner token>",
    "runner_token_expiration_interval": null,
    "group_runners_enabled": true,
    "auto_cancel_pending_pipelines": "enabled",
    "build_timeout": 3600,
    "auto_devops_enabled": false,
    "auto_devops_deploy_strategy": "continuous",
    "ci_config_path": "",
    "public_jobs": true,
    "shared_with_groups": [],
    "only_allow_merge_if_pipeline_succeeds": false,
    "allow_merge_on_skipped_pipeline": null,
    "request_access_enabled": true,
    "only_allow_merge_if_all_discussions_are_resolved": false,
    "remove_source_branch_after_merge": true,
    "printing_merge_request_link_enabled": true,
    "merge_method": "merge",
    "squash_option": "default_off",
    "enforce_auth_checks_on_uploads": true,
    "suggestion_commit_message": null,
    "merge_commit_template": null,
    "squash_commit_template": null,
    "issue_branch_template": null,
    "autoclose_referenced_issues": true,
    "external_authorization_classification_label": "",
    "requirements_enabled": false,
    "requirements_access_level": "enabled",
    "security_and_compliance_enabled": true,
    "compliance_frameworks": []
}

Webコンソールを見てもプロジェクトの作成を確認できます。

IaCツールから作成

今回は実施していませんが、GitLabのプロジェクトはAWS CloudFormation / Terraformでも管理可能です。

github.com

registry.terraform.io

GitLabに複数ユーザーを一括で追加する

今回はGitLabに複数ユーザーを一括で追加する方法について、その一例を紹介します。

背景

GitLab Self-managed版は、Web画面からユーザーを追加する場合、現状1ユーザーずつしか登録することができません。一方で、GitLabインスタンスを作成してプロジェクト等で利用する場合、複数のユーザーを一度に登録する場面にも遭遇することが予想されます。

今回は go-gitlab というGo言語のライブラリを利用して、コマンドラインから複数ユーザーを一括で登録する方法を検証しました。

github.com

検証

今回用意したコードは以下の通りです。CSVファイルの扱いやエラーハンドリングなど修正箇所は色々あるかと思いますが、最低限動作することは確認しています。

package main

import (
    "encoding/csv"
    "flag"
    "log"
    "os"

    "github.com/xanzy/go-gitlab"
)

var (
  csvfile = flag.String("csv", "test.csv", "CSV file name")
  gitlabUrl = flag.String("url", "https://gitlab.example.com/api/v4", "GitLab URL")
)


func main() {
    flag.Parse()

    file, err := os.Open(*csvfile)
    if err != nil {
        log.Fatal("File open error", err)
    }
    defer file.Close()

    r := csv.NewReader(file)
    rows, err := r.ReadAll()
    if err != nil {
        log.Fatal("File read error", err)
    }

    git, err := gitlab.NewClient(os.Getenv("GitLabToken"), gitlab.WithBaseURL(*gitlabUrl))
    if err != nil {
        log.Fatal(err)
    }

    for _, v := range rows {
        opt := &gitlab.CreateUserOptions{
            Email:         gitlab.String(v[0]),
            Name:          gitlab.String(v[1]),
            Username:      gitlab.String(v[2]),
            ResetPassword: gitlab.Bool(true),
        }
     
        user, _, err := git.Users.CreateUser(opt)
        if err != nil {
            log.Fatal("Create User Error", user, err)
        }
    }
}

※参考情報

本ツールは、ユーザー情報を入力したCSVファイルを読み取り、GitLabの users APIに繰り返しリクエストを送ることで、複数のユーザー登録を実現しています。

例えば以下のようなCSVファイルを用意します。

test01@example.com,test-name-01,test-username-01
test02@example.com,test-name-02,test-username-02
test03@example.com,test-name-03,test-username-03

Go言語はインストール済みの前提で、以下のように実行します。

$ go mod init gitlab-users
$ go get github.com/xanzy/go-gitlab
$ go build

#GitLabにアクセスするためのPersonal Access Tokenを環境変数に設定
$ export GitLabToken=<GitLab Personal Access Token>

#実行時はCSVファイルとGitLab URLを指定する

$ ./gitlab-users -csv test.csv -url <GitLab URL>/api/v4
$

処理を完了してGitLabの画面からユーザー情報を見ると、CSVに記載したユーザーが登録されているのを確認できます。

Go言語はビルドすればシングルバイナリで利用できるので、様々な環境での流用もしやすいかと思います。良ければ参考にしてみてください。

GitLabのユーザー作成方法を整理する

今回はGitLabでユーザーを作成する方法について整理します。

背景

GitLabでは複数の方法でユーザーを作成できます。

  • サインインページで作成
  • 管理画面から作成
  • 認証インテグレーション
  • APIから作成
  • Rakeコマンドラインから作成
  • IaCツールから作成

docs.gitlab.com

このうちサインインページでの作成はサービス利用開始時、認証インテグレーションはLDAP / SAMLなどと連携したときに自動的に作成される方式です。

なお、GitLab上でユーザーを作成できるのはSelf-managedのみで、SaaS版では各利用者がユーザーアカウントを作成する必要があります。ただし、SaaS版でも各Projectからユーザーを招待する形でアカウント作成ページに案内できます。

検証

今回は管理画面からの作成、APIでの作成、Rakeコマンドラインでの作成を試します。

検証はAmazon EC2上に作成したGitLabインスタンス上で行います。GitLabのバージョンは v16.7 、Enterprise Editionを利用しています。

管理画面から作成

管理画面からの作成はGitLab UI上から行います。まずは左メニューにある Admin Area にアクセスします。

続いて左メニューの Overview の中から Users を選択します。

ユーザーの一覧が表示されるので、画面右上の New User を選択します。

ユーザーの作成画面が表示されるので、 必要な設定を行います。まずは Name Username Email をそれぞれ設定します。

Password はここで設定せず、設定したメールアドレス宛にリンクが送られます。ユーザーは最初のサインインでパスワードを設定します。

続いてアクセスレベルの設定です。

  • Projects limit: ユーザーごとに作成できるプロジェクトの上限を設定します。
  • Can create top level group: 階層が最上位のグループを作成可能にします。
  • Private profile: User profileページをプライベートにし、自身とAdministrator以外が見れないようにできます。
  • Access level: 所属するGroup / Projectのみにアクセスできる Regular か、すべてのページにアクセスできる Administrator かを選択します。
  • External: Externalユーザーは、権限がない限り内部のプロジェクトを閲覧することもできず、またプロジェクトやグループなどの作成もできません。
  • Validate user account: クレジット・デビットカードの情報を入力したりAdminユーザーが検証することで、共有runnerの無料利用時間を使用できるようになります。


参考リンク


続いてProfileの設定です。アバターSkype、Xなどの情報を設定します。

最後に Admin notes の設定です。ここにはアカウントの変更や操作などの内容を記入しておき、リファレンスとして利用することが想定されています。

必要な情報を入力したら Create user を選択します。


参考リンク


特に問題がなければユーザーが作成されるのを確認できます。

APIから作成

GitLabはAPIを公開しており、アクセストークンを利用してユーザーの作成や取得などを実行できます。

docs.gitlab.com

APIから作成を行う場合、まずはアクセストークンを発行する必要があります。ここではAdminユーザーでの操作を行うため、Adminユーザーアイコンをクリックして Edit profile を選択します。

遷移後の画面左メニューから Access Tokens を選択して Add new token から新規にアクセストークンを発行しておきます。

ここでは api の権限だけあればよいのでチェックを入れます。トークン名を適当に指定し、アクセストークンを作成します。

ユーザーの作成は /users に対して以下のようなPOSTリクエストを発行します。

$ curl --request POST \
       --header "PRIVATE-TOKEN:<Access Token>" \
       --form "email=<Email>" \
       --form "name=<name>" \
       --form "username=<username>" \
       --form "password=<password>" \
  "<GitLabサーバーのURL or IPアドレス>/api/v4/users"

レスポンスはJsonで返されます。Jsonを整形したものを以下に載せておきます。

レスポンス (Json形式)

{
    "id": 4,
    "username": "test",
    "name": "test",
    "state": "active",
    "locked": false,
    "avatar_url": "https://www.gravatar.com/avatar/0ca7c89a88742de0683f903214fd61d0?s=80&d=identicon",
    "web_url": "<GitLabサーバーのURL or IPアドレス>/test",
    "created_at": "2023-12-26T03:48:20.811Z",
    "bio": "",
    "location": "",
    "public_email": null,
    "skype": "",
    "linkedin": "",
    "twitter": "",
    "discord": "",
    "website_url": "",
    "organization": "",
    "job_title": "",
    "pronouns": null,
    "bot": false,
    "work_information": null,
    "followers": 0,
    "following": 0,
    "is_followed": false,
    "local_time": null,
    "last_sign_in_at": null,
    "confirmed_at": null,
    "last_activity_on": null,
    "email": "<Email>",
    "theme_id": 3,
    "color_scheme_id": 1,
    "projects_limit": 100000,
    "current_sign_in_at": null,
    "identities": [],
    "can_create_group": true,
    "can_create_project": true,
    "two_factor_enabled": false,
    "external": false,
    "private_profile": false,
    "commit_email": "<Email>",
    "shared_runners_minutes_limit": null,
    "extra_shared_runners_minutes_limit": null,
    "scim_identities": [],
    "is_admin": false,
    "note": null,
    "namespace_id": 5,
    "created_by": {
        "id": 1,
        "username": "root",
        "name": "Administrator",
        "state": "active",
        "locked": false,
        "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
        "web_url": "<GitLabサーバーのURL or IPアドレス>/root"
    },
    "email_reset_offered_at": null,
    "using_license_seat": false
}

UIから確認してもユーザーが作成されたことを確認できます。

Railsコンソールから作成

GitLabはRuby on Railsを利用して開発されています。Rails ConsoleはGitLabに対してコマンドラインから直接操作する方法を提供するものになります。GitLabのデータを直接操作することが可能なため、利用には注意が必要です。多くは管理者がトラブルシューティングだったり直接アクセスしないと取得できない情報を確認するときなどに利用します。

docs.gitlab.com

Railsコンソールを開始するため、GitLabサーバーにログインします。ログイン後、 gitlab-rails コマンドからコンソールに入ります。

[ec2-user@ip-10-0-0-214 ~]$ sudo gitlab-rails console
--------------------------------------------------------------------------------
 Ruby:         ruby 3.1.4p223 (2023-03-30 revision 957bb7cb81) [x86_64-linux]
 GitLab:       16.7.0-ee (9e7d34f7ff1) EE
 GitLab Shell: 14.32.0
 PostgreSQL:   14.9
------------------------------------------------------------[ booted in 35.54s ]
Loading production environment (Rails 7.0.8)
irb(main):001:0>

コンソールに入ったら、以下のようなコマンドを実行します。

irb(main):001:0> u = User.new(username: 'test-rails', email: '<Email>', name: 'test-rails', password: '<password>'
)
=> #<User id: @test-rails>
irb(main):002:0> u.save!
=> true
irb(main):003:0>

UIに戻ると、指定のユーザーが作成されたのを確認できます。

指定のパスワードでログインできるのも確認できました。

Rails Consoleを利用後は quitコマンドで抜けます。

irb(main):008:0> quit
[ec2-user@ip-10-0-0-214 ~]$

IaCツールから作成

今回は実施していませんが、GitLabのユーザーはAWS CloudFormation / Terraformでも管理可能です。

github.com

registry.terraform.io

GitLab Self-managedでGmailを利用してメール送信を実現する

今回はGitLab Self-managedインスタンスからメール送信を有効にするため、Gmailを利用した例を紹介します。

GitLab Self-managedインスタンスの作成方法は前回の記事を参照してください。

背景

GitLab Self-managed版は、ユーザー作成後のパスワード更新などでメール送信を利用します。Self-managed版ではメール送信を有効にするため、SMTPサーバーの用意と機能設定が必要です。ここではGmailを利用した例を紹介します。

docs.gitlab.com

検証

Gmailの設定

最初にGmail側の設定を行います。ここではまずアプリパスワードを発行します。前提として、Googleアカウントの2段階認証が有効になっている必要があります。

Googleアカウントのページに移動し、 セキュリティ を選択します。その後の画面で 2段階認証プロセス を選択後、画面下部の アプリパスワード を選択します。

アプリパスワード画面に移動するので、適当なアプリ名を入力して 作成 をクリックします。

作成するとアプリパスワードが一度だけ表示されます。後ほど利用するので、表示されたパスワードをメモしておきます。

GitLabの設定

次にGitLab側の設定を変更します。設定は /etc/gitlab/gitlab.rb ファイルを変更することで実現します。

まずはインスタンスにログインします。変更前にバックアップの取得を行っておきます。

[ec2-user@ip-10-0-0-238 ~]$ sudo cp -p /etc/gitlab/gitlab.rb /etc/gitlab/gitlab.rb.bk

次に以下のようにファイルを修正します。設定内容はSMTPサーバーによって異なりますが、ここではGmail用の設定を行います。

[ec2-user@ip-10-0-0-238 ~]$ sudo vi /etc/gitlab/gitlab.rb

(一部抜粋)

gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.gmail.com"
gitlab_rails['smtp_port'] = 587
gitlab_rails['smtp_user_name'] = "<Googleアカウント>@gmail.com"
gitlab_rails['smtp_password'] = "<アプリパスワード>"
gitlab_rails['smtp_domain'] = "smtp.gmail.com"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['smtp_tls'] = false
gitlab_rails['smtp_openssl_verify_mode'] = 'peer'

[ec2-user@ip-10-0-0-238 ~]$

ファイルを修正後は gitlab-ctl reconfigure コマンドで変更を反映します。

[ec2-user@ip-10-0-0-238 ~]$ sudo gitlab-ctl reconfigure
[2024-01-07T00:53:48+00:00] INFO: Started Cinc Zero at chefzero://localhost:1 with repository at /opt/gitlab/embedded (One version per cookbook)
Cinc Client, version 17.10.0
Patents: https://www.chef.io/patents
Infra Phase starting

(割愛)

[2024-01-07T00:54:41+00:00] INFO: Report handlers complete
Infra Phase complete, 14/940 resources updated in 52 seconds
[2024-01-07T00:54:41+00:00] WARN: This release of Cinc Client became end of life (EOL) on May 1st 2023. Please update to a supported release to receive new features, bug fixes, and security updates.
gitlab Reconfigured!
[ec2-user@ip-10-0-0-238 ~]$

動作確認

最後に動作確認です。

まずは管理者のメールアドレスの変更を行います。 root ユーザーでログイン後、アイコンをクリックして Edit profile を選択します。

次にメールアドレスを変更します。初期は admin@example.com です。修正後は画面下部の Update profile settings を選択します。

アドレスを修正すると、指定のメールアドレス宛に以下のようなメールが送信されています。 Confirm your email address をクリックして変更を確定します。

次にユーザーの作成を行います。ここではGItLabの画面上から test-gmail-01 というユーザー名で作成しました。ユーザーの作成に成功すると、指定したメールアドレス宛にメールが送信され、パスワードの変更を要求します。

メール中の Click here to set your password を選択すると、パスワードの変更画面が表示されます。

パスワードの変更に成功すると以下のようなメールが送信されます。試しにログインしてもちゃんとログインできることを確認できます。

GitLab Self-managed環境をAmazon EC2に構築する

今回はGitLab Self-managed環境をAmazon EC2上に構築する例を紹介します。なお、ここではあくまでテスト用の環境構築を想定しているので、本番環境では使用しないでください。

背景

GitLabは大きくSaaS版とSelf-managed版の2つの形で提供しています (正確にはDedicatedという形態もあります)。このうちSelf-managed版はGitLabを動かす基盤を利用者が管理し、パッケージ等をインストールしてGitLab環境を用意する形態です。

docs.gitlab.com

GitLabの特徴として、このSelf-managed版を利用することで、自社のプライベート環境に閉じた形でバージョン管理システムを利用できる点があります。そのため検証環境を構築・利用する機会も多くなります。

今回はAmazon EC2上にGitLab Self-managed環境を構築する例を紹介します。検証環境ということですべてを有効化はしていませんが、よく使うであろうコンテナレジストリの有効化は行っています。

なお、リソースはAWS CloudFormationである程度作成していますが、一部は手動作業を含みます。

検証

今回構築した環境は以下の通りです。異なるGitLabのバージョンでは手順が異なる可能性もあるので、ご注意ください。

  • バージョン: GitLab Enterprise Edition v16.7.0-ee
  • 環境: AWS

今回は以下のような手順で環境を構築します。

AWS CloudFormationによるインスタンス等の作成

今回は以下のテンプレートを使用し、VPCなどのネットワーク、EC2インスタンスを作成しました。

gitlab-ec2.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: "EC2 instance for starting GitLab environment"
Parameters:
  ImageId:
    Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
  InstanceType:
    Default: "t3.xlarge"
    Type: String
  KeyName:
    Type: String
  VPCCidr:
    Default: "10.0.0.0/16"
    Type: String
  PublicSubnetCidr:
    Default: "10.0.0.0/24"
    Type: String
  PublicSubnetAZ:
    Default: "ap-northeast-1a"
    Type: String
  EnvName:
    Default: "GitLab-Test"
    Type: String

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Ref EnvName
  PublicSubnet:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      CidrBlock: !Ref PublicSubnetCidr
      VpcId: !Ref VPC
      AvailabilityZone: !Ref PublicSubnetAZ
      Tags:
        - Key: Name
          Value: !Ref EnvName
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Ref EnvName
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Ref EnvName
  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable
  EIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Ref EnvName
  EIPAssociation:
    Type: AWS::EC2::EIPAssociation
    Properties:
      AllocationId: !GetAtt EIP.AllocationId
      InstanceId: !Ref GitLabInstance
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EC2SecurityGroup
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvName}-EC2SecurityGroup
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: '0.0.0.0/0'
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: '0.0.0.0/0'
        - IpProtocol: tcp
          FromPort: 5050
          ToPort: 5050
          CidrIp: '0.0.0.0/0'
  EC2IAMRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${EnvName}-SSM-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - Ref: EC2IAMRole
      InstanceProfileName: !Sub ${EnvName}-EC2InstanceProfile
  GitLabInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref ImageId
      KeyName: !Ref KeyName
      InstanceType: !Ref InstanceType
      SecurityGroupIds:
        - !Ref EC2SecurityGroup
      IamInstanceProfile: !Ref EC2InstanceProfile
      SubnetId: !Ref PublicSubnet
      UserData:
        Fn::Base64: |
          #!/bin/bash
          http="http://"
          ExtraUrl=`curl ifconfig.io`
          ExtraUrl=$http$ExtraUrl
          curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.rpm.sh | sudo bash
          sudo EXTERNAL_URL=$ExtraUrl yum install -y gitlab-ee
      Tags:
        - Key: Name
          Value: !Sub ${EnvName}-EC2

作成してしばらく待ち、作成されたパブリックIPアドレスを指定するとGitLabのUIにアクセスできるようになります。

rootアカウントでログイン、パスワードの変更

ログインする前にログイン用のパスワードを取得します。上記テンプレートを使用した場合はSession Managerからログインし、以下のコマンドを実行してrootアカウント用の初期パスワードを確認します。

#ec2-userにスイッチ
$ sudo su - ec2-user

#パスワードの確認
sh-4.2$ sudo su - ec2-user
[ec2-user@ip-10-0-0-238 ~]$ sudo cat /etc/gitlab/initial_root_password
# WARNING: This value is valid only in the following conditions
#          1. If provided manually (either via `GITLAB_ROOT_PASSWORD` environment variable or via `gitlab_rails['initial_root_password']` setting in `gitlab.rb`, it was provided beforedatabase was seeded for the first time (usually, the first reconfigure run).
#          2. Password hasn't been changed manually, either via UI or via command line.
#
#          If the password shown here doesn't work, you must reset the admin password following https://docs.gitlab.com/ee/security/reset_user_password.html#reset-your-root-password.

Password: vRC36YdBkHwRX4NlIZlecLMqd/OG0UIEaLyIJO/ghd0=

# NOTE: This file will be automatically deleted in the first reconfigure run after 24 hours.
[ec2-user@ip-10-0-0-238 ~]$

上記パスワードでGitLabにログインします。

上記パスワードは24時間で破棄されるので、rootユーザーのパスワードを変更します。

パスワードの変更後は再度ログインを要求されます。

Route53でドメイン登録、Aレコード追加

次に、構築したGitLabインスタンスで利用するドメインを登録します。後続の作業でhttps化を行いますが、これにはドメイン登録が必要になります。

今回はAmazon Route53を利用していますが、ごく一般的な手順なので必要な方だけご覧ください。

Amazon Route53でのドメイン取得と登録の手順


ここではドメインを取得するところから行います。 Amazon Route53のページに移動し、 登録済みドメイン から ドメインを登録 を選択します。

ドメイン検索に利用したいドメインを検索します。表示されたものから適当なものを選択します。

連絡先情報などを登録してドメイン取得のリクエストを作成します。

AWSから登録した連絡先のメールアドレスに連絡が届くので確認します。まずはメールアドレスの確認依頼が飛ぶので、リンク先をクリックします。

しばらく待つとリクエストしたドメインが利用可能となります。

登録済みドメインにリクエストしたドメインが含まれているので、ドメインを選択します。

作成したGitLabインスタンスIPアドレスに対するAレコードを登録します。


今後の作業のため、事前に test-project という名称でProjectを作成しています。この時点ではhttps化はされていないので、URL部分は以下のような表示となります。

また、この時点ではコンテナレジストリの有効化もされていないので、コンテナレジストリの項目も表示されません。

Let’s Encryptによるhttps化

次にhttps化を行います。GitLabでは Let’s Encrypt を使用したhttpsへの設定変更が用意されています。今回は検証目的なのでこちらを利用します。

docs.gitlab.com

Let’s Encryptを利用するには、GitLabの設定を変更するだけで可能です。まずGitLabインスタンスにログインし、/etc/gitlab/gitlab.rb というファイルを修正します。このファイルはGitLabの設定情報を管理するファイルで、ここでは external_url というパラメータに利用するURL (ここでは gitlabtest.com を利用) を設定します。

# バックアップの取得
[ec2-user@ip-10-0-0-238 ~]$ sudo cp -pi /etc/gitlab/gitlab.rb /etc/gitlab/gitlab.rb.bkup

# 修正
[ec2-user@ip-10-0-0-238 ~]$ sudo vi /etc/gitlab/gitlab.rb
[ec2-user@ip-10-0-0-238 ~]$ sudo diff /etc/gitlab/gitlab.rb /etc/gitlab/gitlab.rb.bkup
32c32
< external_url 'https://gitlabtest.com'
---
> external_url 'http://<GitLabインスタンスのパブリックIPアドレス>'

/etc/gitlab/gitlab.rb に対する修正を反映するには、 gitlab-ctl reconfigure コマンドを実行します。

[ec2-user@ip-10-0-0-238 ~]$ sudo gitlab-ctl reconfigure
[2023-12-29T03:13:34+00:00] INFO: Started Cinc Zero at chefzero://localhost:1 with repository at /opt/gitlab/embedded (One version per cookbook)
Cinc Client, version 17.10.0
Patents: https://www.chef.io/patents
Infra Phase starting

(割愛)

[2023-12-29T03:15:10+00:00] INFO: Report handlers complete
Infra Phase complete, 88/1059 resources updated in 01 minutes 36 seconds
[2023-12-29T03:15:10+00:00] WARN: This release of Cinc Client became end of life (EOL) on May 1st 2023. Please update to a supported release to receive new features, bug fixes, and security updates.
gitlab Reconfigured!

更新ができていれば、WebブラウザからGitLabにアクセスしたとき、以下のようになります。

コンテナレジストリの有効化

次にコンテナレジストリの有効化を行います。コンテナレジストリの有効化も先ほどと同様 /etc/gitlab/gitlab.rb を修正します。ここでは registry_external_url というパラメータを有効にし、利用するURL (ここでは gitlabtest.com:5050 ) を設定します。

docs.gitlab.com

[ec2-user@ip-10-0-0-238 ~]$ sudo vi /etc/gitlab/gitlab.rb
[ec2-user@ip-10-0-0-157 ~]$ sudo diff /etc/gitlab/gitlab.rb /etc/gitlab/gitlab.rb.bkup
32c32
< external_url 'https://gitlabtest.com'
---
> external_url 'http://<GitLabインスタンスのパブリックIPアドレス>'
928c928
< registry_external_url 'https://gitlabtest.com:5050'
---
> # registry_external_url 'https://registry.example.com'

こちらも修正後に反映します。

[ec2-user@ip-10-0-0-238 ~]$ sudo gitlab-ctl reconfigure
[2023-12-29T03:23:13+00:00] INFO: Started Cinc Zero at chefzero://localhost:1 with repository at /opt/gitlab/embedded (One version per cookbook)
Cinc Client, version 17.10.0
Patents: https://www.chef.io/patents
Infra Phase starting

(割愛)

[2023-12-29T03:23:33+00:00] INFO: Report handlers complete
Infra Phase complete, 5/934 resources updated in 20 seconds
[2023-12-29T03:23:33+00:00] WARN: This release of Cinc Client became end of life (EOL) on May 1st 2023. Please update to a supported release to receive new features,bug fixes, and security updates.
gitlab Reconfigured!
[ec2-user@ip-10-0-0-238 ~]$

設定変更ができていれば、以下のようにコンテナレジストリが利用可能となります。

動作確認

ここから動作確認をします。今回はGitLabインスタンス上からコンテナイメージをPushし、コンテナレジストリが機能するのを確認しました。

まずはGitLabインスタンスにDockerのインストールをします。

# Dockerのインストール
[ec2-user@ip-10-0-0-238 ~]$ sudo yum update -y
[ec2-user@ip-10-0-0-238 ~]$ sudo amazon-linux-extras install -y docker
[ec2-user@ip-10-0-0-238 ~]$ amazon-linux-extras | grep docker
 20docker=latest            enabled      \

# Dockerの有効化
[ec2-user@ip-10-0-0-238 ~]$ sudo systemctl start docker
[ec2-user@ip-10-0-0-238 ~]$ systemctl status docker
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; vendor preset: disabled)
   Active: active (running) since Fri 2023-12-29 03:26:31 UTC; 5s ago
     Docs: https://docs.docker.com

(割愛)

[ec2-user@ip-10-0-0-238 ~]$ sudo systemctl enable docker
Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.

# sudo無しでdockerコマンドを実行可能にする
[ec2-user@ip-10-0-0-238 ~]$ grep docker /etc/group
docker:x:986:
[ec2-user@ip-10-0-0-238 ~]$ sudo usermod -a -G docker ec2-user
[ec2-user@ip-10-0-0-238 ~]$ grep docker /etc/group
docker:x:986:ec2-user
[ec2-user@ip-10-0-0-238 ~]$ exit
logout
sh-4.2$ sudo su - ec2-user
Last login: Fri Dec 29 03:08:21 UTC 2023 on pts/0
[ec2-user@ip-10-0-0-238 ~]$ docker version
Client:
 Version:           20.10.25
 API version:       1.41
 Go version:        go1.20.10
 Git commit:        b82b9f3
 Built:             Fri Oct 13 22:46:12 2023
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

(割愛)

[ec2-user@ip-10-0-0-238 ~]$

Dockerをインストールしたので、適当なイメージを取得し、レジストリに配置します。

# nginxイメージの取得
[ec2-user@ip-10-0-0-238 ~]$ docker pull nginx:latest

# GitLabコンテナレジストリへのログイン
# rootアカウントのIDとPWを使用してログインする
[ec2-user@ip-10-0-0-238 ~]$ docker login gitlabtest.com:5050

# コンテナレジストリへのPush
[ec2-user@ip-10-0-0-238 ~]$ docker tag nginx:latest gitlabtest.com:5050/root/test-project
[ec2-user@ip-10-0-0-238 ~]$ docker image ls
REPOSITORY                              TAG       IMAGE ID       CREATED        SIZE
nginx                                   latest    d453dd892d93   2 months ago   187MB
gitlabtest.com:5050/root/test-project   latest    d453dd892d93   2 months ago   187MB

[ec2-user@ip-10-0-0-238 ~]$ docker push gitlabtest.com:5050/root/test-project

GitLabを確認すると、以下のようにコンテナが格納されているのを確認できます。

Amazon EKSをAWS CloudFormationで作成する

今回はAmazon EKSをAWS CloudFormationで作成した例を紹介します。Web上で少し調べてもEKSをCloudFormationで作成する例があまり見当たらなかったので、今回作成してみました。

検証

作成は以下の流れで実施しました。

  • eksctlからクラスターを作成
  • CloudFormationスタックの定義をコピー、修正
  • 動作確認

eksctlAmazon EKSの作成、管理を実現するツールですが、実際にクラスターなどのリソースを管理するのはCloudFormationです。なのでeksctlが生成したテンプレートをほぼそのまま使っています。ただしネットワークリソースなどは既存のものを使う場合もあるかと思ったのでファイルを分けています。

なお、必要に応じてeksctlのアップデートを実施します。

# eksctlのアップデート
$ ARCH=amd64
$ PLATFORM=$(uname -s)_$ARCH
$ curl -sLO "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_$PLATFORM.tar.gz"
$ tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp && rm eksctl_$PLATFORM.tar.gz
$ sudo mv /tmp/eksctl /usr/local/bin
$ eksctl version
0.165.0

# eksctlからクラスターを作成
$ eksctl create cluster

(割愛)

$ eksctl get cluster
NAME                            REGION          EKSCTL CREATED
unique-mushroom-1702593766      ap-northeast-1  True

eksctlで作成されたスタックをベースに作成したテンプレートが以下になります。ここでは3つのファイルを作成しましたが、必要に応じて AWS::CloudFormation::Stack なども使って楽できると思います。

eks-nw.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: 'Network resources for EKS cluster'
Resources:
  # VPC
  VPC:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: 192.168.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/VPC'
  # Subnet
  SubnetPrivateAPNORTHEAST1A:
    Type: 'AWS::EC2::Subnet'
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: 192.168.96.0/19
      Tags:
        - Key: kubernetes.io/role/internal-elb
          Value: '1'
        - Key: Name
          Value: !Sub '${AWS::StackName}/SubnetPrivateAPNORTHEAST1A'
      VpcId: !Ref VPC
  SubnetPrivateAPNORTHEAST1C:
    Type: 'AWS::EC2::Subnet'
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: 192.168.160.0/19
      Tags:
        - Key: kubernetes.io/role/internal-elb
          Value: '1'
        - Key: Name
          Value: !Sub '${AWS::StackName}/SubnetPrivateAPNORTHEAST1C'
      VpcId: !Ref VPC
  SubnetPublicAPNORTHEAST1A:
    Type: 'AWS::EC2::Subnet'
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: 192.168.0.0/19
      MapPublicIpOnLaunch: true
      Tags:
        - Key: kubernetes.io/role/elb
          Value: '1'
        - Key: Name
          Value: !Sub '${AWS::StackName}/SubnetPublicAPNORTHEAST1A'
      VpcId: !Ref VPC
  SubnetPublicAPNORTHEAST1C:
    Type: 'AWS::EC2::Subnet'
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: 192.168.64.0/19
      MapPublicIpOnLaunch: true
      Tags:
        - Key: kubernetes.io/role/elb
          Value: '1'
        - Key: Name
          Value: !Sub '${AWS::StackName}/SubnetPublicAPNORTHEAST1C'
      VpcId: !Ref VPC
  PrivateRouteTableAPNORTHEAST1A:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/PrivateRouteTableAPNORTHEAST1A'
      VpcId: !Ref VPC
  PrivateRouteTableAPNORTHEAST1C:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/PrivateRouteTableAPNORTHEAST1C'
      VpcId: !Ref VPC
  PublicRouteTable:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/PublicRouteTable'
      VpcId: !Ref VPC
  # Route
  PublicSubnetRoute:
    Type: 'AWS::EC2::Route'
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref PublicRouteTable
    DependsOn:
      - VPCGatewayAttachment
  NATPrivateSubnetRouteAPNORTHEAST1A:
    Type: 'AWS::EC2::Route'
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway
      RouteTableId: !Ref PrivateRouteTableAPNORTHEAST1A
  NATPrivateSubnetRouteAPNORTHEAST1C:
    Type: 'AWS::EC2::Route'
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway
      RouteTableId: !Ref PrivateRouteTableAPNORTHEAST1C
  # Route Table Association
  RouteTableAssociationPrivateAPNORTHEAST1A:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PrivateRouteTableAPNORTHEAST1A
      SubnetId: !Ref SubnetPrivateAPNORTHEAST1A
  RouteTableAssociationPrivateAPNORTHEAST1C:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PrivateRouteTableAPNORTHEAST1C
      SubnetId: !Ref SubnetPrivateAPNORTHEAST1C
  RouteTableAssociationPublicAPNORTHEAST1A:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref SubnetPublicAPNORTHEAST1A
  RouteTableAssociationPublicAPNORTHEAST1C:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref SubnetPublicAPNORTHEAST1C
  # InternetGateway
  InternetGateway:
    Type: 'AWS::EC2::InternetGateway'
    Properties:
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/InternetGateway'
  VPCGatewayAttachment:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC
  # NAT Gateway
  NATGateway:
    Type: 'AWS::EC2::NatGateway'
    Properties:
      AllocationId: !GetAtt 
        - NATIP
        - AllocationId
      SubnetId: !Ref SubnetPublicAPNORTHEAST1A
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/NATGateway'
  NATIP:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/NATIP'
  # Security Group
  ClusterSharedNodeSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: Communication between all nodes in the cluster
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/ClusterSharedNodeSecurityGroup'
      VpcId: !Ref VPC
  ControlPlaneSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: Communication between the control plane and worker nodegroups
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/ControlPlaneSecurityGroup'
      VpcId: !Ref VPC
  IngressDefaultClusterToNodeSG:
    Type: 'AWS::EC2::SecurityGroupIngress'
    Properties:
      Description: Allow managed and unmanaged nodes to communicate with each other (all ports)
      FromPort: 0
      GroupId: !Ref ClusterSharedNodeSecurityGroup
      IpProtocol: '-1'
      SourceSecurityGroupId: !Ref ControlPlaneSecurityGroup
      ToPort: 65535
  IngressNodeToNode:
    Type: 'AWS::EC2::SecurityGroupIngress'
    Properties:
      Description: Allow unmanaged nodes to communicate with control plane (all ports)
      FromPort: 0
      GroupId: !Ref ControlPlaneSecurityGroup
      IpProtocol: '-1'
      SourceSecurityGroupId: !Ref ControlPlaneSecurityGroup
      ToPort: 65535
  IngressInterNodeGroupSG:
    Type: 'AWS::EC2::SecurityGroupIngress'
    Properties:
      Description: Allow nodes to communicate with each other (all ports)
      FromPort: 0
      GroupId: !Ref ClusterSharedNodeSecurityGroup
      IpProtocol: '-1'
      SourceSecurityGroupId: !Ref ClusterSharedNodeSecurityGroup
      ToPort: 65535
  IngressNodeToDefaultClusterSG:
    Type: 'AWS::EC2::SecurityGroupIngress'
    Properties:
      Description: Allow unmanaged nodes to communicate with control plane (all ports)
      FromPort: 0
      GroupId: !Ref ControlPlaneSecurityGroup
      IpProtocol: '-1'
      SourceSecurityGroupId: !Ref ClusterSharedNodeSecurityGroup
      ToPort: 65535

Outputs:
  ControlPlaneSecurityGroup:
    Value: !Ref ControlPlaneSecurityGroup
    Export:
      Name: ControlPlaneSecurityGroup
  SubnetPrivateAPNORTHEAST1A:
    Value: !Ref SubnetPrivateAPNORTHEAST1A
    Export:
      Name: SubnetPrivateAPNORTHEAST1A
  SubnetPrivateAPNORTHEAST1C:
    Value: !Ref SubnetPrivateAPNORTHEAST1C
    Export:
      Name: SubnetPrivateAPNORTHEAST1C
  SubnetPublicAPNORTHEAST1A:
    Value: !Ref SubnetPublicAPNORTHEAST1A
    Export:
      Name: SubnetPublicAPNORTHEAST1A
  SubnetPublicAPNORTHEAST1C:
    Value: !Ref SubnetPublicAPNORTHEAST1C
    Export:
      Name: SubnetPublicAPNORTHEAST1C

eks-cluster.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: 'EKS cluster'
Parameters:
  ClusterName:
    Type: String
  
Resources:
  ControlPlane:
    Type: 'AWS::EKS::Cluster'
    Properties:
      KubernetesNetworkConfig:
        IpFamily: ipv4
      Name: !Ref ClusterName
      ResourcesVpcConfig:
        EndpointPrivateAccess: false
        EndpointPublicAccess: true
        SecurityGroupIds:
          - !ImportValue ControlPlaneSecurityGroup
        SubnetIds:
          - !ImportValue SubnetPublicAPNORTHEAST1A
          - !ImportValue SubnetPublicAPNORTHEAST1C
          - !ImportValue SubnetPrivateAPNORTHEAST1C
          - !ImportValue SubnetPrivateAPNORTHEAST1A
      RoleArn: !GetAtt 
        - ServiceRole
        - Arn
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/ControlPlane'
      Version: '1.27'
  # IAM
  PolicyCloudWatchMetrics:
    Type: 'AWS::IAM::Policy'
    Properties:
      PolicyDocument:
        Statement:
          - Action:
              - 'cloudwatch:PutMetricData'
            Effect: Allow
            Resource: '*'
        Version: 2012-10-17
      PolicyName: !Sub '${AWS::StackName}-PolicyCloudWatchMetrics'
      Roles:
        - !Ref ServiceRole
  PolicyELBPermissions:
    Type: 'AWS::IAM::Policy'
    Properties:
      PolicyDocument:
        Statement:
          - Action:
              - 'ec2:DescribeAccountAttributes'
              - 'ec2:DescribeAddresses'
              - 'ec2:DescribeInternetGateways'
            Effect: Allow
            Resource: '*'
        Version: 2012-10-17
      PolicyName: !Sub '${AWS::StackName}-PolicyELBPermissions'
      Roles:
        - !Ref ServiceRole
  ServiceRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - 'sts:AssumeRole'
            Effect: Allow
            Principal:
              Service:
                - eks.amazonaws.com
        Version: 2012-10-17
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/AmazonEKSClusterPolicy'
        - 'arn:aws:iam::aws:policy/AmazonEKSVPCResourceController'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/ServiceRole'

Outputs:
  ClusterName:
    Value: !Ref ClusterName
    Export: 
      Name: ClusterName

eks-ng.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: 'EKS Managed Nodes'
Parameters:
  LaunchTemplateName:
    Type: String
  NodegroupName:
    Type: String
Resources:
  LaunchTemplate:
    Type: 'AWS::EC2::LaunchTemplate'
    Properties:
      LaunchTemplateData:
        BlockDeviceMappings:
          - DeviceName: /dev/xvda
            Ebs:
              Iops: 3000
              Throughput: 125
              VolumeSize: 80
              VolumeType: gp3
        MetadataOptions:
          HttpPutResponseHopLimit: 2
          HttpTokens: required
        SecurityGroupIds:
          - !ImportValue ControlPlaneSecurityGroup
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: Name
                Value: !Ref LaunchTemplateName
              - Key: alpha.eksctl.io/nodegroup-name
                Value: !Ref NodegroupName
              - Key: alpha.eksctl.io/nodegroup-type
                Value: managed
          - ResourceType: volume
            Tags:
              - Key: Name
                Value: !Ref LaunchTemplateName
              - Key: alpha.eksctl.io/nodegroup-name
                Value: !Ref NodegroupName
              - Key: alpha.eksctl.io/nodegroup-type
                Value: managed
          - ResourceType: network-interface
            Tags:
              - Key: Name
                Value: !Ref LaunchTemplateName
              - Key: alpha.eksctl.io/nodegroup-name
                Value: !Ref NodegroupName
              - Key: alpha.eksctl.io/nodegroup-type
                Value: managed
      LaunchTemplateName: !Ref LaunchTemplateName
  ManagedNodeGroup:
    Type: 'AWS::EKS::Nodegroup'
    Properties:
      AmiType: AL2_x86_64
      ClusterName: !ImportValue ClusterName
      InstanceTypes:
        - t3.large
      Labels:
        alpha.eksctl.io/cluster-name: !ImportValue ClusterName
        alpha.eksctl.io/nodegroup-name: !Ref NodegroupName
      LaunchTemplate:
        Id: !Ref LaunchTemplate
      NodeRole: !GetAtt 
        - NodeInstanceRole
        - Arn
      NodegroupName: !Ref NodegroupName
      ScalingConfig:
        DesiredSize: 2
        MaxSize: 5
        MinSize: 2
      Subnets:
        - !ImportValue SubnetPrivateAPNORTHEAST1C
        - !ImportValue SubnetPrivateAPNORTHEAST1A
      Tags:
        alpha.eksctl.io/nodegroup-name: !Ref NodegroupName
        alpha.eksctl.io/nodegroup-type: managed
  NodeInstanceRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - 'sts:AssumeRole'
            Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
        Version: 2012-10-17
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly'
        - 'arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy'
        - 'arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy'
        - 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore'
      Path: /
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}/NodeInstanceRole'

上記ファイルを eks-nw.yaml eks-cluster.yaml eks-ng.yaml の順に利用しリソースを作成します。なおCloudFormationスタックの作成はマネジメントコンソールからでも問題ないですが、この後Amazon EKSにアクセスするため、作成時のIAMユーザーの権限などは事前に確認が必要です。

$ aws cloudformation deploy --template-file eks-nw.yaml --stack-name eks-nw --region ap-northeast-1 --profile <profile名>

$ aws cloudformation deploy --template-file eks-cluster.yaml --stack-name eks-cluster --parameter-overrides ClusterName=eks-cluster --capabilities CAPABILITY_IAM  --region ap-northeast-1 --profile <profile名>

$ aws cloudformation deploy --template-file eks-ng.yaml --stack-name eks-ng --parameter-overrides LaunchTemplateName=eks-lt NodegroupName=eks-ng --capabilities CAPABILITY_IAM  --region ap-northeast-1 --profile <profile名>

作成が完了したら、kubeconfigを更新します。

$ aws eks update-kubeconfig --name eks-cluster --profile <profile名> --region ap-northeast-1

あとはEKSを操作できることを確認して終了です。

$ kubectl run nginx --image=nginx:latest
pod/nginx created

$ kubectl get pods -w
NAME    READY   STATUS              RESTARTS   AGE
nginx   0/1     ContainerCreating   0          5s
nginx   1/1     Running             0          8s

^C

$ kubectl get pods -A
NAMESPACE     NAME                       READY   STATUS    RESTARTS        AGE
default       nginx                      1/1     Running   0               17s
kube-system   aws-node-8lmnn             1/1     Running   0               2m25s
kube-system   aws-node-smwhm             1/1     Running   8 (6m32s ago)   21m
kube-system   coredns-8496bbc677-n9gdh   1/1     Running   0               2m25s
kube-system   coredns-8496bbc677-pvxvb   1/1     Running   0               27m
kube-system   kube-proxy-9h4rs           1/1     Running   0               21m
kube-system   kube-proxy-z7wp9           1/1     Running   0               21m

さいごに

EKSの作成はいくつか方法がありますが、人気があるのは eksctl や Terraform あたりでしょうか。

Googleトレンドで雑に調べるとこんな感じ

EKSをCloudFormationで管理するユースケースはそれほどないのかもしれませんが、AWSリソースの管理をCloudFormationに寄せたいプロジェクトの場合は役に立つかもしれません。