クラウド事業部エンジニアの川勝です。
弊社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にしてタスクを実行するだけになります。
以下記事が参考になると思います。
コンテナのvCPUとかmemoryは最低にしていましたが、実行したら2時間弱くらいで完了しました。
(ちなみに終わってからGithubだと1ファイルの容量上限が100MBなようで、
該当するリポジトリはエラーになってたのですが… 大容量ファイルの制限)
まとめ
調べているとBitbucketからGithubへ移行したという記事は結構みかけました。
そのなかで全てのプライベートリポジトリを一気に移行した、ということで今回記事にまとめてみました。
実際この記事執筆時点ではリポジトリは移したけど各人は絶賛作業中なので、
弊社としての使いがってがどうなったかなどはまだこれからという感じですが
個人的には良好です。
CI機能のGithub actionsをこれから使っていきたいのでまたそちらも記事にしたいと思います。
以上、川勝でした。