調べ物した結果

現役SEが仕事と直接関係ないことを調べた結果とか感想とか

ECS(Fagete)でサイドカーつかってログ出力する

よーわからん。複数出すのが最終目標だけどそもそも単品すら出ないからまとめ

説明はしょるところ

大方端折るけど、大体以下は知っている前提

  • AWS VPC関連
  • ECSの用語(タスクとかタスク定義とかサービスとか)
  • Dockerのコマンド
  • CloudFormation
  • シェルのコマンド

必要なもの

ゼロから作るの面倒。大きく5STEPぐらいで。

  • STEP1:タスクの実行環境を作る
  • STEP2:アプリケーション、ログのコンテナイメージを作る
  • STEP3:タスク定義を作る
  • STEP4:タスクを実行する
  • STEP5:ログを確認する

STEP1:タスクの実行環境を作る

コンテナの実行環境(ECS Fagete)が動く環境を作る。
動く環境を作るのもそこそこ面倒。以下のリソースが最低限いる。

環境整えるのに最低これぐらいいる。面倒。
CloudFormationにパクパクさせよう。

AWSTemplateFormatVersion: "2010-09-09"
Description: "ECS Task Working Environment with VPC, ECS Cluster, Subnets, Endpoints, and ECR without IGW"

Parameters:
  VPCName:
    Description: "The name of the VPC"
    Type: String
    Default: "ECSVPC"

  ClusterName:
    Description: "The name of the ECS Cluster"
    Type: String
    Default: "ECSCluster"

  ECRRepositoryName:
    Description: "The name of the ECR Repository"
    Type: String
    Default: "ecs-ecr-repo"

  PrivateSubnet1CIDR:
    Description: "The CIDR block for the private subnet"
    Type: String
    Default: "10.0.1.0/24"

Resources:
  # VPC
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: "Name"
          Value: !Ref VPCName

  # Private Subnet
  PrivateSubnet1:
    Type: "AWS::EC2::Subnet"
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PrivateSubnet1CIDR
      AvailabilityZone: !Select [0, !GetAZs ""]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: "Name"
          Value: "PrivateSubnet1"

  # ECR Repository
  ECRRepository:
    Type: "AWS::ECR::Repository"
    Properties:
      RepositoryName: !Ref ECRRepositoryName

  # ECS Cluster
  ECSCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: !Ref ClusterName

  # VPC Endpoints for ECR
  VPCEndpointECR:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ecr.api"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref PrivateSubnet1
      SecurityGroupIds:
        - !Ref SecurityGroup
      Tags:
        - Key: "Name"
          Value: "for-ecs-cluseter-ecr-api-endpoint"           

  VPCEndpointECRDkr:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ecr.dkr"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref PrivateSubnet1
      SecurityGroupIds:
        - !Ref SecurityGroup
      Tags:
        - Key: "Name"
          Value: "for-ecs-cluseter-ecr-dkr-endpoint"        

  # VPC Endpoint for S3 (Gateway Endpoint)
  VPCEndpointS3:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      VpcId: !Ref VPC
      RouteTableIds:
        - !Ref PrivateRouteTable
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
      VpcEndpointType: Gateway
      Tags:
        - Key: "Name"
          Value: "for-ecs-cluseter-s3-endpoint"         

  # VPC Endpoint for CloudWatch Logs (Interface Endpoint)
  VPCEndpointCloudWatchLogs:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.logs"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref PrivateSubnet1
      SecurityGroupIds:
        - !Ref SecurityGroup
      Tags:
        - Key: "Name"
          Value: "for-ecs-cluseter-logs-endpoint"
  # Security Group
  SecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "Allow all inbound and outbound traffic"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: -1
          CidrIp: "0.0.0.0/0"
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: "0.0.0.0/0"
      Tags:
        - Key: "Name"
          Value: "ECS-SecurityGroup"

  # Route Table for Private Subnet
  PrivateRouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: "Name"
          Value: "PrivateRouteTable"

  PrivateSubnetRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

Outputs:
  VPCId:
    Description: "The ID of the created VPC"
    Value: !Ref VPC

  SubnetId:
    Description: "The ID of the created private subnet"
    Value: !Ref PrivateSubnet1

  ECSClusterName:
    Description: "The name of the ECS Cluster"
    Value: !Ref ECSCluster

  ECRRepositoryURL:
    Description: "The URL of the ECR Repository"
    Value: !GetAtt ECRRepository.RepositoryUri

STEP2:アプリケーション、ログのコンテナイメージを作る

動作確認用のコンテナイメージをECRに登録する
もうあればいいけど用意不要。
ちょっと試そうと思ってるだけなのにコンテナイメージが2つもいる。

以下3ファイルをCloudShellにアップロードして、シェル実行すれば上で作ったECRにイメージが登録される。
Windowsの場合は改行コードにきをつけて。LFじゃないと暴れるよ。
※Dockerfileの名前に気を付けて。決め打ちしてるから。
※chmodでシェルに実行権限与えないとうごかない。

  • 適当なWEBアプリ。Dockerfile.flask

標準出力に何かはかないとまともにログが取れない(らしい)ので標準出力に何か吐いてさえいればよい。

# ベースイメージとしてPythonを使用
FROM python:3.9-slim

# Flaskをインストール
RUN pip install flask

# FlaskアプリケーションのコードをDockerfile内で直接埋め込む
RUN echo "\
from flask import Flask\n\
import logging\n\
import sys\n\
\n\
app = Flask(__name__)\n\
\n\
# ロギングの設定(標準出力に出力)\n\
app.logger.setLevel(logging.INFO)  # ログレベルをINFOに設定\n\
handler = logging.StreamHandler(sys.stdout)\n\
handler.setLevel(logging.INFO)\n\
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')\n\
handler.setFormatter(formatter)\n\
app.logger.addHandler(handler)\n\
\n\
# Werkzeugのロガーも設定\n\
werkzeug_logger = logging.getLogger('werkzeug')\n\
werkzeug_logger.setLevel(logging.INFO)\n\
werkzeug_logger.addHandler(handler)\n\
\n\
@app.route('/')\n\
def hello():\n\
    app.logger.info('Hello, World! accessed')\n\
    return 'Hello, World!'\n\
\n\
@app.route('/test')\n\
def test():\n\
    app.logger.info('Test endpoint accessed')\n\
    return 'This is a test endpoint'\n\
\n\
if __name__ == '__main__':\n\
    app.run(host='0.0.0.0', port=80)\n\
" > /app.py

# ポート80を開放
EXPOSE 80

# Flaskアプリケーションを実行
CMD ["python", "/app.py"]
" > /app.py

# ポート80を開放
EXPOSE 80

# Flaskアプリケーションを実行
CMD ["python", "/app.py"]
  • 適当設定のFluent。Dockerfile.fluent
# Use Fluent Bit as the base image
FROM amazon/aws-for-fluent-bit:latest

# Create the fluent-bit.conf file directly in the Dockerfile without the INPUT section
RUN echo '[SERVICE] \
    Flush        1 \
    Daemon       Off \
    Log_Level    info \
[OUTPUT] \
    Name cloudwatch_logs \
    Match * \
    region ap-northeast-1 \
    log_group_name fluent-bit-log-group \
    log_stream_prefix from-fluent-bit- \
    auto_create_group true' > /fluent-bit/etc/fluent-bit2.conf
  • イメージの登録用シェル
#!/bin/bash
############
# $1:account id
############

if [ -z "$1" ]; then
    echo "Error: Please specify the first argument as the account ID."
    exit 1
fi

aws ecr create-repository --repository-name ecs-webapp
aws ecr create-repository --repository-name ecs-fluentbit

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin "$1.dkr.ecr.ap-northeast-1.amazonaws.com"

docker build -t ecs-webapp -f Dockerfile.flast.
docker build -t ecs-fluentbit -f Dockerfile.fluentbit .

docker tag ecs-webapp:latest "$1.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-webapp:latest"
docker tag ecs-fluentbit:latest "$1.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-fluentbit:latest"

docker push "$1.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-webapp:latest"
docker push "$1.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-fluentbit:latest"

うまくいってたらこんな感じでECRにイメージが登録される

STEP3:タスク定義を作る

STEP2のイメージを使うようにタスクを定義を作る。
※差し替えが手間だからやってないが。:latestがURIから抜けてる。ECRからちゃんとイメージとれないので注意
とりあえずコンソールからGUIで作ったけど、途中どうしてもJSON直接編集しないといけなかったら一回つくってJSONしました。
ここが一番困りました。いろんな記事があって正解どれやねん状態になってきつかった。





これでうまくいけばいいけど。設定ファイルの差し込みがいるらしい。これもGUIでやれればいいけどできないっぽい。
docs.aws.amazon.com
ので、Jsonをいじります。

]

これでタスク定義は完了。

参考までにJSON全体もおいておく。
自分の環境に読み替えが発生するところがあるので、
上の手順とGUIを照らし合わせたほうが、手順はわかりやすい。
※AccountIDとか、イメージ別のもの使ってたりしたら差し替えがいります。

{
    "family": "tekitou",
    "containerDefinitions": [
        {
            "name": "web-app",
            "image": "[AWS::AccountId].dkr.ecr.ap-northeast-1.amazonaws.com/ecs-webapp:latest",
            "cpu": 512,
            "portMappings": [
                {
                    "name": "80",
                    "containerPort": 80,
                    "hostPort": 80,
                    "protocol": "tcp",
                    "appProtocol": "http"
                }
            ],
            "essential": true,
            "environment": [],
            "mountPoints": [],
            "volumesFrom": [],
            "startTimeout": 10,
            "logConfiguration": {
                "logDriver": "awsfirelens",
                "options": {}
            },
            "systemControls": []
        },
        {
            "name": "log_router",
            "image": "[AWS::AccountId].dkr.ecr.ap-northeast-1.amazonaws.com/ecs-fluentbit:latest",
            "cpu": 0,
            "memoryReservation": 51,
            "portMappings": [],
            "essential": true,
            "environment": [],
            "mountPoints": [],
            "volumesFrom": [],
            "user": "0",
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/ecs-aws-firelens-sidecar-container",
                    "mode": "non-blocking",
                    "awslogs-create-group": "true",
                    "max-buffer-size": "25m",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "firelens"
                }
            },
            "systemControls": [],
            "firelensConfiguration": {
                "type": "fluentbit",
                "options": {
                    "config-file-type": "file",
                    "config-file-value": "/fluent-bit/etc/fluent-bit2.conf"
                }
            }
        }
    ],
    "taskRoleArn": "arn:aws:iam::[AWS::AccountId]:role/ecsTaskExecutionRole",
    "executionRoleArn": "arn:aws:iam::[AWS::AccountId]:role/ecsTaskExecutionRole",
    "networkMode": "awsvpc",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "1024",
    "memory": "3072",
    "runtimePlatform": {
        "cpuArchitecture": "X86_64",
        "operatingSystemFamily": "LINUX"
    }
}

STEP4:タスクを実行する

STEP1で作った環境にSTEP3で作ったタスク定義からタスクを作ります。



ここまではデフォルトでOK
ネットワーク設定はSTEP1で作った環境をつかおう。SGはがばがばだからがばがばで。

タスクがRunningになればOK

STEP5: ログを確認する

うまくいってるとサイドカー側のコンテナログにこんな感じでる。


設定ファイルがおかしいとこんな感じのログが出る。

そもそもだめだとコンテナの起動ログしかでない。この辺で切り分ける。

設定ファイルで指定したほうもでてる。

とりあえず出た。おわり。

途中困って解決したやつ

  • ECRのURI間違えてた。

タスク起動時にECRにアクセスできずにタスクがずっとこけてた。よくみたらURI間違ってた(タグ指定してなかった)

  • Endpointの設定間違えてた。

ECRにアクセスできない事象の原因2つめ。エンドポイントのPrivate DNSの名前解決を有効にしてなかった。

  • Fluent-bitの設定箇所にログがでてくれない

原因がよくわかってない。
「設定ファイルをDockerfileでインライン記述していたのを直してちゃんと改行するようにした」
とか
「[INPUT]セクションとか、ネットの記事をそのままパクッてきたので意味わかってない記述を排除した」
とかそのあたりが原因だったと思われる。シンプルな構成にしたらなんかちゃんと動くようになった。

  • 吐いたはずのアプリログがでておらんかった。

Flastのログレベルが標準だとWARNINGなのでそのせいらしい。Dockerfile調整してなおった。

なんかログはでてたけどこんな感じで


たとえばflaskでは

app.logger.info('Hello, World! accessed')\n\

としてるのでこれがでてほしいのだけれど、でとらんかった。