hero_picture
Cover Image for Bitbucket CloudのプライベートリポジトリをすべてGithubへ移行する

Bitbucket CloudのプライベートリポジトリをすべてGithubへ移行する

クラウド事業部エンジニアの川勝です。

弊社git管理は今までBitbucket Cloudを使用していましたが、この度すべてのリポジトリをGithubへ移行いたしました。

Githubのコア機能が無料で利用可能に や有料プランも見直されて値下げがあったことで社内でGithub使いたい!との声が大きくなったのがきっかけで、

そのGithub使いたい有志でチームを組み移行作業にとりかかりました。

その中で川勝は実際にリポジトリを移す作業を担当したので今回はその方法をまとめておきたいと思います。

目次

手順

  • Bitbucketからリポジトリ一覧取得
  • Githubにリポジトリを作成
  • BitbucketからクローンしてGithubにプッシュ
  • スクリプトにしてAmazon ECS のタスクで実行

Githubには他のバージョン管理システムからリポジトリを取り込む Github Importer という機能があります。

ただし、1リポジトリずつ手作業が必要(APIがあるのでプログラムから実行できますが)なのとプライベートリポジトリの場合は 上記にも記載されていますが、コマンドラインを使用したインポート が必要です。

したがってコマンドラインを使用したインポートの方法に沿って実行していきました。

Bitbucketからリポジトリ一覧取得

最初に対象のリポジトリ名を取得します。

サイトの画面から見てもいいのですが、330リポジトリほどあったのとこの際棚卸ししようとリポジトリの削除も実行したので簡単に一覧を取得を複数回できるようにAPIから取得しました。

1#!/bin/bash
2
3# get-repositories.sh
4# 実行サンプル
5# export ACCESS_TOKEN = {Bitbucketアクセストークン}
6# export ORG = {リポジトリのオーガナイザー}
7# bash get-repositories.sh > repositories.txt
8
9token=${ACCESS_TOKEN}
10org=${ORG}
11
12res=$(curl -s -H "Authorization: Bearer $token" "https://api.bitbucket.org/2.0/repositories/${ORG}")
13echo $res | tr -d '[:cntrl:]' | jq -r '.values[] | .name'
14next=$(echo $res | tr -d '[:cntrl:]' | jq -r '.next')
15
16while [ "$next" != "$null" ]
17do
18    res=$(curl -s -H "Authorization: Bearer $token" "$next")
19    echo $res | tr -d '[:cntrl:]' | jq -r '.values[] | .name'
20    next=$(echo $res | tr -d '[:cntrl:]' | jq -r '.next')
21done

上記スクリプトを実行するまえにBitbucketからアクセストークンを取得しておく必要があります。

ドキュメント を参考に取得しておきます。

これで一覧がとれるので次はGithubへのリポジトリ作成方法です。

Githubにリポジトリ作成

リポジトリの作成は数が多いのでGithub APIから作成しました。

APIの操作ライブラリは google/go-github を使用しています。

1package githubapi
2
3import (
4	"context"
5	"sync"
6
7	"github.com/google/go-github/github"
8	"golang.org/x/oauth2"
9)
10
11// Github type GithubClient.
12type Github struct {
13	client *github.Client
14}
15
16// NewGithub new github client.
17func NewGithub(token string) *Github {
18	return &Github{
19		client: getClient(token),
20	}
21}
22
23var client *github.Client
24var once sync.Once
25
26func getClient(token string) *github.Client {
27	once.Do(func() {
28		ts := oauth2.StaticTokenSource(
29			&oauth2.Token{AccessToken: token},
30		)
31		ctx := context.Background()
32		tc := oauth2.NewClient(ctx, ts)
33		client = github.NewClient(tc)
34	})
35	return client
36}
37
38// CreateRepo create repository.
39func (c *Github) CreateRepo(org, name string) error {
40	ctx := context.Background()
41	isPrivate := true
42	repo := &github.Repository{
43		Name:    &name,
44		Private: &isPrivate,
45	}
46	_, _, err := c.client.Repositories.Create(ctx, org, repo)
47	if err != nil {
48		return err
49	}
50	return nil
51}

CreateRepo が実際にAPIからリポジトリを作成している箇所です。

そしてGithubもまたまた事前にアクセストークンを作成しておく必要がありますので作っておきましょう。

getClientの引数で渡しているものですね。

アクセストークンの作り方

リポジトリはできたので次はソースのクローン、プッシュを行います。

BitbucketからクローンしてGithubにプッシュ

コマンドラインから以下の手順で移行ができます。

Bitbucketからクローン

1git clone --bare git@bitbucket.org:{オーガナイザー}/{リポジトリ}

Githubにpush

1git push --mirror git@github.com:{オーガナイザー}/{リポジトリ}

通常は上記でOKですが、git lfs を使用している場合は以下も追加で行います。

git lfsからfetchしてpush

1# git cloneしたディクレクトリで
2git lfs fetch --all
3git lfs push --all git@github.com:{オーガナイザー}/{リポジトリ}

これらのコマンドをプログラムから実行できるようにします。

1package git
2
3import (
4	"bytes"
5	"fmt"
6	"os/exec"
7)
8
9const (
10	bitbucketURL    = "git@bitbucket.org:%s/%s.git"
11	pushCommand     = "cd %s && git push --mirror git@github.com:%s/%s.git"
12	lfsFetchCommnad = "cd %s && git lfs fetch --all"
13	lfsPushCommand  = "cd %s && git lfs push --all git@github.com:%s/%s.git"
14)
15
16// Clone clone --bare
17func Clone(org, repo, tmp string) error {
18	url := fmt.Sprintf(bitbucketURL, org, repo)
19	cmd := exec.Command("git", "clone", "--bare", url, tmp)
20	return run(cmd)
21}
22
23// Push push --mirror
24func Push(org, repo, tmp string) error {
25	pushCmd := fmt.Sprintf(pushCommand, tmp, org, repo)
26	cmd := exec.Command("bash", "-c", pushCmd)
27	return run(cmd)
28}
29
30// LfsFetch lfs fetch --all
31func LfsFetch(tmp string) error {
32	fetchCmd := fmt.Sprintf(lfsFetchCommnad, tmp)
33	cmd := exec.Command("bash", "-c", fetchCmd)
34	return run(cmd)
35}
36
37// LfsPush lfs push --all
38func LfsPush(org, repo, tmp string) error {
39	pushCmd := fmt.Sprintf(lfsPushCommand, tmp, org, repo)
40	cmd := exec.Command("bash", "-c", pushCmd)
41	return run(cmd)
42}
43
44func run(cmd *exec.Cmd) error {
45	var out bytes.Buffer
46	var stderr bytes.Buffer
47	cmd.Stdout = &out
48	cmd.Stderr = &stderr
49	if err := cmd.Run(); err != nil {
50		fmt.Println(stderr.String())
51		return err
52	}
53	return nil
54}

あとはこれらを呼び出して実行したら完成です!

1package action
2
3import (
4	"fmt"
5	"os"
6	"path/filepath"
7	"sync"
8
9	"github.com/seeds-std/migration-from-bitbucket/git"
10	"github.com/seeds-std/migration-from-bitbucket/githubapi"
11	"github.com/seeds-std/migration-from-bitbucket/slackwebhook"
12)
13
14// One exec one repositry.
15func One(c *githubapi.Github, org, repo string, haveLfs bool) error {
16	fmt.Printf("create repository: %s\n", repo)
17	if err := c.CreateRepo(org, repo); err != nil {
18		return fmt.Errorf("error: do not create repository. %+v", err)
19	}
20	tmp := filepath.Join("/tmp", repo)
21	if err := os.Mkdir(tmp, 0777); err != nil {
22		return fmt.Errorf("error: do not create tmp directory. %+v", err)
23	}
24	defer os.RemoveAll(tmp)
25
26	fmt.Printf("clone repository: %s\n", repo)
27	if err := git.Clone(org, repo, tmp); err != nil {
28		return fmt.Errorf("error: do not clone repository. %+v", err)
29	}
30
31	fmt.Printf("push repository: %s\n", repo)
32	if err := git.Push(org, repo, tmp); err != nil {
33		return fmt.Errorf("error: do not push repository. %+v", err)
34	}
35
36	if haveLfs {
37		fmt.Printf("lfs fetch repository: %s\n", repo)
38		if err := git.LfsFetch(tmp); err != nil {
39			return fmt.Errorf("error: do not lfs fetch repository. %+v", err)
40		}
41
42		fmt.Printf("lfs push repository: %s\n", repo)
43		if err := git.LfsPush(org, repo, tmp); err != nil {
44			return fmt.Errorf("error: do not lfs push repository. %+v", err)
45		}
46	}
47
48	return nil
49}
50
51// All exec all repositories.
52func All(
53	c *githubapi.Github,
54	org string,
55	target []string, // Bitbucketから取得したリポジトリ一覧
56	lfs map[string]bool, // lfsを使用しているリポジトリ
57	ignore map[string]bool, // テストで移行済みのものがあるので、処理しないリポジトリ
58	webhook *slackwebhook.SlackWebhook,
59) {
60	limit := make(chan struct{}, 5)
61	var wg sync.WaitGroup
62	for _, repo := range target {
63		wg.Add(1)
64		go func(repo string) {
65			limit <- struct{}{}
66			defer wg.Done()
67			if _, ok := ignore[repo]; ok {
68				fmt.Printf("skip ignore repository: %s\n", repo)
69				<-limit
70				return
71			}
72			_, haveLfs := lfs[repo]
73			if err := One(c, org, repo, haveLfs); err != nil {
74				fmt.Printf("%s\n", err.Error())
75				webhook.NotifyError(org, repo, haveLfs, err)
76				<-limit
77				return
78			}
79			webhook.NotifySuccess(org, repo, haveLfs)
80			<-limit
81		}(repo)
82	}
83	wg.Wait()
84}
85

Oneで1つずつリポジトリを移行、Allで受け取った一覧からループして実行しています。

ちなみにAllの引数にリポジトリ名の一覧がありますが、今回は一度しか実行しないので予めslice or mapにハードコードしておいて渡してたりします。

これをmainでaction.Allを実行する形にしておきます。

Amazon ECS のタスク

さて最後に実行方法ですが、ローカルのPCでやるのは時間かかるし、git クローンとpushでネットワーク回線を圧迫しそうだったのでAWSのサービスを使用したいと考えました。

  • AWS Lambda
  • AWS Batch
  • Amazon ECS

AWS Lambda

料金的に抑えられそう。

最大実行時間が15分なので全リポジトリを一回の実行では不可能だが、

ただ1リポジトリごとの実行であれば並列で一気に実行できそう…?

と考えましたが、git cloneするには /tmp に展開する必要がありますがAWS Lambdaでは /tmp は500MBまで、という制限がありました。

git lfs使っているくらい巨大なリポジトリもあるので無理だな、、、ということで没。

AWS Batch

バッチ処理といえばAWS Batchがあるな、と設定を始めました。

AWS BatchはAmazon EC2 インスタンスを立ち上げないといけないのですが、

スペックどうしようかな、、というのとどうも無料枠で使えるインスタンスタイプがなさそうだぞ、、、ということで途中でやめてしまいました。

今回のような1つのスクリプト実行するだけにはちょっと設定が多い感じですね。

Amazon ECS

Amazon ECS で AWS Fargateを使用すればインスタンスの立ち上げとか考える必要がない、dockerを起動したら実行されるとだけしておけばよい、、、で、最終的にこちらにしました。

Amazon ECSで実行するにはDockerコンテナが必要です。(AWS Batchもですが)

Dockerfileをつくりましょう。

1FROM golang:1.14.4-alpine3.12
2
3RUN apk add --no-cache \
4    git \
5    git-lfs \
6    make \
7    bash \
8    gcc \
9    libc-dev \
10    openssl \
11    curl \
12    openssh
13
14WORKDIR /go/src/migration-from-bitbucket
15COPY . .
16
17RUN go get -d -v ./...
18RUN go install -v ./...
19
20RUN mkdir -p /root/.ssh && cp -r ./id_rsa ~/.ssh/id_rsa && chmod 600 /root/.ssh/id_rsa
21RUN echo "StrictHostKeyChecking no" >> /root/.ssh/config
22
23CMD ["migration-from-bitbucket", "all"]

migration-from-bitbucket all

と実行すればOKなようになっています。

gitのプライベートリポジトリの操作が必要なので、SSH秘密鍵を配置しています。BitbucketとGithubを同じ鍵で操作できるようにしています。

これでdocker build できたらAmazon ECR にdocker pushしてAmazon ECSタスクから使えるようしたら準備完了です。

Amazon ECRの作成は割愛しますが、起動タイプをFargateにしてタスクを実行するだけになります。

以下記事が参考になると思います。

CircleCI + GitHub + Amazon Elastic Container Registry (Amazon ECR) + Amazon Elastic Container Service (Amazon ECS) (+ AWS Fargate) で継続的デリバリー環境を構成する

コンテナのvCPUとかmemoryは最低にしていましたが、実行したら2時間弱くらいで完了しました。

(ちなみに終わってからGithubだと1ファイルの容量上限が100MBなようで、

該当するリポジトリはエラーになってたのですが… 大容量ファイルの制限

まとめ

調べているとBitbucketからGithubへ移行したという記事は結構みかけました。

そのなかで全てのプライベートリポジトリを一気に移行した、ということで今回記事にまとめてみました。

実際この記事執筆時点ではリポジトリは移したけど各人は絶賛作業中なので、

弊社としての使いがってがどうなったかなどはまだこれからという感じですが

個人的には良好です。

CI機能のGithub actionsをこれから使っていきたいのでまたそちらも記事にしたいと思います。

以上、川勝でした。