CI/CDパイプラインを構築するにあたってaws-cdkにcontributeした話

Atsushi Ishibashi
The Finatext Tech Blog
11 min readApr 8, 2020

はじめに

こんにちは、Finatextでエンジニアをしている石橋(@bashi0501)です。

Finatextではコードに近いところでの小さいサイズのtestや静的解析にCircleCI, GitHub Actionsを利用し、クラウド環境へのリリースパイプラインにはより大きいサイズのテストを実環境と同じネットワークで実行したい、デプロイに使うクレデンシャルをむやみに他のサービスに置きたくないという理由からAWS CodeBuild, AWS CodePipelineを使用しています。

これまではCodePipelineによるリリースパイプラインをマネジメントコンソールから丹精込めてお手製で作っていました。が、以前の田島の記事でもあったように50個近くのAWSアカウントがある中でこれまでの方針で継続・展開していくのは厳しくなってきました。

そこでパイプラインは一つのAWSアカウントに集約し、そこから各アカウントに配布する方針で再設計していくこととなりました。その構築にあたってaws-cdkを採用したのですが、意図通りに動かないところがあり、Contributeして修正するまでの話を書きます。

ちなみに以前のTerraformリソースの作り方にあるようにFinatextではInfrastructure as Code(以下IaC)にはTerraformを採用していますが、今回新たにaws-cdkを使ってみての所感も書ければ、と思います。

クロスアカウントのパイプライン

基本構成としては
- GitHubをソースプロバイダとしたCodeBuildでS3にソースを格納
- CodePipeline上のSourceステージにS3を使用
- BuildステージにはCodeBuildを並列で使用し、testやbuildを回す
- DeployステージではCodeBuildやECSなどを使用し、サービスアカウントに設定されたroleをassumeして各アカウントに配布する

発生した問題

cdkのv1.27.0では以下の抜粋した部分のようにEcsDeployActionはecs.BaseServiceクラスを要求しています。
参照コード

export interface EcsDeployActionProps extends codepipeline.CommonAwsActionProps {/**
* The ECS Service to deploy.
*/
readonly service: ecs.BaseService;
}

そうするとECSサービスの作成とともにCodePipeline上のDeployActionも構築する必要があり、サンプルコードとしては以下のようになります。

const stack = new cdk.Stack();
const taskDefinition = new ecs.FargateTaskDefinition(stack, ‘TaskDefinition’);
taskDefinition.addContainer(‘MainContainer’, {
image: ecs.ContainerImage.fromRegistry(‘amazon/amazon-ecs-sample’),
});
const vpc = new ec2.Vpc(stack, ‘VPC’);
const cluster = new ecs.Cluster(stack, ‘Cluster’, {
vpc,
});
const service = new ecs.FargateService(stack, ‘FargateService’, {
cluster,
taskDefinition,
});
const deployerRole = iam.Role.fromRoleArn(this, “deployerRole”, ‘deployerArn’, {
mutable: false, //クロスアカウントのroleにassumeするため必要
});
const deployAction = new codepipeline_actions.EcsDeployAction({
actionName: ‘DeployAction’,
service: service,
role: deployerRole,
});

しかし、上で説明したように各アカウント上のECSサービスはcdkの外で構築しており、既存リソースを参照する形でEcsDeployActionを作れる必要がありました。

そこでこのPRでBaseServiceクラスではなくて、IBaseServiceインターフェイスに依存させるようにして以下のような形で書くことができるようになりました。
v1.28.0から利用可能となっています。

const stack = new cdk.Stack();
const clsuterName = ‘my-cluster’;
const serviceName = ‘my-service’;
//fromClusterAttributesで必要なだけで利用しないのでdummy
const vpc = ec2.Vpc.fromVpcAttributes(stack, `VPC`, {
vpcId: ‘dummy’,
availabilityZones: [‘a’, ‘b’, ‘c’]
});
//Ec2ServiceでもFargateServiceでもどちらでもOK
const service = ecs.Ec2Service.fromEc2ServiceAttributes(stack, `EcsService`, {
serviceName: serviceName,
cluster: ecs.Cluster.fromClusterAttributes(stack, `Cluster`, {
vpc,
securityGroups: [],
clusterName: clsuterName,
}),
});
const deployerRole = iam.Role.fromRoleArn(this, “deployerRole”, ‘deployerArn’, {
mutable: false, //クロスアカウントのroleにassumeするため必要
});
const deployAction = new codepipeline_actions.EcsDeployAction({
actionName: ‘DeployAction’,
service: service,
role: deployerRole,
});

VPCとか、EC2でもFargeteでもどっちのServiceでも良いとことか、ちょっと微妙なところは残っていますが、当初の目的は達成できました。(ここのところはPRするチャンスありかも?)

個人的に一番しんどかったのはcdkがおそらく?シアトルのチームで開発していて時差が-16時間、つまり日本時間の深夜2時にシアトルは朝10時なのでレビューの往復が続くときは生活リズムが崩れてしまうとこでした笑
日本で働いていてOSSに関わる際の障壁としては時差が一番大きいのかなと思っています。

別解

ここまでcdkに改修を入れて解決するという方向で進めてきましたが、aws-cdkを日頃使っている方は思っていることがあるかもしれません。
cdkの最終的な生成物はあくまでCloudFormationのymlであり、インターフェイスを満たすように自前でクラスを作ってリソースを作ることもできます。

例えば以下のようなクラスを作ることで既存のECSサービスを使ったDeployActionが構築できます。

interface MyEcsDeployActionProps extends codepipeline.CommonAwsActionProps {
readonly input: codepipeline.Artifact;
readonly serviceName: string;
readonly clusterName: string;
}
class MyEcsDeployAction implements codepipeline.IAction {
public readonly actionProperties: codepipeline.ActionProperties;
private readonly props: MyEcsDeployActionProps;
constructor(props: MyEcsDeployActionProps) {
this.actionProperties = {
…props,
category: codepipeline.ActionCategory.DEPLOY,
provider: ‘ECS’,
artifactBounds: { minInputs: 1, maxInputs: 1, minOutputs: 0, maxOutputs: 0 },
inputs: [props.input],
role: props.role
};
this.props = props;
}
public bind(_scope: cdk.Construct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions):
codepipeline.ActionConfig {
return {
configuration: {
ClusterName: this.props.clusterName,
ServiceName: this.props.serviceName,
},
};
}
public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule {
throw new Error(“Method not implemented.”);
}
}

このあたりがTerraformやCloudFormationとは違うところです。使う必要のあるリソースがTerraformで対応されてなければPRを送れば良い、という思考停止に陥っていた僕にとっては戦慄が走りました。
ただその10秒後くらいに「でも結局はCloudFormationがサポートしてないとだめか」と気づきました。

今後の課題

  • 一つのアカウントにAWS CodePipelineなど同じタイプのリソースを作っていくためリミットの制約は気にする必要がある。公式資料
  • CodePipelineのSourceステージのGitHubのトリガーが不十分なためCodeBuildを利用している。CodePipelineのSourceステージのGitHubのトリガーが不十分なためCodeBuildを利用している

IaCの所感

まずcdkを使ってみての感想としては、かなりAWSを把握している上級者向けという印象です。ネット上にあるサンプルではカバーできないものをcdkで作るとなると、まず前提としてCloudFormationのymlは読める必要があるし、`cdk synth`などで吐き出したymlが期待しているものと異なるときはcdkのオリジナルソースを調べる必要があります。
そして、この2つの必要とされるスキルが領域としてはそこそこ遠いものなのかな、という印象です。

ちょうど先日カヤックの藤原さんが以下ツイートをしていました。

おそらく起点となったのはこちらのブログかなと。(ただansibleだったりも含んでの話なので少し話は変わる)

抽象化し過ぎに関しては概ね同意で僕自身のスタンスは以下です。

じゃあcdkは微妙なのか、というとそんなことはないと思っています。

AWSとしてはアプリケーションエンジニアがAWSサービスをどんどん触ってアプリケーションを作っていってほしい、という期待もあってTypeScriptを選択したのかと思います。

ただTypeScriptだとHCLよりも余計にモジュール化・共通化に傾倒する。そこで今回のケースのようなエンジニア全員にIaC書いてほしいけど、やれることは絞りたい、定めたいというときには一つの選択肢にはなってくるかと思います。

おわりに

Finatextグループでは、一緒に働くエンジニアや仲間を募集しています!
従来の金融ビジネスに縛られない、新たな金融サービスを開発したい方は是非ご連絡ください!!お待ちしてます!

求人一覧はこちら
社員インタビューはこちら

--

--

No responses yet