はじめに
serverless framework で開発を続けていたら、 CloudFormation に詳しくなってきた自称プログラマの遠藤です。
本日は、 serverless framework を使って AWS Fargate へのデプロイに挑戦した話です。
いくつか参考になる記事は見かけたのですが、微妙にやりたいことと異なっていたり、動かしてみて分かったこともあったので、記事にしていきたいと思います。
今回は Fargate を Service で起動する際の定義と、 Scheduled Task で起動する際の定義をまとめます。
Service と Scheduled の違い
その前に、 ざっくりですが Service と Scheduled Task の違いを説明しておきます。
共通点
どちらも ECS のクラスターを使って起動するものです。
また、 Dockerイメージの CMD
または ENTRYPOINT
で指定したプロセスが終了するまで動き続けます。
そして、タスクを起動するルールがそれぞれ異なっています。
Service
Service はタスクの数を指定し、指定した数になるように、タスクを起動してくれます。
途中で起動したプロセスが終了した場合なども、自動で再立ち上げしてくれます。
(EC2 の Auto Scaling に近いイメージです。)
例えば、 Web サーバを起動したり、何かのプロセスを監視したりするのに向いていると思います。
Scheduled Task
Scheduled Task (AWS コンソールの日本語表記は 「タスクのスケジューリング」 ) はその名の通り起動時間(間隔)を指定して、一度だけタスクを開始します。
タスクは、起動時のプロセスが終了するまで動き続け、再起動することはありません。
定期的に実行したいバッチ処理などに向いていると思います。
逆に、うっかりこちらで Web サーバのような動き続けるプロセスを起動した場合、サーバは無限に立ち上がってしまいます。
例えば、 「1日1回起動」といった定義をしておくと、1ヶ月後には約 30 台の Web サーバが起動している状態となります。
また、多重起動を制御をする方法はなさそうなので、別途DBなどでロックを用意したり、 AWS SDK で ECS の実行中のタスクリストを見て、チェックなどを入れるのも良いかもしれません。
デプロイ
それでは、実際に serverless framework を使ったデプロイの定義をご紹介します。
共通
ECR へ DockerImage をプッシュ
Fargate を起動するにあたり、 Docker Image を ECR にプッシュしておく必要があります。
これは以前の記事の CloudWatch Events + Lambda + AWS Batch
という項目で紹介していますので、参考にしてください。
Service の定義
Serverless Fargate Tasks プラグインをインストール
Service で起動する場合は、サードパーティ製のプラグインが提供されていますので、こちらを利用するのが便利です。
https://www.serverless.com/plugins/serverless-fargate-tasks
プロジェクトに、npm プラグインを追加します。
$ yarn add serverless-fargate-tasks
IAM で ecsTaskExecutionRole を作成する
プラグインが、 ecsTaskExecutionRole
という固定の名前の Role で Fargate
を実行します。
もし、利用している AWS アカウントに同名の Role がない場合は、作成しておきましょう。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_execution_IAM_role.html
option で Role の指定が可能なので、別名で Role を作成した場合や、 CloudFormation テンプレートで定義する場合は、それらの ARN を指定することも可能です。
serverless.yml へ定義を追加
あとは、 serverless.yml の custom
内に、以下のように必要な項目を定義していきます。
custom:
fargate:
environment: # Task 起動時の環境変数
hoge: hoge # serverless と同じ書式で環境変数を定義可能
vpc:
security-groups:
- "sg-000000"
subnets:
- "subnet-000000"
role: arn:aws:iam::000000000000:role/ecsTaskExecutionRole
tasks:
sample-task:
name: sample-task
environment:
hoge: fuga # 環境変数の上書きが可能(プラグインの仕様で、こちらを定義しないと全体で定義した環境変数も設定されない。バグっぽい。)
image: 00000000000.dkr.ecr.{region}.amazonaws.com/sample-task:latest
cpu: 256
memory: 0.5GB
override:
container:
# Dokcer 起動時に渡すコマンドを上書きで定義可能
Command:
- "python"
- "daemon/main.py"
- "-f"
role の部分は CloudFormation の組み込み関数を利用して、以下のように書くことも可能です。
role:
!Join
- ''
- - 'arn:aws:iam::'
- !Ref AWS::AccountId
- ":role/ecsTaskExecutionRole"
良いところ
プラグインを使わなくても、 CloudForamtion テンプレートで定義することも可能ですが、このプラグインの良いところは、 Lambda 用に定義している環境変数を使いまわせる点にあります。
どういうことかというと、 Lambda 用の環境変数は以下の書式で定義します。
provider:
name: aws
environment:
Key1: Value1
Key2: Value2
AWS::Lambda::Function Environment
一方で、 ECS で実行するコンテナの環境変数は以下の書式で記載します。
Environment:
- { Name: Key1, Value: Value1 }
- { Name: Key2, Value: Value2 }
AWS::ECS::TaskDefinition KeyValuePair
わざわざ serverless framework でデプロイするのは、作成したリソースを共通で使いたかったりする目的があります。
そう言った意味で、このプラグインは、書式の差分を丸めてくれるので、素晴らしいと思います。
他にも設定できる項目がありますが、 Readme より、 コードを見た方が、個人的には分かりやすかったです。
なお、コードを見るとわかりますが、このプラグインでは、 Service の定義しかしてくれません。
Scheduled Task で Fargate を起動したい場合は、残念ながら自分で CloudFormation の定義をする必要があります。
Scheduled Task の定義
serverless.yml へ定義を追加
それでは、必要なリソースを1つずつ定義していきます。
タスクロール
起動したタスクがどのような権限で動くか定義します。
Fargate を起動させるには、 AssumeRolePolicyDocument で ecs-tasks.amazonaws.com
に対して AssumeRole できるようにしておく必要があるようです。
FargateRole:
Type: AWS::IAM::Role
Properties:
RoleName: fargate-sample-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AssumeRolePolicy
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
- ecs-tasks.amazonaws.com
- events.amazonaws.com
Action: sts:AssumeRole
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
- arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole
LogGroup
Fargate の実行結果を出力する CloudWatch のロググループAWS::Logs::LogGroupを作成します。
FargateLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: FargateSampleClusterLog
Cluster
Fargate を実行するクラスタAWS::ECS::Clusterを作成します。
FargateCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: FargateSampleCluster
TaskDefinition
起動するタスクAWS::ECS::TaskDefinitionを定義します。
FargateSampleTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
ContainerDefinitions:
- Command:
- python
- sample.py
- hogehoge
Name: FargateSampleTaskDefinition
Image: 00000000000.dkr.ecr.{region}.amazonaws.com/sample-task:latest
Environment: # 環境変数の設定
- { Name: name, Value: FargateTestCluster }
- { Name: hoge, Value: hoge }
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-region: ap-northeast-1
awslogs-group: !Ref FargateLogGroup # 上記で定義したロググループを参照
Cpu: 256
ExecutionRoleArn: arn:aws:iam::000000000000:role/ecsTaskExecutionRole
Family: FargateSampleTaskDefinition
Memory: 512
NetworkMode: awsvpc
TaskRoleArn: !Ref FargateRole # 上記で定義したタスクロールを参照
Rule
起動ルールAWS::Events::Ruleを作成します。
ECSSchedule:
Type: AWS::Events::Rule
Properties:
State: ENABLED
Targets:
- Id: ScheduledName
RoleArn: !GetAtt FargateRole.Arn
EcsParameters:
TaskDefinitionArn: # 上記で定義した TaskDefinition の ARN を参照
Fn::GetAtt:
- FargateSampleTaskDefinition
- TaskDefinitionArn
TaskCount: 1
LaunchType: FARGATE
NetworkConfiguration:
AwsVpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- sg-00000000
Subnets:
- subnet-00000000
Arn:
Fn::GetAtt:
- FargateCluster # 上記で定義した Cluster を参照
- Arn
ScheduleExpression: cron(* * * * ? *)
全体
これらを yml の resources 配下に以下のように指定します。
resources:
- Resources:
# タスクロール
FargateRole:
...
# ログの出力先
FargateLogGroup:
...
# クラスター定義
FargateCluster:
...
# タスク定義
FargateTestTaskDefinition:
...
# 定期実行ルール
ECSSchedule:
...
Env の読み込みは?
以上でデプロイすれば、 Fargate を起動させることはできますが、先でもお話ししたように Environment の定義が課題です。
そこで、 Lambda 用の env を別ファイルにしておいて、 python の起動時に読み込ませるという方法が取れます。
env.yml を用意
STAGE: dev
HOGE_NAME: hoge
FUGA_NAME: fuga
lambda は env.yml を参照する
以下は、 serverless.yml に記載する
provider:
name: aws
region: ap-northeast-1
environment: ${file(./env/env.yml)}
Docker は image に env ファイルをコピー
ECR にプッシュする Dockerfile に、以下を記載する
RUN pip install PyYAML
COPY env/env.yml env.yml
python 起動時に環境変数をセットする
Docker の Command で起動する python の先頭で env ファイルを読み込んで環境変数にセットしてしまう
import yaml
f = open("env.yml")
data = yaml.load(f, Loader=yaml.SafeLoader)
for key in data:
os.environ[key] = str(data[key])
対応できないケースがある
基本的には以上の内容で、問題ないのですが、環境変数の読み込みで対応できないケースがあります。
それは、 CloudFormation の組み込み関数を利用している場合です。
たとえば、作成したリソースの ARN を環境変数にセットしたい場合、以下のように書くことができます。
HOGE_ARN:
- Fn::GetAtt:
- HOGE
- Arn
# または以下の書式
HOGE_ARN: !GetAtt HOGE.Arn
これらは、デプロイ時に自動的に補完されるのですが、当然、 Docker のビルドでは補完されず、環境変数には !GetAtt HOGE.Arn
という文字列が登録されることになっていまいます。
現状、これを回避できないケースには遭遇していないので、助かっています。
感想など
以上、 Fargate の起動方法別に、定義をご紹介いたしました。
Lambda は便利なのですが、起動時間に制約があったり、デプロイ可能なファイルサイズに上限があったり、どうしても使えないケースもあるかと思います。
そんな時に、既存のコードを使いまわしながら、一部を Fargate に持っていく、という選択肢が取れると実装の幅が広がるのではないかと思いました。
Scheduled Task については、プラグイン化されていないこともあり、 Service と比べると記載ボリュームが増えてしまったり、いくつかの課題が残っています。
というわけで、 Scheduled Task に関しては、近いうちにカスタムプラグインの作成を試したいなと思っています。
うまく実装できたら、また記事にしていきたいと思います。