はじめに
こちらの記事で執筆した通り、2024/01からAWSの設計構築案件に参画し、現在で1年3ヶ月が経過しました。
こちらの記事ではAWS案件に参画するまでにやってきたことをまとめていますが、本記事はAWS設計構築でやったきたことのまとめとなります。
経験してきたフェーズとしては以下の通りです。
各フェーズにおいてどのようなことを経験してきたのかをまとめていきたいと思います。
途中プロジェクト内で別案件の依頼があり、コンテナイメージで使用しているAmazon Linux2 -> 2023へのVersionUp対応、およびLambdaのPython3.8 -> 3.12へのVersionUp対応もしてきました。
基本設計
性能設計書の一部を担当
基本設計フェーズの途中から参画したため、担当したタスクはそれほど多くはありません。
性能設計においては具体的に以下のようなドキュメントを作成しました。
OS、ミドルウェア製品の要件
具体的に、OS(RHEL9)、DB(Oracle, MySQL)、ウイルス対策、監視(Zabbix, Prometheus)、コンテナ管理 (Kubernetes)などのCPU、メモリ、ディスクなどの要件について各公式ドキュメントの情報を確認し一覧にまとめる。
性能対策
AWSサービスにおける性能向上対策においてどんなことができるのか?をまとめる
一例ですが、Amazon RDS for MySQLであればスロークエリログ、InnoDBバッファプール、Performance Insightsを使用するなどです (詳細の内容は省略しています)
キャパシティプランニング
EC2インスタンスやコンテナのメモリ、CPU使用率がどの程度まで負荷が上がったらスケールアップするなどの拡張可否を設計します。
ただ今回のプロジェクトにおいてはそこまで細かく設定することはなく、あまり設計要素はありませんでした。
システム構成図
システム構成図の作成においては各基本設計書から内容を洗い出し、開発、検証、本番各環境の構成図の作成をしました。
ここでは構成図をそのまま載せることはできませんが以下概要です。
- マルチAZ構成 (3AZ)
- 外部から接続にはCloudFront -> AWS WAF -> AWS Network Firewallを使用
- プライベートサブネットにEKSクラスターを構築し、各コンテナをデプロイ
- その背後のプライベートサブネットにジョブ管理サーバ (EC2)、監視サーバ(EC2)、RDS(Oracle, MySQL)を配置
- リリース方式はCI/CDによるGitHubへのPushをトリガーにCodePipelineが起動し、CodeBuildのよりコンテナをデプロイ
1AZで超ざっくり書くと以下のようなイメージ
詳細設計 (パラメータ設計)
詳細設計のフェーズでは主にAWSリソースのパラメータ設計書の作成をしました。
担当したのは以下となります。
- ネットワーク設計
- Amazon VPC
- Subnet
- Route Table
- VPC Endpoint
- Amazon Route 53
- AWS Site-to-Site VPN
- セキュリティ設計
- AWS Network Firewall
- AWS WAF
- AWS key Management Service (KMS)
- 外部接続
- Amazon CloudFront
- コンテナ
- Amazon Elastic Container Sercvice (EKS)
- コンテナイメージ
- マニフェストファイル
- データベース
- Amazon RDS for MySQL
詳細設計フェーズでは基本設計の情報を基に、構築時に必要となる情報の洗い出しを行います。
ネットワーク設計については独学していたころから、比較的みたことがある内容が多かったため、洗い出しにはそれほど苦労はしませんでしたが、AWS Network Firewall、CloudFront、EKSやマニフェストファイルなどについては初物尽くしだったこともあり、大変だったと同時に調べて知識がついていくことに非常に喜びを感じながら業務をしておりました。
都度わからないことが発生した際には会社ブログやQiitaを利用してアウトプットすることで知識の定着化を図りました。
詳細設計を作成する際の設計根拠などを記述する際やPMやPLに対してなぜ、この設計にしたのか?を根拠を持って説明する必要があったため、ブログへのアウトプット数もかなり増えました。
以下はAWS Network Firewallに関するブログ
以下はEKS (特にKubernetesに関する概念が全く分からなかったため、定期的にアウトプットをしていきました)
Kubernetes関連記事
自分以外のメンバーが設計している箇所についても内容がわからないのが嫌だったこともあり、今でもブログにアウトプットを続けています。
詳細設計において意識した点としては、上記の通り、自分があまり触ったことのないサービスについてはドキュメントだけを読んで説明できる自信がなかったため、都度アウトプットをすることで知識の定着化を図り、レビューに臨むようにしました。
結果的にPM、PLの方にも納得してレビューを通していただけることが多かったです。
次に構築のフェーズに移るわけでが、詳細設計の途中のフェーズから現プロジェクトのPM、PLの方から別案件のプロジェクトでDockerイメージのAmazon Linux2(以下AL2)からAmazon Linux2023(以下AL2023)へのVersionUpおよびLambdaのPythonランタイムを3.8から3.12へのVersionUp対応について対応してほしい旨の打診があり、二つ返事でやります、と回答しました。
勉強になるなら、と思いプロジェクトの工数は調整してもらいながら平行して案件を対応しました。
別案件プロジェクト
先に説明した通り、本プロジェクトでAmazon LinuxとPythonのバージョンアップ対応がメインのプロジェクトとなります。
Pythonに関してはCloudFormationで管理しているRuntimeのVersionを修正し、CodeBuildを走らせることで対応は完了になるので特に何ら難しいといったことはありませんでした。
苦労したのはAL2 -> AL2023へのVerionアップ対応の方です。
2と2023でそもそも何が異なっているのか?をAWS公式の情報から洗い出し、
Dockerfileの情報を更新し、検証用のEC2でBuildがうまくいく行くことを確認した上で、実環境のECRにPushという作業でしたが、このBuildがなかなかうまくいかない。
前提としてAL2とAL2023ではベースOSが異なることから、提供されているパッケージリポジトリも違うということで、拾ってこれるパッケージが異なります。
一例ですが、以下にブログでまとめたものがありますので詳細ははこちらを参照いただければと思います。
こういったことを一つ一つ潰してBuildが通ること、Buildが問題なく通ったら想定したプロセスが動いていること、ログローテートが動ていること、などを確認し、問題なければすでに構築されているCodePipeline, CodeCommit, CodeBuildで構築されたCI/CDでデプロイを実施し、ECSタスク定義のリビジョン確認、LBの正常確認で問題なくECSタスクが稼働していることを確認でき無事にリリースすることができました。
私の場合、Dockerfileのどこでこけているかを確認するために1行1行コメントアウトを外していき、docker container build
を実行してエラーで引っ掛かるところを確認していましたが、効率的なやり方があればご教示いただきたいです(もちろんエラー文からどこの行でこけているかを確認していますが、エラーの内容から該当行が原因ではない場合などもあるため、地道に確認していました)
このプロジェクトにおいてLinuxの知識が改めて重要だなと感じましたので、以下の書籍を購入しました。
このプロジェクトから新たにアサインされたエンジニアの方がコンテナを触るのは初めてという方だったこともあり、以下の書籍を紹介しました。
実務をしながら「このコマンドの意味」などを解説しながら教育していく、ということを自然とやっていました。(というかこのプロジェクトは少数規模でそのエンジニアの方と基本マンツーでの対応だったこともあり、即戦力としてやってもらうためにはペアプロ的な感じでやっていった方が効率が良いと感じたからというのもあります。)
結果的にその方も途中からDockerの概念をだいぶ理解されたようで、ある程度自分でタスクをこなせるようになっていったので良かったとともに、自分が人を教育する、というスキルとちゃんと言語化して伝えることで自分自身の成長にも繋がったと思っています。
個人的にDockerをこれから扱おうという方にとってはこの本は神本だと思っているので、Docker書籍で何を購入しようか悩んでる方はおすすめです。
全てのページが色付きページで図解はもちろんのこと、細かいコマンドのオプションの詳細説明がされているので、意味の分からなかったショートオプションをあえてロングオプション表記していることでコマンドの意味を理解し易く解説しています。
IaC構築 (開発)
別案件プロジェクトの方が落ち着いてきたため、元のプロジェクトの方に戻り、今度はIaC構築のフェーズです。
本プロジェクトではAWSリソースはCloudFormationで構築を行います。
CloudFormationの構築タスクでは以下を担当しました。
- Amazon VPC
- AWS Internet Gateway
- Subnet
- AWS Nat Gateway
- VPC Endpoint
- Route Table
- Security Group
- Network ACL
- AWS Network Firewall
- CloudWatch Logs
- Amazon S3
- AWS Key Management Service (KMS)
- AWS EKS
- Amazon ECR
- Amazon EFS
ネットワーク周りのタスクを任されたこともあり、CloudFormationのデプロイのトップバッターは自分になるため、自分以外のメンバーがCloudFormationのコードを記述する際の共通の規約が必要と感じたため、簡易的なCloudFormationの規約を考えながらコードの作成をしていきました。
具体的には、
- 共通パラメータの定義
- Conditionsの定義
- OutPutsの定義
などです。
共通パラメータの定義
基本設計時点で命名規則は定義しているため、その定義に沿ってCloudFormationでどのようにパラメータ名を記述していくか、などを一覧化しました。
以下例です。
カテゴリ | パラメータ | 値 |
---|---|---|
種別 | Prefix | aws |
環境種別 | Environment | d, p, s(dev, prod, stgの略) |
面数 | FaceNumber | 01 |
これはほんの一部ですが、実際に使用しているパラメータではありません。
これら共通パラメータを規約に定義しておき、必ずこのパラメータ値 + AWSリソース名に沿った名称をNameに定義することで統一化を図りました。
Parameters:
Prefix:
Type: String
Description: Specify System Short Name
ConstraintDescription: "Error: String too long or too short."
AllowedPattern: ^[a-zA-z0-9]+$
MaxLength: 10
Default: aws
Environment:
Type: String
Description: Specify Environment Identifer
ConstraintDescription: "Error: String too long or too short."
AllowedPattern: ^[a-z]
MinLength: 1
MaxLength: 1
Default: d
AllowedValues:
- d
- p
- s
FaceNumber:
Type: String
Description: Specify FaceNumber Identifer
ConstraintDescription: Numeric characters only, maximum 2 characters
AllowedPattern: ^0[0-9]{1,1}$|^[1-9][0-9]{0,1}$
MinLength: 1
MaxLength: 2
Default: '01'
Conditionsの定義
各環境ごとにyamlを準備して開発環境はこのyamlを使います、本番環境はこのyamlを使います、と準備をしたら大変だと思いませんか?
そこでこのConditionsを使用することで1つのyamlで各環境で作成するリソースやリソースの設定値を定義することができます。
PM、PLと協議しどうやったら一番効率よくCloudFormationテンプレートの作成ができるかを話し合い、Conditions
を使って一つにyamlでまとめる方が管理もしやすいし、修正が発生した際にも修正漏れを最小限にできるからこの方式で行こうとなりました。
最初はConditions
なんか個人では触ったことないし、意味も良くわからんけど、と思ってましたが、人は慣れるもので使い始めればめちゃくちゃ便利な代物であることを認識していきました。
そもそもConditionsとは?
簡単に言うとこのリソースは開発環境では必要ないけど、本番環境では必要であったり、
このリソースのこの設定値は開発環境では有効にするけど、本番環境では無効にしたい、といった場合に活用できる
セクション句です。
具体例を見てみましょう。
## ::PARAMETERS::
## Template parameters to be configured
Parameters:
Environment:
Type: String
Description: Specify Environment Identifer
ConstraintDescription: "Error: String too long or too short."
AllowedPattern: ^[a-z]
MinLength: 1
MaxLength: 1
Default: d
AllowedValues:
- d
- p
- s
## ::CONDITIONS::
## Determine if stack can be created
Conditions:
isDev: # 開発の場合に作成
!And
- !Equals [!Ref Environment, "d"]
- !Not [!Equals [!Ref "AWS::Region", "ap-northeast-3"]]
isStg: # 検証の場合に作成
!And
- !Equals [!Ref Environment, "s"]
- !Not [!Equals [!Ref "AWS::Region", "ap-northeast-3"]]
isProd: # 本番の場合に作成
!And
- !Equals [!Ref Environment, "p"]
- !Not [!Equals [!Ref "AWS::Region", "ap-northeast-3"]]
上記Conditions
で定義しているコードにisDev
, isStg
, isProd
と用意しています。
リソースセクションで各AWSリソースの定義をする際にCondition: isDev
と記述されたものは開発環境でしかリソースを作成しません。
同様に、isStg
, isProd
と記述されたものは検証環境もしくは本番環境でしかリソースを作成しません。
以下の意味はParameters
セクションで定義しているParameters.Environment
の値と密接に関係しています。
Conditions:
isProd: # 本番の場合に作成
!And
- !Equals [!Ref Environment, "p"]
- !Not [!Equals [!Ref "AWS::Region", "ap-northeast-3"]]
!Ref Environment, "p"
で、Parametersセクションで定義している、Environment
の値がp
である場合、かつデプロイするリージョンが大阪リージョンではない場合には対象リソースを作成してくださいね、という意味になります。
"大阪リージョンではない場合"というのは、東京リージョンで作成するべきリソースを誤って大阪リージョンで作成しないための防止策です。(大阪リージョンというのは例で災対環境を大阪リージョンに作成するため、そこを取り違えないためとなります。例えば災対環境が別のリージョンであれば、そのリージョンに作成したくない場合にここの値を対象リージョンの値に変更するだけです)万が一このCloudFormationテンプレートを大阪リージョンでデプロイしてしまったとしても、"大阪リージョン"であるという条件に引っ掛かり、リソースが作成されることはありません。
以下のS3バケットを作成するリソース定義の中に
S3Bucket:
Type: AWS::S3::Bucket
Condition: isProd # 本番
Properties:
BucketName: !Sub "${Prefix}-${Environment}${FaceNumber}-s3-logbucket-${AWS::AccountId}"
Condition: isProd
を定義していることから、このリソースは本番環境のみ作成したいものであることがわかります。
また、BucketName: !Sub "${Prefix}-${Environment}${FaceNumber}-s3-logbucket-${AWS::AccountId}"
はS3バケットの名前を定義するコードですが、${Environment}
の値によって環境別にS3バケットを作成するかどうかをCondition
で判断できます。
Parameters
で定義している変数の値を当てはめてみると、
${Prefix}-${Environment}${FaceNumber}-s3-logbucket-${AWS::AccountId}
= aws-p-01-s3-logbucket-アカウントID
となります。
つまり、${Environment}
= p
であることから、以下の条件にマッチし、本番環境でのみリソースを作成する、ということになります。
- !Equals [!Ref Environment, "p"]
!Sub
などの組み込み関数の意味については以下のおつまみさんの記事でわかりやすく解説していますのでこちらを参照いただくと良いかと思います。
上記のConditions
の定義によって、各環境のyamlをわざわざ準備することなくIaC化することが可能となりました。
(さらに細かいConditions
の使い方については別記事にて解説できたらと思います)
上記例ではリソースごとに作成する、しないの判断を記述していましたが、リソース内の設定値を環境によって変えることもできます。
また、リソースによってはConditions
で全てを解決することが不可能なものも出てくるので、そういった場合にはテンプレート自体を環境別に分けるなど柔軟な対応は必要となります。
OutPutsの定義
CloudFormationで様々なリソースを作成しようとすると一つのテンプレートで全てのコードを盛り込むと一つのテンプレートの中にものすごい量のコードになってしまいます。
そうなると、メンテナンス性に欠けることからテンプレートはカテゴリごとに分けて管理する必要があります。
例えば、ネットワーク関連、セキュリティ関連、サーバ関連、DB関連といった具合です。
これらカテゴリごとにテンプレートを分けてスタックを作成する際に必要となるのがOutPuts
です。
いわゆるクロススタック参照というやつで、セキュリティスタックのテンプレートでネットワークスタックで定義しているリソースの値を参照したい場合は、ネットワークスタックで値をOutPuts
し、セキュリティスタックでImportValue
する必要があります。
このOutPuts
の値をプロジェクトで統一しておく必要があります。
統一しておかないと、担当者ごとでOutPuts
の値が異なれば、ImportValue
をしてリソースを作成しようとする際にどんな値でOutPuts
しているのがわからないため、一々確認しなければいけない羽目になります。
そのため、OutPuts
する値はリソースで定義したName
やTags.Name
の値と統一することをルール化することで、担当者ごとにぶれることがなくなりました。
具体例
OutPuts
側
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
Tags:
- Key: Name
Value: !Sub "${Prefix}-${Environment}${FaceNumber}-vpc"
Outputs:
VpcId:
Value: !Ref Vpc
Export:
Name: !Sub "${Prefix}-${Environment}${FaceNumber}-vpc"
Description: Vpc Id
VPCリソースで定義した${Prefix}-${Environment}${FaceNumber}-vpc
とExport.Nameで外だししている${Prefix}-${Environment}${FaceNumber}-vpc
を一致させています。
ImportValue
側
SecurityGroupEc2Zabbix:
Type: AWS::EC2::SecurityGroup
Condition: isProd
Properties:
GroupName: !Sub "${Prefix}-${Environment}${FaceNumber}-ec2-zabbix"
GroupDescription: Security group for ec2 zabbix
VpcId:
Fn::ImportValue: !Sub "${Prefix}-${Environment}${FaceNumber}-vpc"
Tags:
- Key: Name
Value: !Sub "${Prefix}-${Environment}${FaceNumber}-ec2-zabbix"
セキュリティグループリソースでVPC Idが必要なため、ImportValue
とSub
を組み合わせてOutPuts
の値を参照できるようにしています。
クロススタック参照はクロスリージョンでの参照はできないため注意が必要です。
バージニア北部リージョンで作成したスタックでOutPuts
した値を東京リージョンでImportValue
して参照することはできません。
クロススタックで特定のリソースの内容を参照したい場合には、直接対象リソースのARN値などをコードに記述するなどが必要となります。
CloudFormationにおいて最も苦労するところ
CloudFormationで最も厄介なのが、スタックの分割方法です。
上記のOutPuts
の説明を見てわかる通り、AWSリソースは色々なリソースに依存しているため、Aテンプレートを先にデプロイしないと、Bテンプレートをデプロイできない、といった問題が浮上します。
例えば、先ほどのセキュリティグループのリソースであれば、VPCのリソースが先にデプロイされていないと、セキュリティグループのリソースでVPC Idを参照できずスタック作成が失敗してしまう、といった具合です。
ほんの一例ですが、図解すると以下のような感じで依存関係がありますので、どれが、どれに依存しているから、〇〇テンプレートを1番にデプロイして、〇〇テンプレートを2番にデプロイして、と順番を考える必要があります。
逆に言うと依存関係のないスタックはいつ流しても良いため、デプロイする順番の考慮から除外します。
以下DevelopersIOにどのようにスタックを分けると良いか?の例も解説されているので参考になるかと思います。
CloudFormationテンプレートの準備期間が1ヵ月と少しという限られた時間だったため、非常に大変でした。
かなり規模の大きい構成だったこともあり単純に物量が多いため、コードの書く量も多かったです。
また、実際にデプロイして失敗した場合には一つ一つ潰していく必要があるため経験値は取得できたもののこの時期はCloudFormationが嫌いになりそうでした(笑)
VScodeにcfn-lint
パッケージをインストールできなかったということもあり(プロジェクトの縛りプレイあるある)、コードの記述誤りをエディタ上からは発見しづらかったのも相まってこの点は少し効率が悪かったです。
単体テストのフェーズについては構築したリソースが正しいかどうかの確認作業のため、特に特別共有できることがないため省略します。
最後に
AWS設計構築に参画して1年でやってきたことの内容をまとめてみました。
プロジェクトが始まる前は果たして自分にこのプロジェクトについていけるか心配でしたが、幸いにもPM, PL他メンバーの方のも恵まれた環境だったため設計や構築を進める上で相談しやすい環境だったためここまで来ることができました。
まだまだ知らないことや経験していきたいことは山ほどあるため、本プロジェクトで学んだことを次のプロジェクトでも生かせるようにアウトプットを継続していきたいと思います