GitLab CI/CD のビルド環境から外部のリポジトリを取得する

この記事は、GitLabのプライベートリポジトリでGoのパッケージの管理、CI/CDでの運用を調査したものです。

目次

GitLab CI/CD のビルド環境から外部のリポジトリへのアクセス

あるgitリポジトリから、他のプライベートなgitリポジトリへアクセスする事があります。
たとえばよくあるのは、社内で開発されているライブラリの管理をしているリポジトリを参照するためにgit サブモジュールを使用している場合等でしょうか。

開発している個人のローカル環境では、各開発者のアカウントでリポジトリへアクセスできるように正しく権限を付与するだけで良いですが、CI/CDのように特定の個人に依存しないDocker等の環境からアクセスしたい場合、メンバーのアカウント情報に依存しないアクセス方法が欲しくなります。
今回は、GitLab の CI/CD 環境(Docker executor)から必要なリポジトリへ適切な権限でアクセスする方法を紹介します。
また、実例としてGo言語で実装した複数の AWS Lambda から、別のリポジトリで管理している自作のGo言語のパッケージを参照する方法も紹介します。

GitLab CI/CDの詳細についてはこちらの記事を参照してください。 blog.linkode.co.jp

Deploy Tokens と Deploy Keys

GitLab では、ユーザー名とパスワード無しでリポジトリへアクセスする手段として、トークンを発行するDeploy Tokensと、SSH Keyを使用するDeploy Keysが用意されています。
トークンを発行する方法はGitLabに依存するので、今回は特定のホスティングサービスに依存しない、汎用的に活用できるSSH Keyで設定を行います。

SSHキーのペア作成

ssh-keygenコマンドで、SSHキーのペアを作成します。
以下の例では、暗号化の方式はRSAとして、ファイル名はgitlab_mylib_rsaとしています。

$ ssh-keygen -t rsa -f gitlab_mylib_rsa

パスフレーズは空で作成しました。
コマンドを実行したフォルダー内に秘密鍵gitlab_mylib_rsaと公開鍵gitlab_mylib_rsa.pubのふたつのファイルが作成されます。

アクセスされる側(Deploy Keys)の設定

SSHキーのペアが作成出来たら、アクセスを行いたいリポジトリDeploy Keysへ、公開鍵を設定します。
ブラウザでGitLabのターゲットとなるリポジトリのページを開き、左側のメニューのSettingsから
Settings -> Repository を選択すると、表示される項目の一覧の中にDeploy Keysを見つけることができるので、Expandボタンを押して、項目の詳細を開きます。

Titleに、任意のタイトルを入力して、Keyに作成した公開鍵ファイル(gitlab_mylib_rsa.pub)の内容をそのまま貼り付けます。
デフォルトではアクセス権がRead Onlyとなっているので、リポジトリに対してPUSH等をさせたい場合はWrite access allowedへチェックを入れます。
今回は取得のみ行うので、チェックは入れていません。

アクセスする側(CI/CD)の設定

今度はアクセスする側のリポジトリで、CI/CDの環境変数秘密鍵を登録します。
Settings -> CI / CD でCI/CDの設定画面を開き、Variablesから設定を行います。

Expandボタンで詳細を開き、Add Variableボタンで変数を設定するモーダルUIが表示されます。

環境変数の名前(Key)をSSH_PRIVATE_KEYとして、値(Value)に秘密鍵gitlab_mylib_rsa)の内容を貼り付けます。
TypeVariableFlagsProtect variableMask variableのチェックは外しておきます。
これで、環境変数として秘密鍵を取得できます。

※安全のためにMask variableはチェックを入れたいところですが、制約があり、マスクすることができません。
https://docs.gitlab.com/ee/ci/variables/README.html#mask-a-custom-variable

.gitlab-ci.yml

環境変数が設定出来たら、.gitlab-ci.ymlへ次のように設定することで、Deploy Keysを設定したリポジトリへアクセスできます。

image: amazon/lambda-build-node10.x:latest
 
stages:
  - build
 
before_script:
 
  # ssh-agent を実行します。
  - eval `ssh-agent`
 
  # 秘密鍵の登録を行います。
  - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
 
  # known_hostsを格納するためのSSHのフォルダーを作成します。
  # ssh-keyscan で対象ホスト(gitlab.com)のホストキー一覧を取得してknown_hostsへ追記します。
  - mkdir -p ~/.ssh
  - ssh-keyscan gitlab.com >> ~/.ssh/known_hosts
  - chmod 644 ~/.ssh/known_hosts
 
build:
  stage: build
  script:
    # SSHプロトコルでライブラリのリポジトリをクローンすることができます。
    - git clone git@gitlab.com:my-group/my-subgroup/my-library.git

Dockerのイメージは仮で、AWS Lambdaをビルドすることを想定して、amazon/lambda-build-node10.xにしています。
また、クローンしているリポジトリも仮に git@gitlab.com:my-group/my-subgroup/my-library.git としています。
SSHに関するスクリプトbefore_scriptへ書いていますが、必要なジョブのみに記述しても問題ありません。

Go の場合

ここまでで、GitLabのCI/CDを行うDocker環境からSSHプロトコルで他のリポジトリへアクセスする方法がわかりました。
ここからは、Deploy Keysの設定を踏まえて、Go言語で作成した自作のパッケージを複数のAWS Lambdaで使用するケースを紹介します。

想定と環境

Go言語で実装しているAWS Lambda関数から、自作のパッケージを参照します。
Goのバージョンは 1.14.1 を使用して、Lambda関数も自作のパッケージもGitLabのプライベートリポジトリで管理します。
また、Goのビルドにmakeを使用し、Lambda 関数とレイヤーのデプロイに Serverless Framework を使用しています。
Serverless Framework はNode.jsで作られたCLIツールで AWS LambdaAzure FunctionsGoogle Cloud Functions等の Serverless Applicationを構成管理、デプロイするためのツールです。
各ツールのインストールや環境構築については割愛します。

import文

Goはホスティングサービス上に配置されたパッケージをimportできます。
https://golang.org/cmd/go/#hdr-Remote_import_paths

ドキュメント内に例示されていますが、GitHubだと次のようにimportします。

    import "github.com/user/project"
    import "github.com/user/project/sub/directory"

ただ、パッケージがGitLabのサブグループに配置されている場合、注意が必要です。 たとえば、パッケージがgitlab.com/group/subgroup/my-projectというリポジトリへ配置されている場合、GitHubの例のように

    import "gitlab.com/group/subgroup/my-project"
    import "gitlab.com/group/subgroup/my-project/sub/directory"

と記述してしまうと、subgroupの部分がリポジトリとみなされてしまい、ビルドエラーとなってしまいます。
これを回避するためには、リポジトリを識別するProject slugの部分に.gitを付与します。

    import "gitlab.com/group/subgroup/my-project.git"
    import "gitlab.com/group/subgroup/my-project.git/sub/directory"

これについては、GitLabへissueが挙がっていますが、現時点では.gitを付与してリポジトリを明示する形が良さそうです。
https://gitlab.com/gitlab-org/gitlab-foss/-/issues/1337
https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30785

Gitの設定(insteadOf)

Goの依存モジュールを管理するGo Modulesは、importされた対象のパッケージリポジトリHTTPSスキームのURLで取得します。
このため、取得時にSSHスキームとなるようにGitの設定を行います。

$ git config --global url."ssh://git@gitlab.com".insteadOf "https://gitlab.com"

GOPRIVATE

Go1.13からデフォルトでプロキシを経由してパッケージを取得するようになっています。
社内リポジトリなどのパブリックに取得できないパッケージの場合、環境変数GOPRIVATEを設定して、プロキシを介さず直接パッケージにアクセスできるよう設定する必要があります。
https://golang.org/ref/mod#module-proxy
https://golang.org/ref/mod#environment-variables

今回は、GitLabでホスティングしているプライベートリポジトリが対象となるため、次のように設定します。

$ export GOPRIVATE=gitlab.com/*

プロジェクトの配置とCI/CDからのデプロイ

ここまでの内容を踏まえて実際にプロジェクトを配置してみます。

ライブラリ側(アクセスされる側)

  • リポジトリ
    パスは gitlab.com/test-go-package-group/some-subgroup/my-golang-library として、Deploy Keysを設定します。

  • フォルダー構成

./
└─libhello/
        go.mod
        get_hello.go
  • libhello/go.mod
    goコマンド
    go mod init gitlab.com/test-go-package-group/some-subgroup/my-golang-library.git/libhello
    でプロジェクトを初期化しています。
module gitlab.com/test-go-package-group/some-subgroup/my-golang-library.git/libhello
 
go 1.14
  • libhello/get_hello.go
    Hello!という文字列を取得するだけのライブラリです。
package libhello
 
// GetHello return "Hello!"
func GetHello() string {
    return "Hello!"
}

AWS Lambda側(アクセスする側)

  • リポジトリ
    パスはどこでも良いのですが、gitlab.com/test-go-package-group/golang-lambda-functionsとして、CI/CDの環境変数SSH_PRIVATE_KEYを設定します。

  • フォルダー構成
    ServerlessFrameworkのコマンドsls create --template aws-goでプロジェクトのテンプレートを作成して、
    go mod init gitlab.com/test-go-package-group/golang-lambda-functions.git
    でプロジェクトを初期化します。

./
├─hello/
│      main.go
├─world/
│      main.go
├─ .gitlab-ci.yml
├─ go.mod
├─ go.sum
├─ Makefile
└─ serverless.yml
  • ソースコードhello/main.goおよびworld/main.go
    2つのLambda関数のハンドラーは、ほとんどテンプレートのままですが、ライブラリをインポートして使用するようにしています。
    2つの関数の見分けがつくように、出力の文字列をそれぞれ別のものに変更しても良いです。
package main
 
import (
    "fmt"
    "log"
 
    "github.com/aws/aws-lambda-go/lambda"
 
    // 自作のパッケージをインポート
    "gitlab.com/test-go-package-group/some-subgroup/my-golang-library.git/libhello"
)
 
func Handler() {
    // libhello のGetHello() から文字列を取得してログ出力
    hello := libhello.GetHello()
    log.Println(fmt.Sprintf("libhello Get [%s]", hello))
}
 
func main() {
    lambda.Start(Handler)
}
.PHONY: build clean deploy

export GO111MODULE=on
export GOOS=linux
export GOPRIVATE=gitlab.com/*

build:
    go build -ldflags="-s -w" -o bin/hello/app.lambda_handler hello/main.go
    go build -ldflags="-s -w" -o bin/world/app.lambda_handler world/main.go

clean:
    rm -rf ./bin

deploy: clean build
    sls deploy --verbose
  • serverless.yml
    特筆する事項はありませんが、Makefileで指定している実行バイナリの出力パスとハンドラーのパスをあわせておきます。

  • .gitlab-ci.yml
    CI/CDでLambda関数のビルドとデプロイを行います。
    SSHのセットアップは、アンカーで記述し使いまわせるようにして、ビルドにはGoのイメージを、デプロイにはServerlessFrameworkを使用したいのでNodeのイメージを指定しています。
    SSHのセットアップの最後に、SSHスキームでGitLabへアクセスするようにGitの設定(git config)も行っています。

stages:
  - build
  - deploy
 
.setup_ssh: &setup_ssh
  - eval `ssh-agent`
  - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
  - mkdir -p ~/.ssh
  - ssh-keyscan gitlab.com >> ~/.ssh/known_hosts
  - chmod 644 ~/.ssh/known_hosts
  - git config --global url."ssh://git@gitlab.com".insteadOf "https://gitlab.com"
 
build:
  image: golang:1.14
  stage: build
  script:
    - *setup_ssh
    - make
  cache:
    paths:
      - bin
 
deploy:
  image: node:14.10-alpine
  stage: deploy
  script:
    - yarn config set prefix /usr/local
    - yarn global add serverless
    - sls deploy
  cache:
    paths:
      - bin
    policy: pull

Lambda関数の動作確認

これで、Lambda関数の管理を行っているリポジトリgit pushすると、関数のビルド、デプロイが行われます。
ServerlessFrameworkのコマンド sls invoke --function hello --log で動作を確認すると、ログが
libhello Get [Hello!]
と出力されていることが確認できます。

まとめ

GitLab CI/CDのDocker executorから、同じくGitLabのプライベートリポジトリへアクセスする方法と、Goで自作したパッケージの配置と使用について一例を紹介しました。

別の記事でAWS Lambda(Go) で Lambda レイヤーを活用する方法を紹介しましたが、比較的頻繁に更新されるような小さなパッケージを複数のLambda関数で使用する場合は今回の方法が有効です。

参考資料