AWSでワークフローエンジンDigdagを立てるにあたりスケールを見越してECS+Fargateで動かす。 全体のコードはGitHubにある。
リソースはCDKで作る。最近GAになったので高レベルのクラスを積極的に使っている。
AWS CDKでCloudFormationのテンプレートをTypeScriptから生成しデプロイする - sambaiz-net
$ npm run cdk -- --version
1.2.0 (build 6b763b7)
VPC
FargateなのでVPCが必要。 テンプレートを書くとSubnetやRouteTable、NATGatewayなど記述量が多くなるところだが、CDKだとこれだけで済む。
CloudFormationでVPCを作成してLambdaをデプロイしAurora Serverlessを使う - sambaiz-net
const vpc = new ec2.Vpc(this, 'VPC', {
cidr: props.vpcCidr,
natGateways: 1,
maxAzs: 2,
subnetConfiguration: [
{
name: 'digdag-public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
name: 'digdag-private',
subnetType: ec2.SubnetType.PRIVATE,
},
{
name: 'digdag-db',
subnetType: ec2.SubnetType.ISOLATED,
}
]
})
DB
DigdagはPostgreSQLを使う。
const db = new rds.DatabaseCluster(this, 'DBCluster', {
engine: rds.DatabaseClusterEngine.AURORA_POSTGRESQL,
engineVersion: '10.7',
instances: 1,
masterUser: {
username: 'digdag',
},
defaultDatabaseName: databaseName,
port: dbPort,
instanceProps: {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE),
vpc: vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.ISOLATED
}
},
parameterGroup: new rds.ClusterParameterGroup(this, 'DBClusterPArameterGroup', {
family: 'aurora-postgresql10',
parameters: {
application_name: 'digdag',
}
}),
removalPolicy: cdk.RemovalPolicy.DESTROY // for test
})
ECS
ECS Cluster, Service, TaskやRole, Route53のRecordを作り、
デフォルトでDBのIngressが空なのでServiceからアクセスできるようにする。
ecsPatterns.LoadBalancedFargateService
によって少ない記述量で実現できている。
ecs.ContainerImage.fromAsset
でStackをデプロイする前にDocker buildが走って cdk bootstrap
で作られたBucketに上がる。
const cluster = new ecs.Cluster(this, 'ECSCluster', {
clusterName: 'digdag',
vpc: vpc
})
const executionRole = new iam.Role(this, 'ExecutionRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')
]
})
const taskRole = new iam.Role(this, 'TaskRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')
]
})
const domainZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
hostedZoneId: props.route53ZoneId,
zoneName: props.route53ZoneName
})
const service = new ecsPatterns.LoadBalancedFargateService(this, 'Service', {
cluster,
image: ecs.ContainerImage.fromAsset('./docker'),
loadBalancerType: ecsPatterns.LoadBalancerType.APPLICATION,
certificate: certificatemanager.Certificate.fromCertificateArn(this, 'Certificate', props.acmArn),
domainName: `${props.route53RecordName}.${props.route53ZoneName}.`,
domainZone,
cpu: 256,
memoryLimitMiB: 512,
environment: {
DB_USERNAME: db.secret!.secretValueFromJson('username').toString(),
DB_PASSWORD: db.secret!.secretValueFromJson('password').toString(),
DB_HOST: db.secret!.secretValueFromJson('host').toString(),
DB_PORT: db.secret!.secretValueFromJson('port').toString(),
DB_DATABASE: databaseName,
S3_LOG_BUCKET: props.logBucket
},
executionRole,
taskRole
})
const dbSG = ec2.SecurityGroup.fromSecurityGroupId(this, 'DBSG', db.securityGroupId)
const serviceSG = ec2.SecurityGroup.fromSecurityGroupId(this, 'ServiceSG', service.service.connections.securityGroups[0].securityGroupId)
dbSG.addIngressRule(serviceSG, ec2.Port.tcp(dbPort))
dbSG.addIngressRule(serviceSG, ec2.Port.tcp(dbPort))
認証
ECSに飛ばす前にCognito認証を挟む。デフォルトでALBのSGのEgressがServiceへの80番ポートしか開いていないため、HTTPS通信できず認証前に500エラーになってしまう。
そのためSGのIDを取ってルールを追加することになるのだが、
string[]
である service.loadBalancer.loadBalancerSecurityGroups
から loadBalancerSecurityGroups[0]
のように取ろうとすると
Tokenが解決されず #{TOKEN[Foo.1234]}
のような文字列になってしまうため
Fn::Selectを使う必要がある。
LambdaとALBでCognito認証をかけて失敗したらログイン画面に飛ばす - sambaiz-net
const lbSG = ec2.SecurityGroup.fromSecurityGroupId(this, 'LBSG',
cdk.Fn.select(0, service.loadBalancer.loadBalancerSecurityGroups))
lbSG.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443))
lbSG.addEgressRule(ec2.Peer.anyIpv6(), ec2.Port.tcp(443))
new loadbalancer.CfnListenerRule(this, 'AuthListenerRule', {
conditions: [{
field: 'path-pattern',
values: ['/*']
}],
listenerArn: service.listener.listenerArn,
priority: 10,
actions: [{
type: 'authenticate-cognito',
order: 1,
authenticateCognitoConfig: {
onUnauthenticatedRequest: 'authenticate',
userPoolArn: props.userPoolArn,
userPoolClientId: props.userPoolClientId,
userPoolDomain: props.userPoolDomain
}
}, {
type: 'forward',
order: 2,
targetGroupArn: service.targetGroup.targetGroupArn
}]
})
digdag.properties
ログはS3に送る。
server.bind = 0.0.0.0
server.port = 80
database.type = postgresql
database.user = <DB_USERNAME>
database.password = <DB_PASSWORD>
database.host = <DB_HOST>
database.port = <DB_PORT>
database.database = <DB_DATABASE>
log-server.type=s3
log-server.s3.bucket=<S3_LOG_BUCKET>
log-server.s3.path=logs/
log-server.s3.direct_download=false
実行時に環境変数の値に書き換えている。
#!/bin/sh
sed -i -e "s/<DB_USERNAME>/${DB_USERNAME}/" \
-e "s/<DB_PASSWORD>/${DB_PASSWORD}/" \
-e "s/<DB_HOST>/${DB_HOST}/" \
-e "s/<DB_PORT>/${DB_PORT}/" \
-e "s/<DB_DATABASE>/${DB_DATABASE}/" \
-e "s/<S3_LOG_BUCKET>/${S3_LOG_BUCKET}/" digdag.properties
/usr/local/bin/digdag server --config digdag.properties
ワークフローの実行
認証後WebUIに飛び、適当なdigファイルを作りRunボタンを押してログが出たら成功。
+hello:
echo>: "Hello Digdag"