TECHSTEP

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

GitLab CI/CDでAmazon ECSサービスを更新する

今回はGitLab CI/CDでコンテナイメージを作成し、作成したイメージを使ってAmazon ECSのサービスを更新する例を紹介します。

背景

GitLab CI/CDからAmazon ECSサービスを更新するには、大きく2つの方法があります。

  • .gitlab-ci.ymlスクリプトを記載する
  • include:template を利用する

GitLabドキュメントでは include:template の利用例が紹介されていますが、今回は1つ目のほうで実施しました。具体的には以下の処理をGitLab CI/CDで実施します。

  • build-image
    • コンテナイメージのビルド
    • Amazon ECRへのイメージ配置
  • deploy-task-definition
    • task definition定義ファイルの書き換え
    • タスク定義の更新
    • サービスの更新

検証

ここから検証です。今回も使用したのはGitLab SaaS版 (Freeプラン) です。

AWSリソースの作成

まずはCI/CDで利用するリソースを作成します。リソースの作成はAWS CloudFormationで行いました。

ecr.yaml

AWSTemplateFormatVersion: "2010-09-09"
Description: ECR for GitLab CI/CD example
Parameters:
  PJPrefix:
    Type: String
    Default: "gitlab-cicd-example"

Resources:
  ECR:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Sub "${PJPrefix}-ecr"
      EncryptionConfiguration:
        EncryptionType: "KMS"
      ImageScanningConfiguration: 
        ScanOnPush: true  
      ImageTagMutability: IMMUTABLE
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-ecr"

Outputs:
  ECR:
    Value: !Ref ECR
    Export:
      Name: !Sub "${PJPrefix}-ecr"

vpc.yaml

AWSTemplateFormatVersion: "2010-09-09"
Description: VPC and Subnet for GitLab CI/CD example

Parameters:
  PJPrefix:
    Type: String
    Default: "gitlab-cicd-example"
  VPCCIDR:
    Type: String
    Default: "10.1.0.0/16"
  PublicSubnetACIDR:
    Type: String
    Default: "10.1.10.0/24"
  PublicSubnetCCIDR:
    Type: String
    Default: "10.1.20.0/24"
  PrivateSubnetACIDR:
    Type: String
    Default: "10.1.100.0/24"
  PrivateSubnetCCIDR:
    Type: String
    Default: "10.1.200.0/24"

Resources: 
# VPC
  VPC: 
    Type: "AWS::EC2::VPC"
    Properties: 
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-vpc"

# Internet Gateway
  InternetGateway: 
    Type: "AWS::EC2::InternetGateway"
    Properties: 
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-igw"

  InternetGatewayAttachment: 
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties: 
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC 

# NAT Gateway
  NATGatewayA: 
    Type: "AWS::EC2::NatGateway"
    Properties: 
      AllocationId: !GetAtt NATGatewayAEIP.AllocationId 
      SubnetId: !Ref PublicSubnetA
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-natgw-a"

  NATGatewayAEIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: vpc
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-natgw-a"

  NATGatewayC:
    Type: "AWS::EC2::NatGateway"
    Properties:
      AllocationId: !GetAtt NATGatewayCEIP.AllocationId 
      SubnetId: !Ref PublicSubnetC
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-natgw-c"

  NATGatewayCEIP:
    Type: "AWS::EC2::EIP"
    Properties:
      Domain: vpc
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-natgw-c"
      
# Subnet
  PublicSubnetA: 
    Type: "AWS::EC2::Subnet"
    Properties: 
      AvailabilityZone: "ap-northeast-1a"
      CidrBlock: !Ref PublicSubnetACIDR
      VpcId: !Ref VPC 
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-public-subnet-a"

  PublicSubnetC: 
    Type: "AWS::EC2::Subnet"
    Properties: 
      AvailabilityZone: "ap-northeast-1c"
      CidrBlock: !Ref PublicSubnetCCIDR
      VpcId: !Ref VPC 
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-public-subnet-c"
                    
  PrivateSubnetA: 
    Type: "AWS::EC2::Subnet"
    Properties: 
      AvailabilityZone: "ap-northeast-1a"
      CidrBlock: !Ref PrivateSubnetACIDR
      VpcId: !Ref VPC 
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-private-subnet-a"

  PrivateSubnetC: 
    Type: "AWS::EC2::Subnet"
    Properties: 
      AvailabilityZone: "ap-northeast-1c"
      CidrBlock: !Ref PrivateSubnetCCIDR
      VpcId: !Ref VPC 
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-private-subnet-c"
                         
# RouteTable
  PublicRouteTableA: 
    Type: "AWS::EC2::RouteTable"
    Properties: 
      VpcId: !Ref VPC 
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-public-route-a"
          
  PublicRouteTableC: 
    Type: "AWS::EC2::RouteTable"
    Properties: 
      VpcId: !Ref VPC 
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-public-route-c"

  PrivateRouteTableA: 
    Type: "AWS::EC2::RouteTable"
    Properties: 
      VpcId: !Ref VPC 
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-private-route-a"

  PrivateRouteTableC: 
    Type: "AWS::EC2::RouteTable"
    Properties: 
      VpcId: !Ref VPC 
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-private-route-c"

  PublicRouteA: 
    Type: "AWS::EC2::Route"
    Properties: 
      RouteTableId: !Ref PublicRouteTableA 
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway 

  PublicRouteC: 
    Type: "AWS::EC2::Route"
    Properties: 
      RouteTableId: !Ref PublicRouteTableC 
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway 

  PrivateRouteA: 
    Type: "AWS::EC2::Route"
    Properties: 
      RouteTableId: !Ref PrivateRouteTableA
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NATGatewayA

  PrivateRouteC:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId: !Ref PrivateRouteTableC
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NATGatewayC

  PublicSubnetARouteTableAssociation: 
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties: 
      SubnetId: !Ref PublicSubnetA 
      RouteTableId: !Ref PublicRouteTableA

  PublicSubnetCRouteTableAssociation: 
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties: 
      SubnetId: !Ref PublicSubnetC 
      RouteTableId: !Ref PublicRouteTableC
                
  PrivateSubnetARouteTableAssociation: 
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties: 
      SubnetId: !Ref PrivateSubnetA
      RouteTableId: !Ref PrivateRouteTableA 

  PrivateSubnetCRouteTableAssociation: 
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties: 
      SubnetId: !Ref PrivateSubnetC
      RouteTableId: !Ref PrivateRouteTableC
             
Outputs:
  VPC:
    Value: !Ref VPC
    Export:
      Name: !Sub "${PJPrefix}-vpc"

  PublicSubnetA:
    Value: !Ref PublicSubnetA
    Export:
      Name: !Sub "${PJPrefix}-public-subnet-a"

  PublicSubnetC:
    Value: !Ref PublicSubnetC
    Export:
      Name: !Sub "${PJPrefix}-public-subnet-c"

  PrivateSubnetA:
    Value: !Ref PrivateSubnetA
    Export:
      Name: !Sub "${PJPrefix}-private-subnet-a"

  PrivateSubnetC:
    Value: !Ref PrivateSubnetC
    Export:
      Name: !Sub "${PJPrefix}-private-subnet-c"

iam.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: OIDC for GitLab CI/CD example
Parameters:
  RepositoryPath:
    Type: String
  PJPrefix:
    Type: String
    Default: "gitlab-cicd-example"
Resources:
  OIDCProviderForGitLab:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: https://gitlab.com
      ClientIdList:
        - https://gitlab.com
      ThumbprintList:
        - ffffffffffffffffffffffffffffffffffffffff
  RoleForGitLab:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PJPrefix}-role"
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRoleWithWebIdentity
            Principal:
              Federated: !Sub 'arn:aws:iam::${AWS::AccountId}:oidc-provider/gitlab.com'
            Effect: Allow
            Condition:
              ForAnyValue:StringLike:
                "gitlab.com:sub":
                  - !Sub "project_path:${RepositoryPath}:ref_type:branch:ref:*"
      Policies:
        - PolicyName: !Sub "${PJPrefix}-policy"
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - sts:GetCallerIdentity
                Resource:
                  - '*'
              - Effect: Allow
                Action:
                  - ecr:GetAuthorizationToken
                Resource: '*'
              - Effect: Allow
                Action: 
                  - ecr:UploadLayerPart
                  - ecr:PutImage
                  - ecr:InitiateLayerUpload
                  - ecr:CompleteLayerUpload
                  - ecr:BatchCheckLayerAvailability
                Resource: !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${PJPrefix}-ecr
              - Effect: Allow
                Action:
                  - ecs:RegisterTaskDefinition
                Resource: '*'
              - Effect: Allow
                Action:
                  - ecs:UpdateServicePrimaryTaskSet
                  - ecs:DescribeServices
                  - ecs:UpdateService
                Resource: !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:service/${PJPrefix}-cluster/${PJPrefix}-service
              - Effect: Allow
                Action:
                  - iam:PassRole
                Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/${PJPrefix}-task-execution-role
                Condition:
                  StringLike:
                    iam:PassedToService: ecs-tasks.amazonaws.com

ecs.yaml

AWSTemplateFormatVersion: "2010-09-09"
Description: ECS and ALB resources for GitLab CI/CD example
Parameters:
  PJPrefix:
    Type: String
    Default: "gitlab-cicd-example"
  ALBSecurityGroupIngressIPAddress:
    Type: String
  InternetALBName:
    Type: String
    Default: "alb"
  TargetGroupName:
    Type: String
    Default: "tg"
  ECSClusterName:
    Type: String
    Default: "cluster"
  ECSTaskName:
    Type: String
    Default: "task"
  ECSTaskCPUUnit:
    AllowedValues: [ 256, 512, 1024, 2048, 4096 ]
    Type: String
    Default: "256"
  ECSTaskMemory:
    AllowedValues: [ 256, 512, 1024, 2048, 4096 ]
    Type: String
    Default: "512"
  ECSContainerName:
    Type: String
    Default: "container"
  ECSImageName:
    Type: String
    Default: "<AWS Account ID>.dkr.ecr.ap-northeast-1.amazonaws.com/gitlab-cicd-example-ecr:0.1"
  ECSServiceName:
    Type: String
    Default: "service"
  ECSTaskDesiredCount:
    Type: Number
    Default: 1

Resources:
#Security Group
  ALBSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" }
      GroupName: !Sub "${PJPrefix}-alb-sg"
      GroupDescription: "-"
      Tags:
        - Key: "Name"
          Value: !Sub "${PJPrefix}-alb-sg"
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref ALBSecurityGroupIngressIPAddress

        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref ALBSecurityGroupIngressIPAddress

  ECSSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" }
      GroupName: !Sub "${PJPrefix}-ecs-sg"
      GroupDescription: "-"
      Tags:
        - Key: "Name"
          Value: !Sub "${PJPrefix}-ecs-sg"

# Security Group Rule
  ECSSecurityGroupIngress: 
    Type: "AWS::EC2::SecurityGroupIngress"
    Properties: 
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80
      SourceSecurityGroupId: !GetAtt [ ALBSecurityGroup, GroupId ] 
      GroupId: !GetAtt [ ECSSecurityGroup, GroupId ]

# Target Group
  TargetGroup: 
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties: 
      VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" } 
      Name: !Sub "${PJPrefix}-${TargetGroupName}"
      Protocol: HTTP
      Port: 80
      TargetType: ip

# ALB
  InternetALB: 
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties: 
      Name: !Sub "${PJPrefix}-${InternetALBName}"
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-${InternetALBName}"
      Scheme: "internet-facing"
      LoadBalancerAttributes: 
        - Key: "deletion_protection.enabled"
          Value: false
        - Key: "idle_timeout.timeout_seconds"
          Value: 60
      SecurityGroups:
        - !Ref ALBSecurityGroup
      Subnets: 
        - { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-a" }
        - { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-c" }

  ALBListener: 
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties: 
      DefaultActions: 
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref InternetALB
      Port: 80
      Protocol: HTTP

# ECS Cluster
  ECSCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: !Sub "${PJPrefix}-${ECSClusterName}"

# Loggroup
  ECSLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/ecs/logs/${PJPrefix}-ecs-group"

# ECS Task Definition
  ECSTaskDefinition:
    Type: "AWS::ECS::TaskDefinition"
    Properties:
      Cpu: !Ref ECSTaskCPUUnit
      ExecutionRoleArn: !Ref ECSTaskExecutionRole
      Family: !Sub "${PJPrefix}-${ECSTaskName}"
      Memory: !Ref ECSTaskMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        - Name: !Sub "${PJPrefix}-${ECSContainerName}"
          Image: !Ref ECSImageName
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref ECSLogGroup
              awslogs-region: !Ref "AWS::Region"
              awslogs-stream-prefix: !Ref PJPrefix
          MemoryReservation: 128
          PortMappings:
            - HostPort: 80
              Protocol: tcp
              ContainerPort: 80

# ECS Service
  ECSService:
    Type: AWS::ECS::Service
    DependsOn: ALBListener
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: !Ref ECSTaskDesiredCount
      LaunchType: FARGATE
      LoadBalancers:
        -
          TargetGroupArn: !Ref TargetGroup
          ContainerPort: 80
          ContainerName: !Sub "${PJPrefix}-${ECSContainerName}"
      NetworkConfiguration:
        AwsvpcConfiguration:
           AssignPublicIp: DISABLED
           SecurityGroups:
             - !Ref ECSSecurityGroup
           Subnets:
             - { "Fn::ImportValue": !Sub "${PJPrefix}-private-subnet-a" }
             - { "Fn::ImportValue": !Sub "${PJPrefix}-private-subnet-c" }
      ServiceName: !Sub "${PJPrefix}-${ECSServiceName}"
      TaskDefinition: !Ref ECSTaskDefinition

  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PJPrefix}-task-execution-role"
      Path: /
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

Outputs:
  ALBDNSName:
    Value: !GetAtt InternetALB.DNSName
    Export:
      Name: !Sub "${PJPrefix}-${InternetALBName}-dnsname"

VPC/ECR/IAMの作成

上記ファイルを使用してVPC/ECR/IAMを作成します。

IAMについてだけ補足すると、ここではOIDCを使ってGitLabからAWSへのアクセス権限を付与しています。

docs.gitlab.com

Dockerイメージの作成

続いてDockerイメージを作成します。作成には以下のファイルを使用しました。

FROM nginx:latest
COPY ./src/index.html /usr/share/nginx/html/index.html
<html><body>Hello GitLab CI/CD</body></html>

ここではAWS CloudShell上でDockerイメージをビルドし、作成したAmazon ECRにプッシュしました。

aws.amazon.com

ECSの作成

次に上記ファイルを使用してECSを作成します。

リソース作成後、ALB URLからアクセスすると、以下のようにテキストが表示されます。

GitLabの設定

続いてGitLab側を設定します。今回は ecs-example というGitLab Projectを使用します。

CI/CD環境変数の設定

まずは .gitlab-ci.yml上で呼び出す環境変数を設定します。ここでは以下の変数を設定します。なお設定時は Masked Expanded をチェックし、Jobログ中には変数情報をマスクし、 $ で呼び出せるようにしておきます。

  • AWS_DEFAULT_REGION: ap-northeast-1
  • AWS_ECR_LOGIN_URL: <AWS Account ID>.dkr.ecr.ap-northeast-1.amazonaws.com
  • AWS_ECR_REPOSITORY: <AWS Account ID>.dkr.ecr.ap-northeast-1.amazonaws.com/gitlab-cicd-example-ecr
  • AWS_ECS_CLUSTER_NAME: gitlab-cicd-example-cluster
  • AWS_ECS_SERVICE_NAME: gitlab-cicd-example-service
  • AWS_IAM_ROLE: arn:aws:iam::<AWS Account ID>:role/gitlab-cicd-example-role

docs.gitlab.com

ファイルの配置

続いてGitLab Projectにファイルを配置します。ファイルは以下のように配置します。

.
├── .gitlab-ci.yml
├── Dockerfile
├── README.md
├── cfn
│   ├── ecr.yaml
│   ├── ecs.yaml
│   ├── iam.yaml
│   └── vpc.yaml
├── src
│   └── index.html
└── task-definition.json

.gitlab-ci.yml task-definition.json は以下のような内容です。なお今回は .gitlab-ci.yml にruleを設定していないので、ファイルを配置した時からCI/CDパイプラインが実行されます。

.gitlab-ci.yml

variables:
  AWS_DEFAULT_REGION: $AWS_DEFAULT_REGION
  AWS_ECR_LOGIN_URL: $AWS_ECR_LOGIN_URL
  AWS_ECR_REPOSITORY: $AWS_ECR_REPOSITORY
  AWS_ECS_CLUSTER_NAME: $AWS_ECS_CLUSTER_NAME
  AWS_ECS_SERVICE_NAME: $AWS_ECS_SERVICE_NAME
  AWS_IAM_ROLE: $AWS_IAM_ROLE
  DOCKER_TLS_CERTDIR: "/certs"
stages:
  - build
  - deploy
default:
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com
  before_script:
    - apk add --no-cache aws-cli
    - >
      export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
      $(aws sts assume-role-with-web-identity
      --role-arn ${AWS_IAM_ROLE}
      --role-session-name "GitLabRunner-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
      --web-identity-token ${GITLAB_OIDC_TOKEN}
      --duration-seconds 3600
      --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
      --output text))
    - IMAGE_TAG=$CI_COMMIT_SHORT_SHA
build-image:
  stage: build
  script:
    - docker build -t $AWS_ECR_REPOSITORY:$IMAGE_TAG .
    - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ECR_LOGIN_URL
    - docker push $AWS_ECR_REPOSITORY:$IMAGE_TAG
deploy-task-definition:
  stage: deploy
  script: 
    - sed -i -e s/SED_TARGET_IMAGE_TAG/$IMAGE_TAG/g task-definition.json
    - TASK_DEF_ARN=$(aws ecs register-task-definition --cli-input-json file://task-definition.json --query 'taskDefinition.taskDefinitionArn' --output text)
    - aws ecs update-service --cluster $AWS_ECS_CLUSTER_NAME --service $AWS_ECS_SERVICE_NAME --task-definition $TASK_DEF_ARN

task-definition.json

{
    "containerDefinitions": [
        {
            "name": "gitlab-cicd-example-container",
            "image": "<AWS Account ID>.dkr.ecr.ap-northeast-1.amazonaws.com/gitlab-cicd-example-ecr:SED_TARGET_IMAGE_TAG",
            "cpu": 0,
            "memoryReservation": 128,
            "portMappings": [
                {
                    "containerPort": 80,
                    "hostPort": 80,
                    "protocol": "tcp"
                }
            ],
            "essential": true,
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/logs/gitlab-cicd-example-ecs-group",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "gitlab-cicd-example"
                }
            }
        }
    ],
    "family": "gitlab-cicd-example-task",
    "executionRoleArn": "arn:aws:iam::<AWS Account ID>:role/gitlab-cicd-example-task-execution-role",
    "networkMode": "awsvpc",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "256",
    "memory": "512"
}

少しだけ補足します。

  • .gitlab-ci.yml:
    • default: default には各Jobで共通に利用する処理を記載します。今回はJobごとにOIDCを使ってAWSにアクセスする準備を行うため、その処理を記載しました。
    • build-image: このJobではコンテナイメージの作成とAmazon ECRへのイメージ配置を行います。
    • deploy-task-definition: このJobでは task-definition.jsoncontainerDefinitions[].image 部分を書き換えたうえで、Amazon ECSのタスク定義の登録、サービスの更新を行います。

※参考:

ここまででCI/CDを動かす準備は完了です。

GitLab CI/CDの検証

ここからGitLab CI/CDの検証をします。

まずはProject中のファイルを修正します。ここではindex.htmlを修正しました。

修正後、CI/CDパイプラインの実行を確認し、しばらくすると完了します。実行されたパイプラインを確認すると、 build-image deploy-task-definition のJobが実行されているのを確認できます。

Amazon ECSの画面を見ると、タスク・サービスが更新される様子も確認できます。しばらくたつと、問題なければ起動していることを確認できます。

最後にAmazon ELBのURLにアクセスすると、修正した内容のテキストが表示されているのを確認できます。

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を確認すると、以下のようにコンテナが格納されているのを確認できます。