このシリーズの一旦の完結編です。 前回構築した ECS にデプロイする仕組みを構築します。 また、コンテナアプリから利用する DynamoDB と SQS も Terraform で構築します。
- はじめに:最終的なゴールと今回の目標の確認
- アプリケーションに必要なリソースを作成する
- アプリケーションの準備
- ECR の作成
- 試しにデプロイ
- デプロイメントパイプラインの設計
- GitLab のリポジトリ作成と GitLab CI の設定
- CodePipeline の作成
- 再度、デプロイを試す
- まとめ
- 参考資料
はじめに:最終的なゴールと今回の目標の確認
- 前回までで、下図の青く囲った部分を構築しました。
- 今回は、赤く囲った部分を構築します。
- ソースコードを管理する GitLab は別で用意します。
アプリケーションに必要なリソースを作成する
- 自作のコンテナイメージを作成する前に、アプリケーションで利用するリソースを作成します。
- DynamoDB は以下のように設定します。
id
をハッシュキーとするテーブルを定義します。- 以下の内容を
dynamodb.tf
に記述して保存します。
resource "aws_dynamodb_table" "example" { name = "Example" read_capacity = 5 write_capacity = 5 hash_key = "id" attribute { name = "id" type = "S" } tags = { Name = "example" } }
- SQS は以下のように設定します。
- SQS のメッセージ送信権限を付与したキューを定義します。
- 以下の内容を
sqs.tf
に記述して保存します。
resource "aws_sqs_queue" "example" { name = "example" max_message_size = 2048 message_retention_seconds = 60 * 60 * 24 * 1 policy = data.aws_iam_policy_document.example-receive.json } data "aws_iam_policy_document" "example-receive" { statement { effect = "Allow" actions = [ "sqs:SendMessage", ] } }
- ECS のタスクに DynamoDB や SQS にアクセスする権限を付与します。
ecs.tf
に以下を追加します。aws_ecs_task_definition
にtask_role_arn
を追加します。container_definitions
のimage
には後で作成する ECR の URL を記述します。AmazonEC2ContainerServiceRole
,AmazonDynamoDBFullAccess
,AmazonSQSFullAccess
のポリシーを付与した IAM ロールを作成し、上のtask_role_arn
に指定します。
# タスク定義 resource "aws_ecs_task_definition" "example" { family = "linkode-example" cpu = 256 memory = 1024 network_mode = "awsvpc" requires_compatibilities = ["FARGATE"] container_definitions = jsonencode([ { "name" : "example", # 利用するイメージを変更 "image" : "${aws_ecr_repository.example.repository_url}:latest", "essential" : true, "logConfiguration" : { "logDriver" : "awslogs", "options" : { "awslogs-region" : "ap-northeast-1", "awslogs-stream-prefix" : "tf-example", "awslogs-group" : "/ecs/example" } }, "portMappings" : [ { "protocol" : "tcp", # アプリケーションの内容に合わせてポート番号を変更 "containerPort" : 3000 } ] } ]) execution_role_arn = module.ecs_task_execution_role.iam_role_arn task_role_arn = aws_iam_role.ecs_task.arn # 新規追加した行 } # ECS サービス resource "aws_ecs_service" "example" { name = "example" cluster = aws_ecs_cluster.example.arn task_definition = aws_ecs_task_definition.example.arn desired_count = 2 launch_type = "FARGATE" platform_version = "1.4.0" health_check_grace_period_seconds = 60 network_configuration { assign_public_ip = false security_groups = [module.tf-example_sg.security_group_id] subnets = [ aws_subnet.private_a.id, aws_subnet.private_c.id, ] } load_balancer { target_group_arn = aws_lb_target_group.ecs.arn container_name = "example" # アプリケーションの内容に合わせてポート番号を変更 container_port = 3000 } lifecycle { ignore_changes = [task_definition] } } module "tf-example_sg" { source = "./security_group" name = "tf-example-sg" vpc_id = aws_vpc.example.id # アプリケーションの内容に合わせてポート番号を変更 port = 3000 cidr_blocks = [aws_vpc.example.cidr_block] } # 以下、新規追加した箇所 # ECS タスク定義に付与する IAM ロール data "aws_iam_policy_document" "ecs_assume_role" { statement { effect = "Allow" actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } } } resource "aws_iam_role" "ecs_task" { name = "tf_example-ecs_task-role" assume_role_policy = data.aws_iam_policy_document.ecs_assume_role.json } resource "aws_iam_role_policy_attachment" "ecs_service" { policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole" role = aws_iam_role.ecs_task.name } resource "aws_iam_role_policy_attachment" "dynamodb-full-access" { policy_arn = "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess" role = aws_iam_role.ecs_task.name } resource "aws_iam_role_policy_attachment" "sqs_full" { policy_arn = "arn:aws:iam::aws:policy/AmazonSQSFullAccess" role = aws_iam_role.ecs_task.name }
アプリケーションの準備
- DynamoDB と SQS を利用する簡単なアプリケーションを TypeScript と Docker で作成します。
- アプリケーションと Dockerfile の内容は GitLab に上げてあります。
ECR の作成
- 作成した Docker イメージを push するためのリポジトリを ECR に作成します。
- 以下の内容を
ecr.tf
に記述して保存します。
# ECR リポジトリの定義 resource "aws_ecr_repository" "example" { name = "example-repo" } # ECR ライフサイクルポリシーの定義 resource "aws_ecr_lifecycle_policy" "example_repo" { repository = aws_ecr_repository.example.name policy = <<EOF { "rules": [ { "rulePriority": 1, "description": "Expire last 30 release tagged images", "selection": { "tagStatus": "tagged", "tagPrefixList": ["release"], "countType": "imageCountMoreThan", "countNumber": 30 }, "action": { "type": "expire" } } ] } EOF }
試しにデプロイ
- ここまでの追加・変更内容を適用します。
. |-- alb.tf |-- clowdwatch.tf |-- dynamodb.tf // 新規作成したファイル |-- ecr.tf // 新規作成したファイル |-- ecs.tf // 内容を変更したファイル |-- iam_role | |-- main.tf | |-- output.tf | `-- variable.tf |-- main.tf |-- network.tf |-- route53.tf |-- s3.tf `|- security_group | |-- main.tf | |-- output.tf | `-- variable.tf `-- sqs.tf // 新規作成したファイル
$ terraform fmt $ terraform get $ terraform validate $ terraform plan $ terraform apply
- ここまで適用したら、 DynamoDB, SQS, ECR が無事に作成されていることを確認します。
- ECR が無事に作成できたら、 Docker イメージを作成し、 push します。
- 以下のコマンドを順番に実行します。
$ aws ecr get-login-password --region [リージョン名] | docker login --username AWS --password-stdin [アカウント名].dkr.ecr.[リージョン名].amazonaws.com $ docker build -t [イメージ名] . $ docker tag [イメージ名]:latest [アカウント名].dkr.ecr.[リージョン名].amazonaws.com/[リポジトリ名]:latest $ docker push [アカウント名].dkr.ecr.[リージョン名].amazonaws.com/[リポジトリ名]:latest
- リポジトリにイメージがプッシュできたら、 ECS をマネジメントコンソールから確認し、ECR から pull してきたイメージで稼働していることを確認します。
- イメージが更新されていない場合、マネジメントコンソールから直接タスクを終了して、再度タスクが立ち上がるのを待ちます。
- 新たなイメージで稼働したら、下記のコマンドで、アプリで実装した動作をすることを確認します。
$ curl 'https://linkode-example.ml/?name=example' {"name":"example"}
- また、 DynamoDB や SQS への動作も正常に行われていることも確認します。
デプロイメントパイプラインの設計
- ここまでで、自作のコンテナイメージを ECS で動かすことが出来るようになりました。
- ここから、ソースコードの変更を自動的に検知して、イメージのビルド→ ECR へのプッシュ→ ECS イメージの更新の流れが自動的に行われるように設定します。
GitLab のリポジトリ作成と GitLab CI の設定
- まずは、ソースコードを保管する GitLab のリポジトリを作成します。
- リポジトリのルートには、
.gitlab-ci.yml
を作成し、コミットしておきます。GitLab CI/CD や .gitlab-ci.yml
に関する説明は過去の記事をご覧ください。
- 以下の内容で作成します。
.gitlab-ci.yml
の内容(クリックして展開)
stages: - test - build test: stage: test image: name: node:14.10-alpine script: - cd app - yarn run test build: stage: build image: name: gcr.io/kaniko-project/executor:debug entrypoint: [""] script: - | cat > /kaniko/.docker/config.json <<EOF { "credsStore": "ecr-login" } EOF - /kaniko/executor --context $CI_PROJECT_DIR/app --dockerfile $CI_PROJECT_DIR/app/Dockerfile --destination [アカウント名].dkr.ecr.ap-northeast-1.amazonaws.com/example-repo:latest
- また、リポジトリの CI/CD の設定から、
AWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
をそれぞれ設定します。
- ここまで作成できたら、一旦 GitLab CI/CD を動かし、ECR のイメージ更新ができていることを確認します。
CodePipeline の作成
- 次に、ECR へのイメージのプッシュを検知して、ECS のイメージ更新を行う設定を行います。
- 以下の内容で
codepipeline.tf
を作成します。- S3, ECR, ESC, IAM に関する適切な権限を付与します。
aws_codepipeline
では、以下のステージを定義します。Source
: ECR から最新イメージを取得します。また、ECR と ECS をつなぐ設定を S3 に置いてある設定ファイルから読み取ります。Deploy
: S3 の設定ファイルの内容を基に、 ECS のイメージを更新します。
codepipeline.tf
の内容(クリックして展開)
# CodePipeline で使用する IAM ロール data "aws_iam_policy_document" "codepipeline" { statement { effect = "Allow" resources = ["*"] actions = [ "s3:PutObject", "s3:GetObject", "s3:GetObjectVersion", "s3:GetBucketVersioning", "ecr:DescribeImages", "ecs:DescribeServices", "ecs:DescribeTaskDefinition", "ecs:DescribeTasks", "ecs:ListTasks", "ecs:RegisterTaskDefinition", "ecs:UpdateService", "iam:PassRole", ] } } module "codepipeline_role" { source = "./iam_role" name = "tf-example-codepipeline" identifier = "codepipeline.amazonaws.com" policy = data.aws_iam_policy_document.codepipeline.json } # CodePipeline 本体 resource "aws_codepipeline" "example" { name = "example" role_arn = module.codepipeline_role.iam_role_arn stage { name = "Source" action { name = "ECRSetting" category = "Source" owner = "AWS" provider = "ECR" version = 1 output_artifacts = ["ECROutArtifact"] configuration = { ImageTag = "latest", RepositoryName = aws_ecr_repository.example.name } } action { name = "S3Setting" category = "Source" owner = "AWS" provider = "S3" version = 1 output_artifacts = ["S3OutArtifact"] configuration = { S3Bucket = [後で作成する S3 バケット名], S3ObjectKey = "imagedefinitions.json.zip", PollForSourceChanges = "false" } } } stage { name = "Deploy" action { name = "Deploy" category = "Deploy" owner = "AWS" provider = "ECS" version = 1 input_artifacts = ["S3OutArtifact"] configuration = { ClusterName = aws_ecs_cluster.example.name ServiceName = aws_ecs_service.example.name DeploymentTimeout = 10 } } } artifact_store { location = "tf-example" type = "S3" } }
- 次に、 ECR へのイメージプッシュのイベントを検知する EventBridge を定義します。
- 以下のように
eventbridge.tf
を作成します。aws_cloudwatch_event_rule
で検知する対象の ECR リポジトリを指定します。aws_cloudwatch_event_target
で、ルールで定めたイベントが起こった場合の動作を設定します。
eventbridge.tf
の内容(クリックして展開)
# --- # CodePipeline を起動させる Event Bridge # Event Bridge の Role data "aws_iam_policy_document" "eventbridge" { statement { effect = "Allow" resources = ["${aws_codepipeline.example.arn}"] actions = [ "codepipeline:StartPipelineExecution", ] } } module "eventbridge_role" { source = "./iam_role" name = "tf-example_eventbridge" identifier = "events.amazonaws.com" policy = data.aws_iam_policy_document.eventbridge.json } # Event Bridge 本体 resource "aws_cloudwatch_event_rule" "ecr_push" { name = "ecr_push" description = "ECR にイメージを Push されたら、 CodePipeline のイベントを発火する" event_pattern = jsonencode( { "source" : [ "aws.ecr" ], "detail-type" : [ "ECR Image Action" ], "detail" : { "action-type" : [ "PUSH" ], "result" : [ "SUCCESS" ], "repository-name" : [ "example-repo" ] } } ) } resource "aws_cloudwatch_event_target" "ecr" { rule = aws_cloudwatch_event_rule.ecr_push.name target_id = "CodePipeline" arn = aws_codepipeline.example.arn role_arn = module.eventbridge_role.iam_role_arn }
- S3 に ECR と ECS をつなぐ設定を記述したファイルを置きます。
- 以下の内容で
imagedefinitions.json
を作成し、 ZIP 圧縮しておきます。
[ { "name": "example", "imageUri": "[アカウント名].dkr.ecr.ap-northeast-1.amazonaws.com/example-repo:latest" } ]
- 以下のように、バージョニングを有効にしたバケットを作成します。
再度、デプロイを試す
- ここまでで準備完了です。実際に、 GitLab にソースコードをプッシュして、デプロイまで自動的に流れることを確認します。
- ローカルでの変更を GitLab にプッシュします。
- GitLab CI/CD が動き始め、パイプラインが作動し始めます。
- パイプラインが無事成功したら、AWS の CodePipeline の画面でデプロイの流れが動作していることがわかります。
- このあと、ECS を確認すると、無事にサービスのタスク定義のリビジョンが上がっていることが確認できます。
まとめ
- 4 回に渡って、 ECS のサービス環境を Terraform で構築する方法を追ってきました。
- GitLab と連携し、 CI/CD の第一歩も実現できました。
- AWS のすべてのリソースを Terraform で管理するのではなく、場合に応じて手動で管理したほうが良いものもあります。プロジェクトや利用するサービスの内容に応じて判断することになりますが、正解は無さそうです。
- 今後は、インフラ環境をコード化出来るようになったことを利用し、インフラ環境の CI/CD の実現を進めていければと思います。
参考資料
- Node+TypeScript+ExpressでAPIサーバ構築
- サンプルのアプリケーションの内容を参考にしました。
- GitLab CI で kaniko を使って Docker Image をビルドしてみる
- kaniko を利用する箇所を参考にしました。
- ECRへのPushでECSをデプロイするだけのシンプルなCodePipelineを試す
- CodePipeline の作成方法・内容について参考にしました。
- ECRからのCodePipelineが走らなくなってた話
- ECR に Push したことを検知するイベントの作成方法を参考にしました。
- Terraform の公式ドキュメント:各リソースの設定方法や利用法、注意点を参照しています。