Automate Laravel Deployment to AWS ECS with AWS CDK and CodePipeline
In the first part of this tutorial series, we manually deployed a PHP (Laravel) application to Amazon ECS. In this second part, we'll automate the entire deployment process using AWS Cloud Development Kit (CDK), CodePipeline, and CodeBuild. This will enable continuous integration and continuous deployment (CI/CD) for your application, ensuring that every code change is automatically built, tested, and deployed.
Prerequisites
Before we begin, make sure you have the following:
- AWS Account: An AWS account with permissions to create resources like IAM roles, ECS clusters, and CodePipeline.
- AWS CLI: Installed and configured with your AWS credentials.
- AWS CDK: Installed globally using
npm install -g aws-cdk
. - Git Repository: Your Laravel application's code pushed to a Git repository (e.g., GitHub, CodeCommit).
- Docker: Installed and running on your development machine.
Overview
We'll accomplish the following steps:
- Set Up AWS CDK Project: Initialize a new CDK project in TypeScript.
- Define Infrastructure with CDK: Use CDK to define AWS resources like ECR repository, ECS cluster, task definitions, and services.
- Set Up CodeBuild Project: Configure CodeBuild to build your Docker image and push it to ECR.
- Set Up CodePipeline: Create a pipeline that triggers on code commits, builds the Docker image, and deploys it to ECS.
- Deploy CDK Stack: Deploy the CDK stack to create the resources in your AWS account.
- Test the Deployment: Verify that your application is automatically deployed upon code changes.
Step 1: Set Up AWS CDK Project
Initialize a New CDK Project
Create a new directory for your CDK project and initialize it:
mkdir laravel-ecs-cdk
cd laravel-ecs-cdk
cdk init app --language typescript
This will generate a basic CDK project structure with TypeScript.
Install Required CDK Libraries
Install the necessary CDK libraries for ECS, ECR, CodePipeline, and CodeBuild:
npm install @aws-cdk/aws-ecs @aws-cdk/aws-ecr @aws-cdk/aws-ec2 @aws-cdk/aws-iam @aws-cdk/aws-codepipeline @aws-cdk/aws-codepipeline-actions @aws-cdk/aws-codebuild @aws-cdk/aws-elasticloadbalancingv2
Step 2: Define Infrastructure with CDK
Open the lib/laravel-ecs-cdk-stack.ts
file and start defining your infrastructure.
Import Required Modules
At the top of your stack file, import the required modules:
import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as ecs from '@aws-cdk/aws-ecs';
import * as ecr from '@aws-cdk/aws-ecr';
import * as iam from '@aws-cdk/aws-iam';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';
Define VPC and ECS Cluster
Create a VPC and an ECS cluster within it:
export class LaravelEcsCdkStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create VPC with public and private subnets
const vpc = new ec2.Vpc(this, 'LaravelVpc', {
maxAzs: 2,
});
// Create ECS Cluster
const cluster = new ecs.Cluster(this, 'LaravelCluster', {
vpc: vpc,
});
}
}
Create ECR Repository
Define an ECR repository to store your Docker images:
// Create ECR Repository
const repository = new ecr.Repository(this, 'LaravelRepository', {
repositoryName: 'laravel-app',
});
Define IAM Roles
Create an IAM role for CodeBuild to access AWS services:
// IAM Role for CodeBuild
const codeBuildRole = new iam.Role(this, 'CodeBuildRole', {
assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
});
// Grant permissions to CodeBuild Role
repository.grantPullPush(codeBuildRole);
Define ECS Task Definition and Service
Set up the ECS task definition and service:
// Define Task Role and Execution Role
const taskExecutionRole = new iam.Role(this, 'TaskExecutionRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'),
],
});
// Define Task Definition
const taskDefinition = new ecs.FargateTaskDefinition(this, 'LaravelTaskDef', {
memoryLimitMiB: 512,
cpu: 256,
executionRole: taskExecutionRole,
});
// Add Container to Task Definition
const container = taskDefinition.addContainer('LaravelContainer', {
image: ecs.ContainerImage.fromEcrRepository(repository),
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'LaravelApp' }),
});
// Expose Port 80
container.addPortMappings({
containerPort: 80,
});
// Create Security Group for the Service
const serviceSecurityGroup = new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {
vpc,
description: 'Allow HTTP traffic',
allowAllOutbound: true,
});
serviceSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP');
// Create Fargate Service
const service = new ecs.FargateService(this, 'LaravelService', {
cluster,
taskDefinition,
desiredCount: 1,
securityGroups: [serviceSecurityGroup],
});
Configure Application Load Balancer
Set up an Application Load Balancer to distribute traffic:
// Create ALB
const lb = new elbv2.ApplicationLoadBalancer(this, 'LaravelALB', {
vpc,
internetFacing: true,
});
// Add Listener on Port 80
const listener = lb.addListener('PublicListener', {
port: 80,
open: true,
});
// Attach the Service to the Load Balancer
listener.addTargets('LaravelTarget', {
port: 80,
targets: [service],
healthCheck: {
path: '/',
interval: cdk.Duration.seconds(60),
},
});
// Output the Load Balancer DNS
new cdk.CfnOutput(this, 'LoadBalancerDNS', {
value: lb.loadBalancerDnsName,
});
Step 3: Set Up CodeBuild Project
Create a CodeBuild project to build your Docker image:
// Define CodeBuild Project
const codeBuildProject = new codebuild.PipelineProject(this, 'CodeBuildProject', {
role: codeBuildRole,
environment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
privileged: true, // Required to build Docker images
},
environmentVariables: {
'REPOSITORY_URI': { value: repository.repositoryUri },
},
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
pre_build: {
commands: [
'echo Logging in to Amazon ECR...',
'aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $REPOSITORY_URI',
],
},
build: {
commands: [
'echo Build started on `date`',
'echo Building the Docker image...',
'docker build -t $REPOSITORY_URI:latest .',
'docker push $REPOSITORY_URI:latest',
],
},
post_build: {
commands: [
'echo Build completed on `date`',
],
},
},
}),
});
Step 4: Set Up CodePipeline
Create a CodePipeline to automate the build and deployment process.
Define Source Artifact
Specify the source of your code:
// Source Artifact
const sourceOutput = new codepipeline.Artifact();
Configure Source Action
Set up the source action to pull code from your Git repository. For example, if you're using GitHub:
// GitHub Source Action
const sourceAction = new codepipeline_actions.GitHubSourceAction({
actionName: 'GitHub_Source',
owner: 'your-github-username',
repo: 'your-laravel-repo',
oauthToken: cdk.SecretValue.secretsManager('github-token'),
output: sourceOutput,
branch: 'main',
});
Note: You need to store your GitHub OAuth token in AWS Secrets Manager with the name
github-token
.
Define Build Artifact
Specify the build output:
// Build Artifact
const buildOutput = new codepipeline.Artifact();
Configure Build Action
Set up the build action:
// CodeBuild Action
const buildAction = new codepipeline_actions.CodeBuildAction({
actionName: 'CodeBuild',
project: codeBuildProject,
input: sourceOutput,
outputs: [buildOutput],
});
Update ECS Service on Deployment
Create an action to update the ECS service when a new image is pushed:
// Deploy Action
const deployAction = new codepipeline_actions.EcsDeployAction({
actionName: 'DeployAction',
service: service,
input: buildOutput,
});
Create the Pipeline
Assemble the pipeline:
// Define Pipeline
const pipeline = new codepipeline.Pipeline(this, 'LaravelPipeline', {
pipelineName: 'LaravelPipeline',
restartExecutionOnUpdate: true,
});
// Add Stages to Pipeline
pipeline.addStage({
stageName: 'Source',
actions: [sourceAction],
});
pipeline.addStage({
stageName: 'Build',
actions: [buildAction],
});
pipeline.addStage({
stageName: 'Deploy',
actions: [deployAction],
});
Step 5: Deploy CDK Stack
Bootstrap CDK
If you haven't bootstrapped your AWS environment for CDK, do it now:
cdk bootstrap aws://ACCOUNT_ID/REGION
Replace ACCOUNT_ID
and REGION
with your AWS account ID and desired region.
Deploy the Stack
Deploy the CDK stack:
cdk deploy
This command will list the resources to be created and ask for confirmation. Type y
to proceed.
Step 6: Test the Deployment
Push Code Changes
Make a code change in your Laravel application and push it to your Git repository's main branch.
Verify CodePipeline Execution
Navigate to the AWS CodePipeline console and select your pipeline. You should see a new execution triggered by your code change.
Check CodeBuild Logs
In the CodePipeline execution, click on the CodeBuild action to view the build logs. Ensure that the build succeeds and the Docker image is pushed to ECR.
Verify ECS Service Update
Once the deployment stage is reached, the ECS service should automatically update with the new Docker image.
Access Your Application
Obtain the DNS name of your Application Load Balancer from the CloudFormation outputs or the AWS console. Visit the URL in your browser to see your updated application.
Additional Configurations
HTTPS Support
For HTTPS support, you need to:
- Obtain an SSL/TLS Certificate: Use AWS Certificate Manager (ACM) to get a certificate for your domain.
- Update the Load Balancer Listener: Modify the listener to use port 443 and attach the SSL certificate.
- Adjust Security Groups: Allow inbound traffic on port 443.
Environment Variables
If your Laravel application requires environment variables, you can pass them to the container in the task definition:
container.addEnvironment('APP_ENV', 'production');
container.addEnvironment('APP_KEY', 'your-app-key');
Consider using AWS Secrets Manager or SSM Parameter Store for sensitive information.
Persistent Storage with EFS
To handle Laravel's storage requirements, you can attach an EFS volume to your task definition:
// Create EFS File System
const fileSystem = new efs.FileSystem(this, 'FileSystem', {
vpc,
encrypted: true,
});
// Add Volume to Task Definition
taskDefinition.addVolume({
name: 'LaravelStorage',
efsVolumeConfiguration: {
fileSystemId: fileSystem.fileSystemId,
},
});
// Mount Point
container.addMountPoints({
containerPath: '/var/www/storage',
sourceVolume: 'LaravelStorage',
readOnly: false,
});
Conclusion
You've successfully automated the deployment of your Laravel application to AWS ECS using AWS CDK, CodePipeline, and CodeBuild. With this setup:
- Continuous Integration: Every code change is automatically built and tested.
- Continuous Deployment: Successful builds are automatically deployed to ECS.
- Infrastructure as Code: Your infrastructure is defined in code, making it repeatable and version-controlled.
Next Steps (we'll leave these for another tutorial)
- Monitoring and Logging: Integrate AWS CloudWatch for enhanced monitoring.
- Scaling: Configure auto-scaling policies for your ECS service.
- Testing: Add automated tests to your CodeBuild project.
Congratulations! You've taken a significant step towards modernizing your application's deployment process.