GitLabとAWS CodePipelineでScalaのCI環境を作る


先日、AWSのCodePipelineというサービスを使ってCI環境をしてみました。
必要な設定が思いの外多かったので備忘も兼ねて構築方法を共有します。

CodePipelineについて

AWSには以下の開発者向けの3つのサービスがあります。

  • CodeCommit gitのホスティング
  • CodeBuild ビルドサービス
  • CodeDeploy デプロイサービス

CodePipelineはこれらのサービスを組み合わせたワークフローを管理するサービスです。

CIの構成

以下の図の様に、GitLabにpushしたソースコードをAWSのCodePipeline側でビルドして最終的にECSにデプロイするCIを作ります。

構築の手順

次の流れで構築していきますが、ECSなどが作成済の環境にCIを作る場合は適宜読み飛ばしてください。
ちなみに、Scalaならではの対応が必要なのは1.と8.の手順のみです。

  1. Scalaのプロジェクトの作成
  2. CodeCommitの作成
  3. GitLabでのコードのミラーリングの設定
  4. ECRの作成
  5. ECSの作成
  6. (オプション)ローカルからのデプロイ
  7. CodePipelineの作成
  8. 設定ファイルの作成
  9. 実行

Scalaプロジェクトの作成

今回は例としてPlay Frameworkで作成したWebアプリケーションをデプロイします。

まず、Dockerイメージを作るためのプラグインを追加します。

plugins.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.5.2")

build.sbtのプロジェクト名とバージョンが、Dockerイメージの名前とタグになるため適宜修正します。
また、dockerのビルド用の設定も追加しておきます。

build.sbt
name := """scala-ci-sample"""
organization := "com.example"
version := "1.0.0"

lazy val root = (project in file("."))
  .enablePlugins(PlayScala)
  .settings(
    maintainer in Docker := "[email protected]",
    dockerBaseImage := "openjdk:8-jdk-alpine",
    dockerCommands ++= Seq(
      Cmd("USER", "root"),
      Cmd("RUN", "apk --update add bash")
    ),
    dockerExposedPorts ++= Seq(9000),
    daemonUserUid in Docker := None,
    daemonUser in Docker := "daemon"
  )

CIとは直接関係ありませんが、application.confにデプロイする際に必要な設定も追加しておきます。
本番で利用される場合は適切な値を設定してください。

application.conf
play.server.pidfile.path=/dev/null
play.filters.hosts.allowed = ["."]
play.http.secret.key="change_me"
play.http.secret.key=${?APPLICATION_SECRET}

各種設定が終わったら、ローカルでDockerイメージが作成できるか確認します。

$ sbt docker:publishLocal
$ docker images
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
scala-ci-sample          1.0.0               246b9833441a        About a minute ago  149MB

問題なくDockerイメージが作成できる事を確認したら、ソースコードはGitLabにpushしておきます。

CodeCommitの作成

リポジトリの作成

AWS側でソースコードを管理するCodeCommitのリポジトリを作成します。

AWSコンソールの「リポジトリを作成」ボタンから作成する事ができます。

今回はリポジトリ名をscala-ci-sample-repoにします。

作成できたら「HTTPSのクローン」からリポジトリのURLを取得しておきます。このURLはGitLabでミラーリングの設定をする際に使用します。(※1)

「設定」で確認できるリポジトリのARNも控えておきます。このARNはリポジトリへのミラーリング用の権限を作成する際に使用します。(※2)

権限の設定

リポジトリを作っただけでは外部からアクセスできないため、権限の設定を行います。
まずはIAMでポリシーを作成します。

CodeCommitのPullとPushの権限を与えます。
リソースにはリポジトリのARN(※2)を指定します。

次にポリシーをアタッチしたユーザーを作成します。

これだけではまだCodeCommitにhttpで直接アクセスする権限を持たないので、認証情報のタブから権限を追加します。

認証情報を作成のボタンを押すと認証情報が作成され、csvファイルとしてダウンロードできます。この認証情報はGitLab側でのミラーリングの設定に使用します。(※3)

GitLabでのコードのミラーリングの設定

GitLabにコードがコミットされた際に、AWSのCodeCommitに転送するミラーリングの設定を行います。

Settings -> Repository の Mirroring repositories で設定を行います。

設定内容は以下の通りです。
Git repository URL: https://@<リポジトリのURL(※1)>
Mirror direction: Push
Authentication method: Password
Password: CodeCommitの認証情報(※3)のPassword

設定できたらゴミ箱の左のボタンを押すと正しく接続できるか確認できます。

CodeCommit側でもソースコードが表示できれば、ミラーリングの設定は完了です。

ECRの作成

ECRのリポジトリを作成します。

作成後にリポジトリのURIを控えておきます。(※4)

ECSの作成

詳細は割愛しますが、ECSのクラスター、タスク、サービス、ロードバランサーなどを作成します。
タスクを作成する際にコンテナを追加しておきます。イメージの場所にECR(※4)を指定します。まだイメージが登録されていないので仮のタグを指定しておきます。

(オプション) 手動でのデプロイの確認

ここまでで、デプロイ先の環境ができたので、ローカルで作ったDockerイメージをECSにデプロイしてみます。
この手順はスキップできますが、初回のデプロイはセキュリティーグループや環境変数の設定が間違っている事がよくあるので一旦手動でデプロイしてうまく動くか確認するのがおすすめです。

ECRへは以下のコマンドでイメージをpushできます。

(aws ecr get-login --no-include-email --region ap-northeast-1)
# 上記のコマンドの実行後に表示されたログインコマンドをコピー&ペーストで実行する
docker tag scala-ci-sample:1.0.0 <アカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/scala-ci-sample-ecr:1.0.0
docker push <アカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/scala-ci-sample-ecr:1.0.0

push後にECSのタスクとサービスを更新して問題なく動く事を確認します。

CodePipelineの作成

CodePipelineの「パイプラインを作成する」から作成します。
必要事項を入力して順次進んでいくと、ソースステージ、ビルドステージ、デプロイステージが作られます。

ソースステージの作成

CodeCommitのリポジトリとビルド対象のブランチを指定します。

ビルドステージの作成

「プロジェクトを作成する」からビルドプロジェクトを作ります。

CodeBuildの作成

ビルドプロジェクトに適当な名前をつけます。

環境ではマネージド型イメージを選択して、OSはAmazon Linuxを選択します。
今回はマネージド型イメージを選択しましたが、sbtなどがインストール済のコンテナを作ってそちらを利用するようにするとビルド時間が短縮できるかと思います。

今回はBuildspecを使用してビルドの定義を行います。
buildspec.ymlはCircleCIで言うところのconfig.ymlに相当するファイルです。ファイルの詳細については後述します。

ビルド時にECRにアクセスできる権限を付与する必要があるので、ロール名を控えておきます。(※5)

作成後にビルドステージに戻ってプロジェクトとして設定します。

デプロイステージの作成

デプロイステージではデプロイ対象のECSのクラスターとサービスを指定します。

ポリシーの設定

今回はCodeBuild内でイメージを作成してECRにpushする必要があるため、
IAMからCodeBuildが使用するポリシー(※5)にECRにアクセスする権限を付与します。
追加する必要のある権限は以下の通りです。

    {
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:CompleteLayerUpload",
        "ecr:GetAuthorizationToken",
        "ecr:InitiateLayerUpload",
        "ecr:PutImage",
        "ecr:UploadLayerPart"
      ],
      "Resource": "*",
      "Effect": "Allow"
    },

設定ファイルの作成

ソースコードのルートディレクトリにbuildspec.ymlを追加します。
installではbuildの環境の指定、pre_buiuldではsbtのインストールなどのビルドの準備、buildでは実際のビルド、post_buildでは次のデプロイステージのための準備を定義します。

buildspec.yml
version: 0.2

phases:
  install:
    runtime-versions:
      docker: 18
      java: corretto8
  pre_build:
    commands:
      - echo commit hash ${CODEBUILD_RESOLVED_SOURCE_VERSION}
      - $(aws ecr get-login --no-include-email --region ap-northeast-1)
      - yum -y install sbt
  build:
    commands:
      - echo Build started
      - sbt docker:publishLocal
  post_build:
    commands:
      - echo publish ECR
      - docker tag scala-ci-sample:1.0.0 <アカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/scala-ci-sample-ecr:${CODEBUILD_RESOLVED_SOURCE_VERSION}
      - docker push <アカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/scala-ci-sample-ecr:${CODEBUILD_RESOLVED_SOURCE_VERSION}
      - printf '[{"name":"scala-ci-sample-container","imageUri":"%s"}]' <アカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/scala-ci-sample-ecr:${CODEBUILD_RESOLVED_SOURCE_VERSION} > imagedefinitions.json
artifacts:
  files: imagedefinitions.json

pre_buiuldやbuildは特に説明は不要だと思いますが、post_buildで何を行っているか解説します。
まず、以下の部分ではビルド結果のイメージにタグ付してECRにpushしています。タグはbuild.sbtで指定されたバージョンではなくコミットハッシュを使用するようにしています。
CloudBuildではコミットハッシュを表す環境変数の CODEBUILD_RESOLVED_SOURCE_VERSION の他にもいくつかの環境変数が用意されています。
参考: https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-env-vars.html

- docker tag scala-ci-sample:1.0.0 <アカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/scala-ci-sample-ecr:${CODEBUILD_RESOLVED_SOURCE_VERSION}
- docker push <アカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/scala-ci-sample-ecr:${CODEBUILD_RESOLVED_SOURCE_VERSION}

以下の部分ではビルドステージでECSへデプロイを行う際にデプロイ対象のコンテナ名とイメージのURLを記載した imagedefinitions.json というファイルが必要なのですが、今回はビルドの度にタグが変更されるため、ビルドステージ内で動的に作成しています。

- printf '[{"name":"scala-ci-sample-container","imageUri":"%s"}]' <アカウント>.dkr.ecr.ap-northeast-1.amazonaws.com/scala-ci-sample-ecr:${CODEBUILD_RESOLVED_SOURCE_VERSION} > imagedefinitions.json

実行

上記で作成したファイルをGitLabにpushすると以降はmasterブランチに変更が入る度にCIが実行されるようになります。

参考

https://dev.classmethod.jp/cloud/aws/gitlab-codecommit-mirroring/
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-spec-ref.html
https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/file-reference.html