September 30, 2020

GitLabとAWS CodePipelineを使ってAWS ECS/FargateのCI/CD

こんにちは。

本記事では、GitLabとAWS ECS/FargateのCI/CD環境の構築する方法を紹介します。

今回の記事を書く動機は、GitLabの目玉機能であるGitLab CI/CDとAWSのCode4兄弟と言われているAWS CodeCommit, AWS CodePipeline, AWS CodeDeployAWS CodeBuildの協調がいまいち最初は分からなかったからです。これからGitLabとAWSのCode4兄弟でCI/CDをしていく人の参考になれば幸いです。

注意

用語について

本記事では、GitHubではなくGitLabを使用しているため用語がGitHubの場合と異なる場合があります(例: GitHubのPull Request = GitLabのMerge Request)。 GitLabにまだ馴染みがない方は、適宜、読み替えてください。

作成したCI/CDのシステム図

以下のシステムを構築しました。

GitLabに関する情報がどうしても少ないため、よくある採用パターンなのかは分かりませんがあまり違和感のない実装になっていると思っています。

詳細

細かく解説していこうと思います。まず、全体のPipelineの定義は以下のようになっています。 ソースを指定するstage、ビルドを指定するstageとデプロイを指定するstageの3つがあります。

resource "aws_codepipeline" "pipeline" {
  name     = "my-pipeline"
  role_arn = aws_iam_role.codepipeline.arn

  artifact_store {
    location = aws_s3_bucket.pipeline_bucket.bucket
    type     = "S3"
  }

  stage {
    name = "Source"

    action {
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeCommit"
      version          = 1
      output_artifacts = ["source"]
      configuration = {
        BranchName     = "develop"
        RepositoryName = aws_codecommit_repository.my_repository.repository_name
      }
    }
  }

  stage {
    name = "Build"

    action {
      name      = "Build"
      category  = "Build"
      owner     = "AWS"
      provider  = "CodeBuild"
      version   = "1"
      run_order = 2
      input_artifacts = [
      "source"]
      output_artifacts = [
      "build"]
      configuration = {
        ProjectName = aws_codebuild_project.my_project.name
      }
    }
  }

  stage {
    name = "Deploy"

    action {
      name            = "Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "ECS"
      version         = 1
      run_order       = 1
      input_artifacts = ["Build"]

      configuration {
        ClusterName = aws_ecs_cluster.my_clustername
        ServiceName = aws_ecs_service.my_service.name
        FileName = "${var.file_name}"
      }
    }
  }
}

① DeveloperがGitLabに変更をPushしMerge Requestを出す

② GitLab CI/CDのPipelineがトリガーされ、テストが実行される

ここでは、Go言語で書かれたアプリケーションのユニットテストを実行していたり、リントをしています。例えば、テストだと以下にパイプラインを.gitlab-ci.ymlに定義すれば簡単にテストをすることができます。

image: golang:1.15

variables:
  REPO_NAME: gitlab.com/xxxxxxx/microservice

before_script:
  - mkdir -p $GOPATH/src/$(dirname $REPO_NAME)
  - ln -svf $CI_PROJECT_DIR $GOPATH/src/$REPO_NAME
  - cd $GOPATH/src/$REPO_NAME

stages:
  - test

test:
  stage: test
  script:
    make test

GitLabプロジェクトごとに一つずつPipelineを定義しています。

③ AWS CodeCommitにミラーリングされる

GitLabのプロジェクトに対して一つのAWS CodeCommitのリポジトリを作成しています。

resource "aws_codecommit_repository" "repository" {
  repository_name = "my-repo"
}

Merge RequestをマージするとAWS CodeCommitはこのイベントを受け取って、ミラーリングをします。このミラーリングはPushを選択します。GitLabはAWS CodeCommitとのSSH接続はサポートされておらず(参考: Impossible mirroring to AWS CodeCommit, GitLab)、パスワード認証のみサポートされています。こちらはTerrafromのAWS Provider、またはGitLab Providerで設定することができないので、GUI上から設定する必要があります。これによって、設定したGitLabプロジェクトの全ての変更がAWS CodeCommitのリポジトリへミラーリングされます。

④ AWS CodeCommitの変更を検知してAWS CodeBuildをトリガーする

AWS CodeCommitの指定したブランチに変更があった場合に、AWS CodeBuildをトリガーすることができます。

⑤ AWS CodeBuildでDockerイメージがビルドされる

次に、AWS CodeCommitにミラーリングされたソースコードをもとにAWS CodeBuildでアプリケーションのDockerイメージをビルドします。そして、その生成物をAmazon ECRへプッシュします。 事前に、ECRのリポジトリを作成しておかなければなりません。

resource "aws_ecr_repository" "repository" {
  name                 = "repository"
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }
}

⑥ AWS CodeDeployがトリガーされ、AWS ECS/Fargateにデプロイされる

AWS CodeDeployがトリガーされ、AWS ECS/Fargateのタスク定義が更新され、新しいDockerイメージのアプリケーションがデプロイされます。

その他

これらの一連のフローの中で以下のようなことを行っています。

  • CloudWatch LoggingでAWS CodePipeline全体のログを収集しています
  • Amazon S3を使って生成物をやり取りしています

考えられる選択肢

この手法以外にも、いくつか選択肢はあります。感想戦的に振り返ってみようと思います。

1. GitLab CI/CDでCI/CDをやりきる

GitLab CI/CDの名前の通り、CI/CDをGitLabだけで完結できないのかを考えました。以下の3つの点から今回のGitLab CI/CDとAWSの両方を使ったシステムを採用しました。

1.1 Developerフィードバックの高速化

今回は、GitLab CI/CDはCIの部分について使っていました。それはAWS CodeBuildでテストを行ってしまうと、Developerへのフィードバックは遅くなってしまうからです。 GitLabの変更はAWS CodeBuildを直接トリガーすることができず、AWS CodeCommitのミラーリングを介してトリガーされます。そのため時間がかかりますし、その結果をもとにMergeをブロックすることなどが難しくなります。なるべくDeveloperに近い場所でテストなどを使用とするとGitLab CI/CDが最適でした。

また、AWS CodeBuildのテストレポート機能が2020年5月にGAになりました(参考: 「自動テストがより便利に!!CodeBuildのテストレポート機能がGAされました!!」, Developers.IO)。しかし、現在サポートされているテストフレームワークは以下の通りで、たとえテストレポート機能を使うと思ってもGo言語のアプリケーション開発をしているので採用するには至りませんでした。

  • JUnit
  • Ccumber
  • TestNG
  • TRX

やはりDeveloperフィードバックが遅ければ意味が薄れてしまいます。そのためにもGitLab CI/CDでユニットテストを実行したり、リントをしたりすることは良い選択だと思っています。テストレポートなどは別途、テストレポートツールなどを採用するべきだと思います。

1.2 GitLab Runnerの運用問題

GitLab CI/CD内でDockerイメージをビルドすることはできず、GitLab Runnerを用いる必要があります。

しかし、小さなチームで開発していて、インフラ周辺の運用に携わっているメンバーが少ない中で、新たにGitLab Runnerを管理するのは面倒でした。AWS CodeBuildのマネージド・サービスでビルドをしたほうが管理コストを考えると良かったです。

1.3 Registryの選択、AWS ECR or GitLab Docker Registry

GitLab Docker RegistryにDockerイメージを置く案が出ていましたが、AWS ECRへ置くようにしました。

理由としては以下の通りです。

  • 依存性のサイクル
    • 1.2で解説したように、GitLab Runnerの採用を見送ってAWS CodeBuildを採用しました。仮に、GitLab Docker Registryを採用するとGitLab → AWS CodeCommit・CodeBuild → GitLabのような流れになり、プロバイダを行ったり来たりします。
    • AWS ECS/FargateがGitLab Docker RegistryにDockerイメージを引っ張ってくるようになると、さらに問題が悪化します。クレデンシャル情報を双方に置く必要があり、良いことがないです。そのため、AWS CodeBuildを採用したのでAWS ECRを採用することは必然でした。
  • イメージスキャンニング
    • AWS ECRはイメージスキャニングをすることができます(参考: 「イメージスキャン」、AWS)。GitLabにも同様の機能はありますが、GitLab Runnerを使う必要があります(参考: 「Container Scanning」、AWS)。今回はGitLab Runnerは使わないため、採用できませんでした。
  • イメージのライフサイクル
    • AWS ECRはイメージのライフサイクルポリシーを定義してイメージのライフサイクル管理を細かく定義することができます(参考: 「ライフサイクル」、AWS)。そのため、不要になったイメージを簡単に、手間かけることなく削除することができて、コストの削減をすることができます。

2. AWS CodeCommitではなくAmazon S3を使う

GitLabはGitHubと違って、AWS CodeBuildのソースとして直接指定することはできず、今回はAWS CodeCommitを使用していました。しかし、その方法はAWS CodeCommitだけに限られず、Amazon S3に圧縮したソースコードをアップロードする方法もあります。 しかし、GitLab CI/CDの中でアップロードをしなければならないこと、S3バケットを管理しなければならないことは他の用途でもS3バケットがたくさんある中では面倒でした。そのため、ミラーリングという、簡単にソースコードをAWS側へ渡すことのできる方法を選択しました。

Amazon S3はブランチのような概念はないため、AWS CodeBuildのソースとする場合、開発フローを整えることが難しいのではないかと感じました。 チーム内のDeveloperは今まで通りGitLabを使った開発にフォーカスし続けられて、新たな学習コストを払うことなく、CI/CDを組み込むことができました。

これから

私のチームはSlackを使っているので、これらのCI/CDからSlack通知など、よりフィードバックやフィードを改善していきたいと思っています。 Developerが爆速で開発できるための基盤づくりにフォーカスしてやっていきたいです。

参考文献

© KeisukeYamashita 2020