1
/
5

-Qiita記事Part.27-CloudFormation使ってみた

こんにちは、ナイトレイインターン生の鈴木です。
Wantedlyをご覧の方に、ナイトレイのエンジニアがどのようなことをしているか知っていただきたく、Qiitaに公開している記事をストーリーに載せています。

そして今回の記事はWantedly初登場、インフラエンジニアの大塩さんの記事です!
少しでも私たちに興味をお持ちいただけた方は下に表示される募集記事もご覧ください↓↓

こんにちは!株式会社ナイトレイで働くインフラエンジニアです。
今回は、CloudFormationを用いた実装の一部を紹介します!

背景

aws opsworksサービス終了に伴い、これまでchefで管理していたwebサイトのインフラリソースを、Cloud Formationで作り直すことに!ついでに、ec2インスタンスからECSに乗り換えることにしたので、一部共有します。

Cloud Formationとは

AWSのリソースをコードで管理できるサービスです。テンプレートと呼ばれるテキストファイル(YAML/JSON)を読み込むと、自動でAWSの環境を作ってくれます。料金は、利用しているリソース分支払えば良いだけで、Cloud Formation自体の利用は無料です。チュートリアルも用意されているのでご参考までに📕

チュートリアル
AWS CloudFormation テンプレートを使用して、スタックをプロビジョニングしたい AWS リソースを定義します。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/CHAP_Using.html

EC2からECSへの移行について

ECS(Elastic Container Service)とは、Dockerコンテナのデプロイや運用管理を簡単に行うための、AWSのサービスです。コンテナベースでサービスを管理できるため、EC2と比べ、運用を簡素化、また効率化することができます。

プロジェクト構成

.
├── Makefile
├── README.md
└── {service_name}/
  └── cf/
├── {env}/ 環境固有のファイル群
│ ├── .params
│ └── main.yml
└── templates/
├── app/
│  ├── task.yml
│  ├── ecr.yml
│  └── ecs.yml
└── lb/
  └── alb.yml

実際に作成したものから、一部抜粋します。

サービス単位でディレクトリを切り、テンプレートとスタック作成のファイルを分離しました。

また、パラメータをgithubなどで管理したくないので、.paramsをs3に置き、実行時にshellでダウンロードしてくる仕様にしました。

(他にもネットワークやセキュリティなどなど作りましたが、本記事では割愛します。)

テンプレート作成

公式リファレンスを見ながら作っていきます。

Template reference
Learn about the resources types, resource properties, resource attributes, intrinsic functions, and pseudo parameters that you can use in CloudFormation templates.
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-reference.html

ecr.yml

Parameters:
Env:
Type: String
ServiceName:
Type: String

Resources:
# ECR
EcrNginx:
Type: AWS::ECR::Repository
Properties:
RepositoryName: !Sub ${ServiceName}-${Env}-nginx
ImageTagMutability: MUTABLE
ImageScanningConfiguration:
ScanOnPush: true
EncryptionConfiguration:
EncryptionType: AES256
LifecyclePolicy:
LifecyclePolicyText: >
{
"rules": [
{
"action": {
"type": "expire"
},
"selection": {
"countType": "imageCountMoreThan",
"countNumber": 4,
"tagStatus": "any"
},
"description": "Delete more than 4 images",
"rulePriority": 1
}
]
}
RegistryId: !Ref AWS::AccountId

EcrPhp:
Type: AWS::ECR::Repository
Properties:
RepositoryName: !Sub ${ServiceName}-${Env}-php
ImageTagMutability: MUTABLE
ImageScanningConfiguration:
ScanOnPush: true
EncryptionConfiguration:
EncryptionType: AES256
LifecyclePolicy:
LifecyclePolicyText: >
{
"rules": [
{
"action": {
"type": "expire"
},
"selection": {
"countType": "imageCountMoreThan",
"countNumber": 4,
"tagStatus": "any"
},
"description": "Delete more than 4 images",
"rulePriority": 1
}
]
}
RegistryId: !Ref AWS::AccountId

微々たるものですが、コストかかるのでイメージは3世代管理に。

task.yml

Parameters:
Env:
Type: String
ServiceName:
Type: String
AccountId:
Type: String
WebCpu:
Type: Number
WebMemory:
Type: Number
NginxLatestTag:
Type: String
PhpLatestTag:
Type: String

Resources:
# Task Definition
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub ${ServiceName}-${Env}-web
Cpu: !Ref WebCpu
Memory: !Ref WebMemory
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn: !Sub arn:aws:iam::${AccountId}:role/ecsTaskExecutionRole
TaskRoleArn: !Sub arn:aws:iam::${AccountId}:role/ecsTaskRole
ContainerDefinitions:
- Name: !Sub ${ServiceName}-${Env}-nginx-container
Image: !Sub ${AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${ServiceName}-${Env}-nginx:${NginxLatestTag}
Essential: true
PortMappings:
- HostPort: 80
ContainerPort: 80
Protocol: tcp
LinuxParameters:
initProcessEnabled: true
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-create-group: true
awslogs-group: !Sub /ecs/${ServiceName}-${Env}-nginx
awslogs-region: ap-northeast-1
awslogs-stream-prefix: "ecs"
awslogs-datetime-format: "%Y-%m-%d %H:%M:%S"
- Name: !Sub ${ServiceName}-${Env}-php-container
Image: !Sub ${AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${ServiceName}-${Env}-php:${PhpLatestTag}
EnvironmentFiles:
- Value: !Sub arn:aws:s3:::${ServiceName}-env/ecs/${Env}.env
Type: s3
PortMappings:
- HostPort: 9000
ContainerPort: 9000
Protocol: tcp
LinuxParameters:
initProcessEnabled: true
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-create-group: true
awslogs-group: !Sub /ecs/${ServiceName}-${Env}-php
awslogs-region: ap-northeast-1
awslogs-stream-prefix: "ecs"
awslogs-datetime-format: "%Y-%m-%d %H:%M:%S"

Outputs:
TaskDefinition:
Value: !Ref TaskDefinition

スタンダードなnginxとphpコンテナ構成。

ecs.yml

Parameters:
Env:
Type: String
ServiceName:
Type: String
SgWebId:
Type: AWS::EC2::SecurityGroup::Id
TaskDefinition:
Type: String
PrivateSubnetCId:
Type: AWS::EC2::Subnet::Id
PrivateSubnetDId:
Type: AWS::EC2::Subnet::Id
TargetGroup:
Type: String

Resources:
# ECS Cluster
EcsCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub ${ServiceName}-${Env}-cluster

# ECS Service
EcsService:
Type: AWS::ECS::Service
Properties:
PlatformVersion: 1.4.0
ServiceName: !Sub ${ServiceName}-${Env}-service
Cluster: !Ref EcsCluster
TaskDefinition: !Ref TaskDefinition
DesiredCount: 2
LaunchType: FARGATE
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 100
DeploymentCircuitBreaker:
Enable: true
Rollback: true
EnableExecuteCommand: true
HealthCheckGracePeriodSeconds: 120
PropagateTags: TASK_DEFINITION
NetworkConfiguration:
AwsvpcConfiguration:
Subnets:
- !Ref PrivateSubnetCId
- !Ref PrivateSubnetDId
SecurityGroups:
- !Ref SgWebId
AssignPublicIp: DISABLED
LoadBalancers:
- TargetGroupArn: !Ref TargetGroup
ContainerName: !Sub ${ServiceName}-${Env}-nginx-container
ContainerPort: 80

Outputs:
EcsCluster:
Value: !Ref EcsCluster
EcsService:
Value: !GetAtt EcsService.Name

パラメータのtypeはstringとリソースタイプでばらつきがありますが、まぁ良しとします(本当は、リソースタイプを指定すべきですね…)。

alb.yml

Parameters:
Env:
Type: String
AccountId:
Type: String
ServiceName:
Type: String
Domain:
Type: String
Certificates:
Type: String
SgAlbId:
Type: AWS::EC2::SecurityGroup::Id
VpcId:
Type: String
PublicSubnetCId:
Type: AWS::EC2::Subnet::Id
PublicSubnetDId:
Type: AWS::EC2::Subnet::Id

Resources:
Alb:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub ${ServiceName}-${Env}-alb
Scheme: internet-facing
Type: application
IpAddressType: ipv4
Subnets:
- !Ref PublicSubnetCId
- !Ref PublicSubnetDId
SecurityGroups:
- !Ref SgAlbId
LoadBalancerAttributes:
- Key: idle_timeout.timeout_seconds
Value: 120

TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub ${ServiceName}-${Env}-tg
Port: 80
Protocol: HTTP
TargetType: ip
VpcId: !Ref VpcId
HealthCheckEnabled: true
HealthyThresholdCount: 2
HealthCheckTimeoutSeconds: 5
HealthCheckProtocol: HTTP
HealthCheckPort: 80
HealthCheckPath: /healthcheck
HealthCheckIntervalSeconds: 30
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 300
- Key: stickiness.enabled
Value: true
- Key: stickiness.type
Value: lb_cookie
- Key: stickiness.lb_cookie.duration_seconds
Value: 86400

DefaultListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref Alb
Port: 443
Protocol: HTTPS
Certificates:
- CertificateArn: !Sub arn:aws:acm:ap-northeast-1:${AWS::AccountId}:certificate/${Certificates}
SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
DefaultActions:
- Type: fixed-response
FixedResponseConfig:
ContentType: text/plain
StatusCode: 403

CustomListener:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Priority: 100
ListenerArn: !Ref DefaultListener
Actions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
Conditions:
- Field: path-pattern
PathPatternConfig:
Values:
- "/*"
- Field: host-header
HostHeaderConfig:
Values:
- !Ref Domain

Outputs:
Alb:
Value: !Ref Alb
TargetGroup:
Value: !Ref TargetGroup

ターゲットグループはデフォルト403で返すようにして、Hostが指定したドメインの場合のみECSコンテナにアクセスさせるようにします。

定番のアクセス制限ですね。

余談ですが、サブネットにaがない理由はNatゲータウェイ作成時に下記エラーが出たためです。

“Nat Gateway is not available in this availability zone.”

長年AWS使っていて初めて遭遇しました…こんな事があるんですね。

調べたところAZには新旧あるようで、古いアカウントだと発生しうるようです。

何か対応すれば使えるようになるという類のものではなさそうなので、今回aは使用しないことにしました。

参考:

旧AZ(apne1-az3)でできないこと - サーバーワークスエンジニアブログ
皆さんはap-northeast-1aを使っていらっしゃいますか。 使ってますよね。 私のap-northeast-1aとあなたのap-northeast-1aは本当は違うAZかもしれない。 そんなこと聞いたことありませんか。 自分のAZがなんであるのかはRAMで確認できます。 自分のアカウント間でアベイラビリティーゾーンをマッピングする方法を教えてください。 東京リージョンの場合、一般的にはこんな感じに 「お客様のAZ ID」 が3つ見えます。 ところが、古い時期に作成されたAWSアカウントにはAZが4
https://blog.serverworks.co.jp/tech/2019/10/02/apne1-az3/

スタック作成

main.yml

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
Env:
Type: String
VpcId:
Type: String
IgwId:
Type: String
ServiceName:
Type: String
ServiceDirName:
Type: String
Domain:
Type: String
Certificates:
Type: String
TemplateBucketName:
Type: String
WebCpu:
Type: Number
WebMemory:
Type: Number
PublicCidrC:
Type: String
PublicCidrD:
Type: String
PrivateCidrC:
Type: String
PrivateCidrD:
Type: String
NginxLatestTag:
Type: String
PhpLatestTag:
Type: String

Resources:
#---ネットワーク割愛---

Ecr:
Type: AWS::CloudFormation::Stack
Properties:
Parameters:
Env: !Ref Env
ServiceName: !Ref ServiceName
TemplateURL: !Sub 'https://${TemplateBucketName}.s3.ap-northeast-1.amazonaws.com/${ServiceDirName}/templates/app/ecr.yaml'
Tags:
- Key: Name
Value: !Sub ${ServiceName}-${Env}
Alb:
Type: AWS::CloudFormation::Stack
DependsOn: Sg
Properties:
Parameters:
Env: !Ref Env
AccountId: !Ref "AWS::AccountId"
ServiceName: !Ref ServiceName
Domain: !Ref Domain
Certificates: !Ref Certificates
SgAlbId: !GetAtt Sg.Outputs.SgAlbId
VpcId: !Ref VpcId
PublicSubnetCId: !GetAtt Subnet.Outputs.PublicSubnetC
PublicSubnetDId: !GetAtt Subnet.Outputs.PublicSubnetD
TemplateURL: !Sub 'https://${TemplateBucketName}.s3.ap-northeast-1.amazonaws.com/${ServiceDirName}/templates/lb/alb.yaml'
Tags:
- Key: Name
Value: !Sub ${ServiceName}-${Env}
EcsTask:
Type: AWS::CloudFormation::Stack
DependsOn: Alb
Properties:
Parameters:
Env: !Ref Env
AccountId: !Ref "AWS::AccountId"
ServiceName: !Ref ServiceName
WebCpu: !Ref WebCpu
WebMemory: !Ref WebMemory
NginxLatestTag: !Ref NginxLatestTag
PhpLatestTag: !Ref PhpLatestTag
TemplateURL: !Sub 'https://${TemplateBucketName}.s3.ap-northeast-1.amazonaws.com/${ServiceDirName}/templates/app/task.yaml'
Tags:
- Key: Name
Value: !Sub ${ServiceName}-${Env}
Ecs:
Type: AWS::CloudFormation::Stack
DependsOn: EcsTask
Properties:
Parameters:
Env: !Ref Env
ServiceName: !Ref ServiceName
SgWebId: !GetAtt Sg.Outputs.SgWebId
TaskDefinition: !GetAtt EcsTask.Outputs.TaskDefinition
PrivateSubnetCId: !GetAtt Subnet.Outputs.PrivateSubnetC
PrivateSubnetDId: !GetAtt Subnet.Outputs.PrivateSubnetD
TargetGroup: !GetAtt Alb.Outputs.TargetGroup
TemplateURL: !Sub 'https://${TemplateBucketName}.s3.ap-northeast-1.amazonaws.com/${ServiceDirName}/templates/app/ecs.yaml'
Tags:
- Key: Name
Value: !Sub ${ServiceName}-${Env}

#---バッチ、スケーリング設定割愛

変更セット作成

CloudFormationには、create-stackコマンドが用意されていますが、今回はterraformっぽくdeployを使います。

aws cloudformation deploy \
--stack-name "$STACK_NAME" \
--template-file "./cf/$ENV/main.yaml" \
--parameter-overrides $(cat ./cf/$ENV/.params) \
--no-execute-changeset \
--profile "$PROFILE"

--no-execute-changesetを付与すると、変更セットの作成にとどめてくれます。

デプロイ

aws cloudformation deploy \
--stack-name "$STACK_NAME" \
--template-file "./cf/$ENV/main.yaml" \
--parameter-overrides $(cat ./cf/$ENV/.params) \
--profile "$PROFILE"

--no-execute-changesetを外しただけです。

ちなみに初回のデプロイに失敗した場合、削除せずに変更セットを再作成しようとするとエラーになります。デプロイ成功後2回目以降は削除不要で変更セットが作成できます。

削除

aws cloudformation delete-stack \
--stack-name "$STACK_NAME" \
--profile "$PROFILE"

感想

ymlファイルは非常にシンプルで作成しやすいです(CloudFromationはJsonでも可)。

しかし、変更セットはterraformに比べると差分がわかりにくかったです。システム規模が大きくなるとボトルネックになりそう…。

また、今回は、stacksetsを使わなかったので次回は使ってみたいです!

最後に

私たちの会社、ナイトレイでは一緒に事業を盛り上げてくれるエンジニアを募集しています!
Web開発メンバー、GISエンジニア、サーバーサイドエンジニアなど複数ポジションで募集しているため、
「専攻分野を活かしたい」「横断的に様々な業務にチャレンジしてみたい」と言ったご要望も相談可能です!

✔︎ GISの使用経験があり、観光・まちづくり・交通・防災系などの分野でスキルを活かしてみたい
✔︎ ビッグデータの処理が好き!(達成感を感じられる)
✔︎ データベース構築、サーバー周りを触るのが好き
✔︎ 社内メンバーだけではなく顧客とのやり取りも実はけっこう好き
✔︎ 自社Webサービスの開発で事業の発展に携わってみたい
✔︎ 地理や地図が好きで、位置情報データにも興味を持っている

一つでも当てはまる方は是非お気軽に「話を聞きに行きたい」ボタンを押してください!

Invitation from 株式会社ナイトレイ
If this story triggered your interest, have a chat with the team?
株式会社ナイトレイ's job postings

Weekly ranking

Show other rankings
Like 鈴木 梨子's Story
Let 鈴木 梨子's company know you're interested in their content