{"id":1788,"date":"2021-09-19T20:28:27","date_gmt":"2021-09-19T10:28:27","guid":{"rendered":"https:\/\/ntsblog.homedev.com.au\/?p=1788"},"modified":"2023-10-26T21:11:20","modified_gmt":"2023-10-26T10:11:20","slug":"deploy-nextjs-react-website-to-aws-ecs-container-via-bitbucket-pipelines","status":"publish","type":"post","link":"https:\/\/ntsblog.homedev.com.au\/index.php\/2021\/09\/19\/deploy-nextjs-react-website-to-aws-ecs-container-via-bitbucket-pipelines\/","title":{"rendered":"Deploy NextJs React website to AWS ECS container via bitbucket pipelines"},"content":{"rendered":"<div id=\"ntsbl-3407629176\" class=\"ntsbl-before-content ntsbl-entity-placement\"><script async src=\"\/\/pagead2.googlesyndication.com\/pagead\/js\/adsbygoogle.js?client=ca-pub-6288941070289539\" crossorigin=\"anonymous\"><\/script><ins class=\"adsbygoogle\" style=\"display:inline-block;width:728px;height:90px;\" \ndata-ad-client=\"ca-pub-6288941070289539\" \ndata-ad-slot=\"9356781486\"><\/ins> \n<script> \n(adsbygoogle = window.adsbygoogle || []).push({}); \n<\/script>\n<\/div>\n<p>Settle back for a big post on nextJs variables, bitbucket pipeleines and ECS containers..<\/p>\n\n\n\n<p><br>Recently I was given the challenge to work out the best way to host a NextJs React website in AWS infrastructure.<br>We chose to attempt to dockerize the react website and host this on an AWS ECS cluster using fargate.<\/p>\n\n\n\n<p><br>We had the following requirements;<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>have a CI \/ CD deployment pipeline<\/li>\n\n\n\n<li>support multiple environments, dev \/ qa \/ uat and production<\/li>\n\n\n\n<li>be highly scaleable<\/li>\n\n\n\n<li>cost effective<\/li>\n<\/ol>\n\n\n\n<p>We chose to;<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>host the website as a docker container;<\/li>\n\n\n\n<li>on a AWS ECS cluster<\/li>\n\n\n\n<li>using fargate<\/li>\n\n\n\n<li>deploying via bitbucket pipelines, we wanted to\n<ol class=\"wp-block-list\">\n<li>build the docker images once<\/li>\n\n\n\n<li>push to ECR once, but<\/li>\n\n\n\n<li>deploy the image to mutliple different environments<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n\n\n\n<p>This was difficult as there was lots of documentation but nothing that tied it all together.This post will not go into the setup of the ECS cluster, but focus on the CI \/ CD pipeline configuration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">NextJs Docker image<\/h2>\n\n\n\n<p>Dockerizing the NextJs website is very simple.I followed instructions provided on the following website;<\/p>\n\n\n\n<p><a href=\"https:\/\/medium.com\/swlh\/dockerize-your-next-js-application-91ade32baa6\">https:\/\/medium.com\/swlh\/dockerize-your-next-js-application-91ade32baa6<\/a><br><\/p>\n\n\n\n<p>Assuming you have your own nextJs website..<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Run site locally<\/h3>\n\n\n\n<p>inside you repo to test and run your site do the following<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>npm install next react react-dom<\/li>\n\n\n\n<li>To run the app in quick dev mode\n<ol class=\"wp-block-list\">\n<li>npm run dev<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>to deploy\n<ol class=\"wp-block-list\">\n<li>npm run build<\/li>\n\n\n\n<li>npm start<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Create a docker image of the site<\/h3>\n\n\n\n<p>This assumes you have docker desktop running on your computer<\/p>\n\n\n\n<p><br>Create a Dockerfile as follows in the root of your website<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># Dockerfile\n\n# base image\nFROM node:alpine\n\n# create &amp; set working directory\nRUN mkdir -p \/usr\/src\nWORKDIR \/usr\/src\n\n# copy source files\nCOPY . \/usr\/src\n\n# install dependencies\nRUN npm install\n\n# start app\nRUN npm run build\nEXPOSE 3000\nCMD npm run start<\/pre>\n\n\n\n<p>This specifies to run you site on port 3000<\/p>\n\n\n\n<p><br>build an image of you site<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">docker build -t my-website .<\/pre>\n\n\n\n<p>Run the image<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">docker run --name my-website-container -dp 5000:3000 my-website<\/pre>\n\n\n\n<p>This should show your website running on port 5000.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Bitbucket pipelines deployment<\/h2>\n\n\n\n<p>This is where it gets interesting. <br>The following is a full deployment pipeline.<\/p>\n\n\n\n<p>This is configured to do the following;<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Be triggered off the development branch<\/li>\n\n\n\n<li>perform a build and push to the docker image to ECR.<\/li>\n\n\n\n<li>automatically deploy to the development environment<\/li>\n\n\n\n<li>manually trigger&nbsp; deployment to qa<\/li>\n<\/ol>\n\n\n\n<p>Pre-requisites;<\/p>\n\n\n\n<p>AWS<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>ECS Cluster &#8211; (eg. my-web-cluser)<\/li>\n\n\n\n<li>ECS Service&nbsp; (eg &#8211; svc-my-website)<\/li>\n\n\n\n<li>ECR &#8211; elastic container repository &#8211; (eg &#8211; my-website-ecr)<\/li>\n\n\n\n<li>Task Definition &#8211; td-my-website<\/li>\n\n\n\n<li>s3 bucket &#8211; s3-bitbucket-deployment<\/li>\n<\/ol>\n\n\n\n<p>Bitbucket<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>repository for your code<\/li>\n\n\n\n<li>development branch<\/li>\n\n\n\n<li>pipelines enabled<\/li>\n\n\n\n<li>Deployments\n<ol class=\"wp-block-list\">\n<li>test environments\n<ol class=\"wp-block-list\">\n<li>dev<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>staging environments\n<ol class=\"wp-block-list\">\n<li>qa<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>prod environments\n<ol class=\"wp-block-list\">\n<li>prod<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n\n\n\n<p>In the deployment we need to setup the following &#8220;global variables&#8221;These variables are used in the pipeline file<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>s3Bucket\n<ul class=\"wp-block-list\">\n<li>the path to your s3 bucket<\/li>\n\n\n\n<li>s3:\/\/s3-bitbucket-deployment\/my-website<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>AWS_REGION\n<ul class=\"wp-block-list\">\n<li>the region you want to run in<\/li>\n\n\n\n<li>i use ap-southeast-2, so if see that in the examples change to your zone<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>AWS_ACCESS_KEY_ID\n<ul class=\"wp-block-list\">\n<li>the aws access key<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>AWS_SECRET_KEY\n<ul class=\"wp-block-list\">\n<li>the aws secret key<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>ECR_ARN\n<ul class=\"wp-block-list\">\n<li>the arn to the ECR tat you have configured<\/li>\n\n\n\n<li>{aws-account-number}.dkr.ecr.ap-southeast-2.amazonaws.com\/my-website<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>ECS_CLUSTER\n<ul class=\"wp-block-list\">\n<li>the name of your ECS cluster<\/li>\n\n\n\n<li>my-web-cluster<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>ECS_ROLE\n<ul class=\"wp-block-list\">\n<li>the role used when setting up the task definition<\/li>\n\n\n\n<li>arn:aws:iam::{aws-account-number}:role\/ecsTaskExecutionRole<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>SECURTY-GROUP\n<ul class=\"wp-block-list\">\n<li>The default security group being used for the ECS Service<\/li>\n\n\n\n<li>Note I am setting this as a default NON-Production grooup<\/li>\n\n\n\n<li>If you have different security config per environment then setup on the specific deployment environment<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>SUBNET_1\n<ul class=\"wp-block-list\">\n<li>the subnet of the ECS Service<\/li>\n\n\n\n<li>I used the 2a zone<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>SUBNET_2\n<ul class=\"wp-block-list\">\n<li>the secondary subnet of the ECS Service<\/li>\n\n\n\n<li>I used the 2c zone<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p><strong>Environment specific varaibles<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>env<\/strong>\n<ul class=\"wp-block-list\">\n<li>the name of the environment<\/li>\n\n\n\n<li>e.g. dev \/ qa \/ prod etc<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>ECS_SERVICE<\/strong>\n<ul class=\"wp-block-list\">\n<li>the name of your ecs service<\/li>\n\n\n\n<li>svc-dev-my-website<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>ECS_TASK_DEF_NAME<\/strong>\n<ul class=\"wp-block-list\">\n<li>the name of the task definition<\/li>\n\n\n\n<li>td-dev-my-website<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>Interesting points to note on these parametersIf you name the AWS Keys as above, they automatically get picked up as the AWS configuration so when you run any AWS CLI command you do NOT need to pass in the extra parameters or setup anything as it picks up these keys.<br>You can name your environments as you please, this is just the configuration for my example.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">S3 Bucket setup<\/h2>\n\n\n\n<p>I will write an entire section of the environment config for the NextJs website, suffice to say my solution is to reference the .env config from the s3 Bucket<\/p>\n\n\n\n<p>I assume you have a .env.local file for your nextJs website, so upload the following versions of it to your s3 bucket<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>.env &#8211; base definition used for the build<\/li>\n\n\n\n<li>one per environment; eg\n<ul class=\"wp-block-list\">\n<li>dev.env<\/li>\n\n\n\n<li>qa.env<\/li>\n\n\n\n<li>uat.env<\/li>\n\n\n\n<li>prod.env<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Obviously each one would contain environment specific environment settings<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Deployment Pipeline<\/h2>\n\n\n\n<p>This is a full deployment file for dev and qa. You should be able to easily add the deployment for production<\/p>\n\n\n\n<p>I will run through each section of the deployment in detail<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">pipleline syntax<\/h3>\n\n\n\n<p>syntax to understand;<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Its a yaml fie so indenting and spacing matters<\/li>\n\n\n\n<li>echo just prints output into the logs, useful for debugging and working out what is going wrong with your script.<\/li>\n\n\n\n<li>export&nbsp; VAR=&#8221;hello&#8221;\n<ol class=\"wp-block-list\">\n<li>export sets a variable that can be used<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>echo $VAR\n<ol class=\"wp-block-list\">\n<li>$VAR is how you reference your variable.<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">image: atlassian\/default-image:2\n\ndefinitions: \n    services: \n      docker: \n        memory: 2048  # docker running out of memory\n\npipelines: \n  default: \n    - step: \n        name: startup pipeline \n        script: \n          - echo \"do nothing\"\n\n  branches: \n    development:   \n      - step: \n          name: Build and publish to ECR \n          services: \n            - docker \n          caches: \n            - docker \n          artifacts: \n            - tag.txt \n          script: \n            - echo \"in build step\" \n            # setup env config \n            - echo \"setup .env configuration\" \n            - aws s3 cp $s3Bucket\/.env . \n            # setup build tags \n            - IMAGE_NAME=$BITBUCKET_REPO_SLUG \n            - export DATESTAMP=\"$(date +%Y-%m-%d)\" \n            - export TAG=\"$DATESTAMP.${BITBUCKET_BUILD_NUMBER}\" \n            - export ECR_IMAGE=\"$ECR_ARN:$TAG\" \n            - echo $TAG \n            - echo $ECR_IMAGE \n            # docker build \n            - docker build -t $IMAGE_NAME:$TAG . \n            - docker tag $IMAGE_NAME:$TAG $ECR_ARN \n            # push to ecr - pipe \n            - pipe: atlassian\/aws-ecr-push-image:1.4.2 \n              variables: \n                AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID \n                AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY \n                AWS_DEFAULT_REGION: $AWS_REGION \n                IMAGE_NAME: $IMAGE_NAME:$TAG \n                TAGS: $TAG \n            # output artifacts \n            - echo $TAG &gt; tag.txt \n      - step: \n          name: dev deployment \n          deployment: dev  \n          script:  \n            - echo \"in $env deploy step\" \n            # Get variables from build step \n            - export TAG=$(cat .\/tag.txt) \n            - echo $TAG \n            # setup aws \n            - curl \"https:\/\/awscli.amazonaws.com\/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\" \n            - unzip awscliv2.zip \n            - .\/aws\/install -b ~\/bin\/aws \n            - export PATH=~\/bin\/aws:$PATH \n            - aws --version \n            # setup version \n            - export ECR_IMAGE=\"$ECR_ARN:$TAG\" \n            - echo $ECR_IMAGE \n            - echo $ECS_TASK_DEF_NAME \n            # update task definition \n            - rm -rf task-definition.json \n            - file_contents=$(&lt; task-definition-template.json ) \n            - echo \"${file_contents\/\/ECS_TASK_DEF_NAME\/$ECS_TASK_DEF_NAME}\" &gt; task-definition.json \n            - file_contents=$(&lt; task-definition.json ) \n            - echo \"${file_contents\/\/ECR_IMAGE\/$ECR_IMAGE}\" &gt; task-definition.json \n            - file_contents=$(&lt; task-definition.json ) \n            - echo \"${file_contents\/\/ECS_SERVICE\/$ECS_SERVICE}\" &gt; task-definition.json \n            - file_contents=$(&lt; task-definition.json ) \n            - echo \"${file_contents\/\/#env\/$env}\" &gt; task-definition.json \n            - cat task-definition.json \n            # register task  \n            - export TASK_VERSION=$((aws ecs register-task-definition --execution-role-arn $ECS_ROLE --cli-input-json file:\/\/\/opt\/atlassian\/pipelines\/agent\/build\/task-definition.json) | grep revision | cut -d \":\" -f2 | cut -d \",\" -f1 | tr -d ' ') \n            - echo $TASK_VERSION \n            # update ecs service \n            - aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --task-definition $ECS_TASK_DEF_NAME:$TASK_VERSION \n      - step: \n          name: qa deployment \n          deployment: qa \n          trigger: manual \n          script:  \n            - echo \"in $env deploy step\" \n            # Get variables from build step \n            - export TAG=$(cat .\/tag.txt) \n            - echo $TAG \n            # setup aws \n            - curl \"https:\/\/awscli.amazonaws.com\/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\" \n            - unzip awscliv2.zip \n            - .\/aws\/install -b ~\/bin\/aws \n            - export PATH=~\/bin\/aws:$PATH \n            - aws --version \n            # setup version \n            - export ECR_IMAGE=\"$ECR_ARN:$TAG\" \n            - echo $ECR_IMAGE \n            - echo $ECS_TASK_DEF_NAME \n            # update task definition \n            - rm -rf task-definition.json \n            - file_contents=$(&lt; task-definition-template.json ) \n            - echo \"${file_contents\/\/ECS_TASK_DEF_NAME\/$ECS_TASK_DEF_NAME}\" &gt; task-definition.json \n            - file_contents=$(&lt; task-definition.json ) \n            - echo \"${file_contents\/\/ECR_IMAGE\/$ECR_IMAGE}\" &gt; task-definition.json \n            - file_contents=$(&lt; task-definition.json ) \n            - echo \"${file_contents\/\/ECS_SERVICE\/$ECS_SERVICE}\" &gt; task-definition.json \n            - file_contents=$(&lt; task-definition.json ) \n            - echo \"${file_contents\/\/#env\/$env}\" &gt; task-definition.json \n            - cat task-definition.json \n            # register task  \n            - export TASK_VERSION=$((aws ecs register-task-definition --execution-role-arn $ECS_ROLE --cli-input-json file:\/\/\/opt\/atlassian\/pipelines\/agent\/build\/task-definition.json) | grep revision | cut -d \":\" -f2 | cut -d \",\" -f1 | tr -d ' ') \n            - echo $TASK_VERSION \n            # update ecs service \n            - aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --task-definition $ECS_TASK_DEF_NAME:$TASK_VERSION<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Image<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">image: atlassian\/default-image:2<\/pre>\n\n\n\n<p>This is the default build image which is an Ubuntu based linux image.<\/p>\n\n\n\n<p>Details of what is there by default is here <a href=\"https:\/\/hub.docker.com\/r\/atlassian\/default-image\/\">https:\/\/hub.docker.com\/r\/atlassian\/default-image\/<\/a><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Docker Definition&nbsp; &#8211; Memory<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">definitions: \n    services: \n      docker: \n        memory: 2048  # docker running out of memory<\/pre>\n\n\n\n<p>When building my docker image I was running out of memory.<\/p>\n\n\n\n<p>This definition allows you to control the amount of memory that the docker build image has access too.<br>By setting to 2GB this allowed my build to complete successfully<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">default<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">default:\n    - step:\n        name: startup pipeline\n        script:\n          - echo \"do nothing\"\n<\/pre>\n\n\n\n<p>Can someone explain why I need this?<br>Seems that I have issues if I do not run a default step, it seems to allow the build environment to come up and &#8220;checkout&#8221; the git repo, but I do nothing in this step.<\/p>\n\n\n\n<p>I have managed to remove this block, in later versions.<\/p>\n\n\n\n<p>What should really be here is a test block to allow the build to run tests, and then fail if the tests fail.Currently on my play React website I have no tests, so no test block<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Build Step<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">artifacts<\/h3>\n\n\n\n<p>This is a good trick. <\/p>\n\n\n\n<p>There is no way to pass calculated variables between steps.<br>The easiest way to do this is by artifacts.<br>You define an artifact in the step e.g. tag.txt <br>You create this file during the step and then this artifact is available for subsequent steps.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"> branches: \n    development:   \n      - step: \n          name: Build and publish to ECR \n          services: \n            - docker \n          caches: \n            - docker \n          artifacts: \n            - tag.txt<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">script<\/h3>\n\n\n\n<p>I will walk you through each section<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Env config.\n<ul class=\"wp-block-list\">\n<li>my build was crashing without a required variable in the .env file during the docker build<\/li>\n\n\n\n<li>I don&#8217;t want sensitive data committed to my git repo, so<\/li>\n\n\n\n<li>copy down the default .env file from the s3 bucket during the build step<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">            # setup env config \n            - echo \"setup .env configuration\" \n            - aws s3 cp $s3Bucket\/.env .<\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Setup the Tag Name\n<ul class=\"wp-block-list\">\n<li>The tag name is super important for versioning<\/li>\n\n\n\n<li>I am using a date stamp and build number to identify my build image<\/li>\n\n\n\n<li>$ECR_IMAGE is the full path to the image and the ECR<\/li>\n\n\n\n<li>$TAG is the tag of the build<\/li>\n\n\n\n<li>I echo the variables so I can make sure it is working correctly<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">            # setup build tags \n            - IMAGE_NAME=$BITBUCKET_REPO_SLUG \n            - export DATESTAMP=\"$(date +%Y-%m-%d)\" \n            - export TAG=\"$DATESTAMP.${BITBUCKET_BUILD_NUMBER}\" \n            - export ECR_IMAGE=\"$ECR_ARN:$TAG\" \n            - echo $TAG \n            - echo $ECR_IMAGE<\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Build the docker image\n<ul class=\"wp-block-list\">\n<li>Build the image<\/li>\n\n\n\n<li>Tag the build with our tag<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">            # docker build \n            - docker build -t $IMAGE_NAME:$TAG . \n            - docker tag $IMAGE_NAME:$TAG $ECR_ARN<\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Push to the ecr\n<ul class=\"wp-block-list\">\n<li>I am using the Atlassian pipe command to push<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">            # push to ecr - pipe\n            - pipe: atlassian\/aws-ecr-push-image:1.4.2\n              variables:\n                AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID\n                AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY\n                AWS_DEFAULT_REGION: $AWS_REGION\n                IMAGE_NAME: $IMAGE_NAME:$TAG\n                TAGS: $TAG<\/pre>\n\n\n\n<p><br>You can deploy without using the pipe command, it would look something like this;<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">            # push to ecr\n            - aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_IMAGE\n            - docker push $ECR_IMAGE<\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Artifacts\n<ul class=\"wp-block-list\">\n<li>The tag used to create the build is output to tag.txt<\/li>\n\n\n\n<li>this artifact is now available for subsequent steps<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">            # output artifacts \n            - echo $TAG &gt; tag.txt<\/pre>\n\n\n\n<p>So where are we now?<\/p>\n\n\n\n<p>The build has been completed and the docker image has been pushed up to aws ecr.<\/p>\n\n\n\n<p>Next step is to deploy your task definition<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Task Definition<\/h2>\n\n\n\n<p>The task definition is the spec of your task, which is the container running your application.<br>For the ability to deploy to different environments we need to be able to dynamically configure the task definition for each specific environment we are deploying too.<\/p>\n\n\n\n<p>To do this we create a task definition file that has been &#8220;Marked Up&#8221; with keywords.<br>These keywords are used in the pipeline to do a &#8220;find replace&#8221; to replace with dynamic values that configure the task definition for your environment.<br>This is how we are able to build a single image but deploy to multiple environments<\/p>\n\n\n\n<p>task-definition-template.json<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{\n    \"family\": \"ECS_TASK_DEF_NAME\", \n    \"networkMode\": \"awsvpc\", \n    \"containerDefinitions\": [\n        {\n            \"name\": \"ECS_SERVICE\", \n            \"image\": \"ECR_IMAGE\", \n            \"portMappings\": [\n                {\n                    \"containerPort\": 80, \n                    \"hostPort\": 80, \n                    \"protocol\": \"tcp\"\n                },\n                {\n                    \"containerPort\": 3000, \n                    \"hostPort\": 3000, \n                    \"protocol\": \"tcp\"\n                }\n            ],\n            \"environmentFiles\": [\n                {\n                    \"value\": \"arn:aws:s3:::s3-bitbucket-deployment\/my-website\/#env.env\", \n                    \"type\": \"s3\"\n                }\n            ], \n              \"logConfiguration\": {\n                  \"logDriver\": \"awslogs\",\n                  \"options\": {\n                      \"awslogs-group\": \"\/ecs\/ECS_TASK_DEF_NAME\",\n                      \"awslogs-region\": \"ap-southeast-2\",\n                      \"awslogs-stream-prefix\": \"ecs\"\n                  }\n              },\n            \"essential\": true\n        }\n    ], \n    \"requiresCompatibilities\": [\n        \"FARGATE\"\n    ], \n    \"cpu\": \"256\", \n    \"memory\": \"512\"\n  }<\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>ECS_TASK_DEF_NAME &#8211; the name of your task definition<\/li>\n\n\n\n<li>ECS_SERVICE- the name of the ECS service<\/li>\n\n\n\n<li>ECR_IMAGE &#8211; the full path to the ECR_IMAGE that has been passed<\/li>\n\n\n\n<li>#env &#8211; the environment e.g. dev \/ qa \/ prod\n<ul class=\"wp-block-list\">\n<li>needs to match the name of your environment file in the s3 bucket<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>Important items to note:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>family\n<ul class=\"wp-block-list\">\n<li>This MUST match the name of you task definition<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>name\n<ul class=\"wp-block-list\">\n<li>this MUST, MUST, MUST (REALLY IMPORTANT), match the name of your service.\n<ul class=\"wp-block-list\">\n<li>If the first time you create the task definition this value is set incorrectly then this task definition will NEVER WORK.<\/li>\n\n\n\n<li>you cannot edit the task definition as it seems that there is something special about the first definition that revisions do not change it and it will never work<\/li>\n\n\n\n<li>you cannot delete a task definition you can only mark all the definition versions as INACTIVE<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>environmentFiles\n<ul class=\"wp-block-list\">\n<li>This is how you can pass in &#8220;RUNTIME&#8221; environment variables for your react website<\/li>\n\n\n\n<li>The references to an s3Bucket allows storing a single file with your variables.<\/li>\n\n\n\n<li>file must be {name}.env to work.<\/li>\n\n\n\n<li>I will discuss environment files below.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Deployment Step<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">step deployment<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>deployment: dev\n<ul class=\"wp-block-list\">\n<li>This lines up with the deployment environments<\/li>\n\n\n\n<li>This is triggered automatically<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">- step: \n          name: dev deployment \n          deployment: dev<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">script<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>TAG<\/strong>\n<ul class=\"wp-block-list\">\n<li>Get Tag variable from artifact<\/li>\n\n\n\n<li>Read it from the artifact passed in from the previous step<\/li>\n\n\n\n<li>echo the tag for debugging<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">            # Get variables from build step \n            - export TAG=$(cat .\/tag.txt) \n            - echo $TAG<\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>AWS Setup<\/strong>\n<ul class=\"wp-block-list\">\n<li>we download the awscliv2.zip file<\/li>\n\n\n\n<li>extract and install it<\/li>\n\n\n\n<li>add ~bin\/aws to the PATH so that you can reference aws without needing to provide a path<\/li>\n\n\n\n<li>aws &#8211;version\n<ul class=\"wp-block-list\">\n<li>this line confirms that aws is installed and can be run successfully<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">            # setup aws  \n            - curl \"https:\/\/awscli.amazonaws.com\/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\" \n            - unzip awscliv2.zip \n            - .\/aws\/install -b ~\/bin\/aws \n            - export PATH=~\/bin\/aws:$PATH \n            - aws --version<\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Setup Version<\/strong>\n<ul class=\"wp-block-list\">\n<li>Recreate the version and variables to match the image that has been uploaded to the ECR<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">            # setup version \n            - export ECR_IMAGE=\"$ECR_ARN:$TAG\" \n            - echo $ECR_IMAGE \n            - echo $ECS_TASK_DEF_NAME<\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Update the task definition<\/strong>\n<ul class=\"wp-block-list\">\n<li>ensure that any previous task definition does not exist<\/li>\n\n\n\n<li>Load the template into memory and then find replace out to task-definition.json<\/li>\n\n\n\n<li>replace the 4 different tags into the task-definition<\/li>\n\n\n\n<li>cat task definition writes the contents of the task definition to the log so we confirm the find\/replace has worked<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">            # update task definition \n            - rm -rf task-definition.json \n            - file_contents=$(&lt; task-definition-template.json ) \n            - echo \"${file_contents\/\/ECS_TASK_DEF_NAME\/$ECS_TASK_DEF_NAME}\" &gt; task-definition.json \n            - file_contents=$(&lt; task-definition.json ) \n            - echo \"${file_contents\/\/ECR_IMAGE\/$ECR_IMAGE}\" &gt; task-definition.json \n            - file_contents=$(&lt; task-definition.json ) \n            - echo \"${file_contents\/\/ECS_SERVICE\/$ECS_SERVICE}\" &gt; task-definition.json \n            - file_contents=$(&lt; task-definition.json ) \n            - echo \"${file_contents\/\/#env\/$env}\" &gt; task-definition.json \n            - cat task-definition.json  <\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Register the task definition<\/strong>\n<ul class=\"wp-block-list\">\n<li>this creates a new version of the task definition<\/li>\n\n\n\n<li>We get that result into a $TASK_VERSION variable<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">            # register task  \n            - export TASK_VERSION=$((aws ecs register-task-definition --execution-role-arn $ECS_ROLE --cli-input-json file:\/\/\/opt\/atlassian\/pipelines\/agent\/build\/task-definition.json) | grep revision | cut -d \":\" -f2 | cut -d \",\" -f1 | tr -d ' ') \n            - echo $TASK_VERSION<\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Update the ECS Service<\/strong>\n<ul class=\"wp-block-list\">\n<li>connects the service to the new version of the task definition<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">            # update ecs service \n            - aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --task-definition $ECS_TASK_DEF_NAME:$TASK_VERSION<\/pre>\n\n\n\n<p>That is it, if you look at your service in the cluster you will see a new instance being provisioned and the new Task definition version. When the load balancer works out the new instance is healthy it, the old instance will be drained and then disposed of.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">NextJs environment files and Docker Images<\/h2>\n\n\n\n<p>One of the key elements to understand about how the above deployment actually works is to understand how environment files work in NextJs and Docker.<br>When a .env is created in a NextJs React website you add content like<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">MY_VAR=build<br>You can access this variable with;<\/pre>\n\n\n\n<pre class=\"wp-block-preformatted\">process.env.MY_VAR<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Docker build<\/h3>\n\n\n\n<p>When a docker build is run the .env file is COMPILED into the docker image AT BUILD TIME!<\/p>\n\n\n\n<p>But in our deployment we want to build a SINGLE image but deploy it to multiple environments.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Docker run passing in an environment file<\/h3>\n\n\n\n<p>Create a run.env file and put<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">MY_VAR=runtime<\/pre>\n\n\n\n<p>Run the following command<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">docker run --name my-website-container --env-file run.env -dp 5003:3000 my-website<\/pre>\n\n\n\n<p>This &#8212; env-file will pass in the contents of the run.env file and if you inspect the running image you will see that the MY_VAR will reflect the updated value runtime.<\/p>\n\n\n\n<p><br><strong>THE PROBLEM<\/strong><\/p>\n\n\n\n<p>However, try and access process.env.MY_VAR and it will still say <strong><em>build<\/em><\/strong><\/p>\n\n\n\n<p>So what is the problem?<\/p>\n\n\n\n<p>The system needs to be adjusted to access the environment variables at runtime.<br><strong>THE SOLUTION<\/strong><\/p>\n\n\n\n<p><a href=\"https:\/\/nextjs.org\/docs\/api-reference\/next.config.js\/runtime-configuration\">https:\/\/nextjs.org\/docs\/api-reference\/next.config.js\/runtime-configuration<\/a><br><\/p>\n\n\n\n<p>You need to access the environment variables at runtime.<\/p>\n\n\n\n<p>To do that&nbsp; you need to edit your next.config.js file<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">module.exports = { \n  serverRuntimeConfig: { \n    myServerVar: process.env.MY_VAR, \/\/ Pass through env variables but only available on the server side\n  }, \n  publicRuntimeConfig: { \n    \/\/ Will be available on both server and client \n    myPublicVar: process.env.MY_VAR, \/\/ pass through and available both server and client side\n  }, \n}<\/pre>\n\n\n\n<p>On a home page you would then be able to do the following;<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">import getConfig from \"next\/config\"; \nconst { publicRuntimeConfig } = getConfig();\n\nexport default function Home() { \n  return ( \n    &lt;div className={styles.container}&gt; \n      &lt;Head&gt; \n        &lt;title&gt;Create Next App&lt;\/title&gt; \n        &lt;meta name=\"description\" content=\"Generated by create next app\" \/&gt; \n        &lt;link rel=\"icon\" href=\"\/favicon.ico\" \/&gt; \n      &lt;\/Head&gt; \n      &lt;main className={styles.main}&gt; \n        &lt;h1 className={styles.title}&gt; \n          Welcome to &lt;a href=\"https:\/\/nextjs.org\"&gt;Next.js!&lt;\/a&gt; \n        &lt;\/h1&gt; \n        &lt;h3&gt;MY_VAR&lt;\/h3&gt; \n        &lt;div&gt;{publicRuntimeConfig.myPublicVar}&lt;div&gt; \n          \n      &lt;\/main&gt; \n      &lt;footer className={styles.footer}&gt; \n        &lt;a \n          href=\"https:\/\/vercel.com?utm_source=create-next-app&amp;utm_medium=default-template&amp;utm_campaign=create-next-app\" \n          target=\"_blank\" \n          rel=\"noopener noreferrer\" \n        &gt; \n          Powered by{' '} \n          &lt;span className={styles.logo}&gt; \n            &lt;Image src=\"\/vercel.svg\" alt=\"Vercel Logo\" width={72} height={16} \/&gt; \n          &lt;\/span&gt; \n        &lt;\/a&gt; \n      &lt;\/footer&gt; \n    &lt;\/div&gt; \n  ) \n} \nexport const getServerSideProps = () =&gt; { \n  return {props: {}}; \n}<\/pre>\n\n\n\n<p>If you have this working in your local docker images with the &#8211;env-file passing in the environment file..<\/p>\n\n\n\n<p>Then when you create&nbsp; your task definition with; <\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">            \"environmentFiles\": [ \n                { \n                    \"value\": \"arn:aws:s3:::s3-bitbucket-deployment\/my-website\/#env.env\",  \n                    \"type\": \"s3\" \n                } \n            ],<\/pre>\n\n\n\n<p><br>This is exactly what AWS does it essentially runs the docker container with this -env-file variable but linking to your file in the S3 bucket.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Final Thoughts<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Triggers<\/h3>\n\n\n\n<p>The QA deployment is a manual deployment as specified by the manual trigger<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">      - step:  \n          name: qa deployment  \n          deployment: qa  \n          trigger: manual<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Versioning<\/h3>\n\n\n\n<p>Using the task definition I can pass in individual values at runtime as well<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{  \n    \"family\": \"\",  \n    \"containerDefinitions\": [  \n        {  \n            \"name\": \"\",  \n            \"image\": \"\",  \n            ...  \n            \"environment\": [  \n                {  \n                    \"name\": \"website-version\",  \n                    \"value\": \"SITE_VERSION\"  \n                }  \n            ],  \n            ...  \n        }  \n    ],  \n    ...  \n}<\/pre>\n\n\n\n<p>I am going to pass through the $TAG into a variable SITE_VERSION.<br>Building on 1-Jan-2022 and pipeline build # 43 would create a TAG=&#8221;2020-01-01.43&#8243; and this would be passed into the environment settings.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Branches<\/h3>\n\n\n\n<p>The build file as presented would promote a commit into development to the dev environment which could be promoted to QA and all the way to production, (COPY \/ PASTE the QA deployment and call it Prod).<\/p>\n\n\n\n<p>I think this may cause problems. I think it may be better to leave the dev environment triggering off the development branch.QA should be triggered off the master branch, so this way the following would be the deployment approach;<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Developers Pull Requests are merged into development and deployed to the dev environment<\/li>\n\n\n\n<li>when we are confident with the build we can do a PR into master<\/li>\n\n\n\n<li>master triggers the QA build.<\/li>\n<\/ul>\n\n\n\n<p>By separating dev and QA environments by connecting QA to the master branch, you have the ability that if a change gets into development that breaks the build, you could create a release branch of the validated features only and then merge those features through into the master branch which then is able to be tested in QA.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Tags<\/h3>\n\n\n\n<p>Some builds can be triggered off tags in bitbucket pipelines.<br>If I wanted to perform a feature release into dev or qa without committing it to the development or mast branch, then this could be achieved via tags.A pipeline tag ;<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>feature-dev-*<\/li>\n\n\n\n<li>feature-qa-*<\/li>\n<\/ul>\n\n\n\n<p>could trigger a specific release of a feature branch into a specific environment so it could be &#8220;smoke tested&#8221; prior to pushing the code into the development or master branch<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Thats it. Hope these detailed instructions help. Like everything in software development there is no right or wrong way to do things, but this is my solution to handle the ability to build a docker image once but deploy it multiple times to multiple environments.<\/p>\n\n\n\n<p>The beauty of this, is that the code you tested in development is the same that can end up in production as you are using the EXACT SAME ECR IMAGE, it is the same code. build once deploy multiple times.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Settle back for a big post on nextJs variables, bitbucket pipeleines and ECS containers.. Recently I was given the challenge to work out the best [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[32,48,52,50,51],"tags":[38,41,39,42],"class_list":["post-1788","post","type-post","status-publish","format-standard","hentry","category-aws","category-aws-ecs","category-bitbucket","category-ci-cd","category-pipelines","tag-aws","tag-bitbucket","tag-ecr","tag-pipelines"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/ntsblog.homedev.com.au\/index.php\/wp-json\/wp\/v2\/posts\/1788","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ntsblog.homedev.com.au\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ntsblog.homedev.com.au\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ntsblog.homedev.com.au\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/ntsblog.homedev.com.au\/index.php\/wp-json\/wp\/v2\/comments?post=1788"}],"version-history":[{"count":0,"href":"https:\/\/ntsblog.homedev.com.au\/index.php\/wp-json\/wp\/v2\/posts\/1788\/revisions"}],"wp:attachment":[{"href":"https:\/\/ntsblog.homedev.com.au\/index.php\/wp-json\/wp\/v2\/media?parent=1788"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ntsblog.homedev.com.au\/index.php\/wp-json\/wp\/v2\/categories?post=1788"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ntsblog.homedev.com.au\/index.php\/wp-json\/wp\/v2\/tags?post=1788"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}