AWS CodePipelineがSourceとしてAmazon ECRを指定できるようななりました! #reinvent

はじめに

おはようございます、加藤です。「re: Invent組のブログが飛び交っていますね!」

AWS CodePipeline(以降、CodePipeline)がAmazon ECR(以降、ECR)というDockerコンテナレジストリへの更新をデータソースとして使用できるようになりました!

例えば、2つのリポジトリを運用していたとします。

  • node.jsがインストールされたbase-image
  • base-imageに作成したAppを追加したhello-world

base-imageはDockerfileなどへの変更(CodeCommitへのPush)を検知して、CodePipelineを実行する事ができます。

hello-worldもapp.jsなどへの変更を検知して、CodePipelineを実行する事ができます。

hello-worldはbase-imageに依存しているので、base-imageに変更があった場合もCodePipelineを実行してビルドを行う必要がありますね。つまりECRへのPushをCodePipelineのソースにしたいということです。今回のアップデートで可能になったのはこの部分です!!

やってみた

図のような構成を作成してみます。下記のブログを参考にやってみました。

Build a Continuous Delivery Pipeline for Your Container Images with Amazon ECR as Source | AWS DevOps Blog

CodeBuildサービスロールの作成

CodeBuildが使用するロールを作成します、AWS CLIで行いました。まず2つのJSONを用意します。

create-role.json

{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Service": "codebuild.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
      }
    ]
  }

put-role-policy.json

{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "CloudWatchLogsPolicy",
        "Effect": "Allow",
        "Action": [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ],
        "Resource": [
          "*"
        ]
      },
      {
        "Sid": "CodeCommitPolicy",
        "Effect": "Allow",
        "Action": [
          "codecommit:GitPull"
        ],
        "Resource": [
          "*"
        ]
      },
      {
        "Sid": "S3GetObjectPolicy",
        "Effect": "Allow",
        "Action": [
          "s3:GetObject",
          "s3:GetObjectVersion"
        ],
        "Resource": [
          "*"
        ]
      },
      {
        "Sid": "S3PutObjectPolicy",
        "Effect": "Allow",
        "Action": [
          "s3:PutObject"
        ],
        "Resource": [
          "*"
        ]
      }
    ]
  }

JSONが存在するディレクトリで下記のコマンドを実行します。

aws iam create-role --role-name CodeBuildServiceRole --assume-role-policy-document file://create-role.json
aws iam put-role-policy --role-name CodeBuildServiceRole --policy-name CodeBuildServiceRolePolicy --policy-document file://put-role-policy.json
aws iam attach-role-policy --role-name CodeBuildServiceRole --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser

Baseイメージ作成環境

ECRの作成

node.jsがインストールされたDockerコンテナを格納するリポジトリを作成します。

aws ecr create-repository --repository-name base-image

CodeCommitの作成

Baseイメージ用のCodeCommitを作成します。

mkdir base-image && cd base-image
export BASE_IMAGE_CLONE_URL_HTTP=$(aws codecommit create-repository --repository-name base-image | jq -r '.repositoryMetadata.cloneUrlHttp')
git init
git config credential.helper '!aws codecommit credential-helper $@'
git config  credential.UseHttpPath true
git remote add origin ${BASE_IMAGE_CLONE_URL_HTTP}

ファイルの作成

Dockerfileを作成します。node.jsをインストールするだけです。

Dockerfile

FROM alpine:3.8
RUN apk add nodejs --update --no-cache 

buildspec.ymlを作成します。REPOSITORY_URIのAWSアカウント番号は環境に合わせて変更してください。

buildspec.yml

version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws --version
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - REPOSITORY_URI=012345678910.dkr.ecr.ap-northeast-1.amazonaws.com/base-image
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $REPOSITORY_URI:latest .
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker images...
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG

作成したファイルをリポジトリにPushします。

git add .
git commit -m '初回コミット'
git push -u origin master

CodePipelineの作成

パイプライン名はbase-image、サービスロールが存在しない場合は新規作成してください。

作成したCodeCommitをソースとして使用します。

ビルドにはCodeBuildを使用します。まだ作成していないので、Create projectから作成します。

プロジェクト名はbase-imageにします。

AWSが用意してくれているマネージド型イメージを使用します。

Key Value
OS Ubuntu
ランタイム Docker
ランタイムバージョン aws/codebuild/docker:17.09/0
イメージのバージョン aws/codebuild/docker:17.09.0-1.5.0
サービスロール CodeBuildServiceRole
(最初に作成したロールです)

CodePipelineの画面に戻ってきました。

デプロイステージは不要なのでスキップします。

最終確認でOKすると、パイプラインが走ります。

ECRを確認するとイメージが格納されていました!

App(Hello World)イメージ作成環境

ECRの作成

アプリケーションが組み込まれたDockerコンテナを格納するリポジトリを作成します。

aws ecr create-repository --repository-name hello-world

CodeCommitの作成

Appイメージ用のCodeCommitを作成します。

mkdir hello-world && cd hello-world
export HELLO_WORLD_CLONE_URL_HTTP=$(aws codecommit create-repository --repository-name hello-world | jq -r '.repositoryMetadata.cloneUrlHttp')
git init
git config credential.helper '!aws codecommit credential-helper $@'
git config  credential.UseHttpPath true
git remote add origin ${HELLO_WORLD_CLONE_URL_HTTP}

ファイルの作成

Dockerfileを作成します。app.jsをコピーするだけです。

FROMのAWSアカウント番号は環境に合わせて変更してください。

Dockerfile

FROM 012345678910.dkr.ecr.ap-northeast-1.amazonaws.com/base-image
ENV PORT=80
EXPOSE $PORT
COPY app.js /app/
CMD ["node", "/app/app.js"]

app.jsを作成します。Hello Worldと表示するだけです、ポートは3000で受け付けます。

app.js

var http = require('http');

http.createServer(function (request, response) {
  response.writeHead(200, {'Content-Type': 'text/plain'});
  response.end('Hello World\n');
}).listen(3000);

buildspec.ymlを作成します。REPOSITORY_URIのAWSアカウント番号は環境に合わせて変更してください。

buildspec.yml

version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws --version
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - REPOSITORY_URI=012345678901.dkr.ecr.ap-northeast-1.amazonaws.com/hello-world
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=build-$(echo $CODEBUILD_BUILD_ID | awk -F":" '{print $2}')
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $REPOSITORY_URI:latest .
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker images...
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - echo Writing image detail file...
      - printf '[{"name":"hello-world","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
artifacts:
  files:
    - 'imageDetail.json'

作成したファイルをリポジトリにPushします。

git add .
git commit -m '初回コミット'
git push -u origin master

CodePipelineの作成

パイプライン名はhello-world、サービスロールが存在しない場合は新規作成してください。

base-imageのリポジトリをソースとして使用します。

base-imageの時と同様にCodeBuildを作成します。

CodePipelineの画面に戻ってきました。

デプロイステージは不要なのでスキップします。

SourceがECRしか無いため、失敗します。

SourceにCodeCommitを追加します。

出力アーティファクトをSourceAtrifactと設定してください。

このままだと、Source内で出力アーティファクトが重複してしまうので、ECRの方の出力アーティファクトをBaseImageに変更します。

変更のリリースボタンを押下して、CodePipelineを起動します。

成功しました!!

動作テスト

base-imageにインストールするパッケージを増やして動作を確認してみます。rubyを追加しました。

Dockerfile(base-image)

FROM alpine:3.8
RUN apk add nodejs ruby --update --no-cache 

CodeCommitにPushします。

git add .
git commit -m 'rubyのインストールを追加'
git push -u origin master

無事に完了しました!base-imageのCodePipeline完了後に自動でhello-worldのCodePipelineが起動しています。

Appのコードは変わっていないでの、Commitメッセージは変化なしです。

次にApp側もテストしてみます。無駄に!を足してみました。

app.js

var http = require('http');

http.createServer(function (request, response) {
  response.writeHead(200, {'Content-Type': 'text/plain'});
  response.end('Hello World!!!!!!!!!!!\n');
}).listen(3000);

CodeCommitにPushします。

git add .
git commit -m '!を足した'
git push -u origin master

今度はhello-worldのCodePipelineだけが動きました!

あとがき

今回のようなシンプルな例では、意味がないですがBaseイメージへインストールするパッケージが増えたり設定変更なども行っている場合だとビルドの高速化が期待できそうですね!