Fargateでタスク実行する with CodePipeline + CodeBuild

Fargateが魅力的だったので今更ながらDockerを仕事にも取り入れるようになってきました。Fargateは既存のECSのようなクラスタインスタンスが不要なのでLambdaの延長のような形でコストや運用をあまり気にせずに使えるのが良いですね。特にLambdaが苦手な長時間の処理であったりVPCの中でRDSを操作するような処理はこれからはFargateに任せたいところです。

今回やったこと

CIによるDockerイメージのビルド含めECSのFargate環境を動かしてみます。

  1. CodePipelineでGitHubのプライベートリポジトリからソースを取得する
  2. CodeBuildでDockerイメージを作成しECRにpushする
  3. ECRのイメージを利用してFargateのタスク定義を作成する
  4. AWS SDKからFargateのタスクを実行する

Fargateに対応しているVirginia(us-east-1)のRegionを使います。

今回CodePipelineとCodeBuildも初めて利用しました。触ってみると案外シンプルで使いやすくできていました。ちなみに今回やりたかったことはGitHubのソースからDockerイメージを作成するだけなのでCodeBuildだけでも出来ます。

Dockerfileを用意する

あまりDockerfileを書いてこなかったのでベストプラクティス等は分かりませんが下記のようなものを用意しました。プロジェクトの任意の場所においています。CodeBuildの設定画面的になんとなくルート推奨っぽく見えますがルートじゃなくてもパスを指定すれば動くようになっています。

FROM amazonlinux:latest

ADD ./ /app/

RUN yum install -y aws-cli \
	&& curl --silent --location https://rpm.nodesource.com/setup_6.x | bash \
	&& yum install -y wget \
	&& wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo \
	&& yum install -y yarn \
	&& cd /app \
	&& rm -rf node_modules \
	&& yarn install

EXPOSE 4000
ENTRYPOINT cd /app && yarn start

プロジェクトのソースをADDでイメージの中に含めて固めるというのがこれまでやってこなかったので少し新鮮でした。

ローカルでイメージを作成するには下記のようにします。

$ docker build . -t <my-project>

コンテナを起動し中に入るには下記のようにします。

$ docker run -it --entrypoint /bin/bash <my-project>

CodeBuildの設定を作成する

下記の記事を参考にさせて頂きました。

AWS CodeBuildを使ってDockerイメージをビルドし、Amazon EC2 Container Registry(ECR)へpushする

下記はほぼ上記の記事ままです。一部ぼくの場合はDockerfileをルートに置いてないのでcd app等をしている箇所が入っています。こちらの設定では環境固有の情報はCodeBuildの環境変数の設定から差し込む形になっています。

version: 0.1
 
phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - cd app/ && docker build -t $IMAGE_REPO_NAME .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG

ECRにpushするのにCodeBuildのロールにECRの権限が必要になります。

CodePipelineとCodeBuildの設定

CodePipeline

CodePipelineではSource、Build、DeployでそれぞれProviderを選べるようになっています。このProviderで使いたいものが入っているかが利用の決め手になりそうです。

まずSourceについて、GitHubも対応しています(GitHub Enterpriseは現状非サポート)。どのようにGitHubと連携するのか謎だったのですがマネジメントコンソールからだとOAuthでGUIから連携する形になるようです。思ってたよりお手軽です。CLIからだと別の手段もあるかもしれません。他にはS3、CodeCommitがあります。

次にBuildを設定しますが、CodeBuildの他にもJenkins等いくつか選択肢があります。CodeBuildをCodePipelineの設定画面から作成する事ができます。

比較的お手軽な印象で特にハマることなくできました。Deployについては今回はCodeBuildの中でECRにpushしてるだけなので特に何もしていません。

CodeBuild

CodeBuildの設定をします。少しハマりやすかったのは下記のポイントです。

  • ECRのリポジトリは先にマネジメントコンソールで作っておく
  • Build specificationでプロジェクトルートからのパスでbuildspec.ymlの場所を指定する e.g. dir1/buildspec.yml
  • CodeBuildのロールでECRの権限が必要

概ねそんなところだっと記憶しています。

Fargateのタスク定義を作成する

ようやく慣れてきたんですがECSのマネジメントコンソールが個人的にあまり分かりやすくなくて最初少し混乱してました。設定画面としてはクラスタインスタンスも作るしコンテナというかタスクの定義も作るしECRもあるしで、新しい概念が多いのが原因だと思います。タスク、サービス等Dockerともまた違ってややこしいんですよね…。

Fargateは本来はManagedなのでクラスタインスタンスは関係ないのですが、いちおうECSの枠組みにあるのでクラスタの定義が必要になるようです。ただNetwork Onlyというほぼ何も設定しないクラスタを作成するだけでOKです。タスク定義のほうはEC2の場合とそんなに変わりませんが、CPUやメモリのところがFargateならではになっています。

タスク定義からタスクを起動する

タスク定義からタスクを実行する事ができます。少し分かりづらいところとして、これからタスクを実行するのにタスク定義のところで設定したFargateかEC2という設定を再度する必要がありました。また実行するFargateが利用するVPC、サブネット、セキュリティグループもここで設定する必要があります。コンテナの設定も上書きできるので、どちらかと言うとタスク定義はタスクの雛形として利用するもので、タスク実行時に更に指定して具体的に落とし込む形で使うのかなと思いました。

Entry PointとCommandを書き換える

タスク定義の段階あるいはタスク実行時にEntry PointとCommandを指定する事ができます。ここの書き方が少しハマりました。特に僕の場合下記のような形で2つのコマンドをタスクとして動作させたかったのですが、やり方を調べるのに時間がかかりました。

echo 'test1' && echo 'test2'

上記のような複数のコマンドですが、下記のようにするとうまく動きました。

  • Entry Point: /bin/bash
  • Command: -c, echo ‘test1’ && echo ‘test2’

分かっちゃうとなんてことないんですが…。

AWS SDKからFargateのタスクを実行する

Node.jsからですが下記のような形でタスク実行が出来ました。マネジメント・コンソールで入れている内容とほぼ同じです。

const AWS = require('aws-sdk');
AWS.config.update({ region: 'us-east-1' });

const ecs = new AWS.ECS();
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ECS.html#runTask-property
const params = {
  cluster: 'test',
  taskDefinition: 'task1',
  launchType: 'FARGATE',
  networkConfiguration: {
    awsvpcConfiguration: {
      subnets: [ 
        'subnet-xxxx'
      ],
      assignPublicIp: 'ENABLED',
      securityGroups: [
        'sg-xxxx',
      ]
    }
  },
  platformVersion: 'LATEST',

};
ecs.runTask(params, (err, data) => {
  if (err) {
    console.log(err);
  } else {
    console.log(data);
  }
});

ただ1点ハマりどころがあって、assignPublicIpをENABLEに指定しない場合、CannotPullContainerError: API error (500): Get https://XXX.dkr.ecr.us-east-1.amazonaws.com/v2/...というエラーが発生してタスクがずっとPENDINGのままでRUNNINGになってくれません。

下記のIssueで原因について説明されています。

Fargate: CannotPullContainer located on ECS registry